Files
kjol/go/tw/tailwind.go
2026-07-17 16:58:16 -04:00

9816 lines
301 KiB
Go
Raw Blame History

package tw
// Native Go Tailwind v4 compiler. A from-scratch, pure-Go implementation of the
// Tailwind v4 engine — CSS parser (AST), candidate scanner, utility/variant
// generation, design system + theme resolution, sorting, and preflight — with no
// Node, no goja, no official tailwindcss distribution. Consolidated here from the
// former cmd/bundle/tw_*.go engine. Entry points: twCompile (compile a config +
// candidates to CSS) and scanSources (scan source files for utility candidates).
import (
_ "embed"
"fmt"
"math"
"math/big"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
)
// Port of packages/tailwindcss/src/ast.ts
//
// A single mutable node struct with a Kind discriminator (rather than one Go
// type per kind). This mirrors the upstream structurally-typed objects and is
// required because variants morph a node's kind in place (the equivalent of
// `Object.assign(node, styleRule(...))`), which is `*node = *other` here.
//
// Deviations (tracked follow-ups):
// @INCOMPLETE optimizeAst does not yet prune unused @theme variables/keyframes
// or emit color-mix()/@property browser fallbacks (Polyfills). -mta
type nodeKind int
const (
nRule nodeKind = iota
nAtRule
nDeclaration
nComment
nContext
nAtRoot
)
type AstNode struct {
Kind nodeKind
// rule
Selector string
// at-rule
Name string
Params string
// declaration
Property string
Value string
Important bool
Undefined bool // value is `undefined` upstream; dropped by optimizeAst
// context
Context map[string]string
// children: rule / at-rule / context / at-root
Nodes []*AstNode
}
// ---- factories ----------------------------------------------------------
func styleRule(selector string, nodes ...*AstNode) *AstNode {
return &AstNode{Kind: nRule, Selector: selector, Nodes: nodes}
}
func atRule(name, params string, nodes ...*AstNode) *AstNode {
return &AstNode{Kind: nAtRule, Name: name, Params: params, Nodes: nodes}
}
func rule(selector string, nodes ...*AstNode) *AstNode {
if len(selector) > 0 && selector[0] == '@' {
return parseAtRule(selector, nodes)
}
return styleRule(selector, nodes...)
}
func decl(property, value string) *AstNode {
return &AstNode{Kind: nDeclaration, Property: property, Value: value}
}
func declImportant(property, value string) *AstNode {
return &AstNode{Kind: nDeclaration, Property: property, Value: value, Important: true}
}
func comment(value string) *AstNode { return &AstNode{Kind: nComment, Value: value} }
func contextNode(ctx map[string]string, nodes []*AstNode) *AstNode {
return &AstNode{Kind: nContext, Context: ctx, Nodes: nodes}
}
func atRoot(nodes []*AstNode) *AstNode { return &AstNode{Kind: nAtRoot, Nodes: nodes} }
// nodeChildren returns a pointer to a node's child slice, or nil for leaves.
func nodeChildren(n *AstNode) *[]*AstNode {
switch n.Kind {
case nRule, nAtRule, nContext, nAtRoot:
return &n.Nodes
}
return nil
}
// ---- clone --------------------------------------------------------------
func cloneAstNode(node *AstNode) *AstNode {
cp := *node
if node.Nodes != nil {
cp.Nodes = cloneAstNodes(node.Nodes)
}
if node.Context != nil {
m := make(map[string]string, len(node.Context))
for k, v := range node.Context {
m[k] = v
}
cp.Context = m
}
return &cp
}
func cloneAstNodes(nodes []*AstNode) []*AstNode {
out := make([]*AstNode, len(nodes))
for i, n := range nodes {
out[i] = cloneAstNode(n)
}
return out
}
// ---- optimize -----------------------------------------------------------
func optimizeAst(ast []*AstNode, ds *DesignSystem) []*AstNode {
var atRoots []*AstNode
seenAtProperties := make(map[string]bool)
var transform func(node *AstNode, parent *[]*AstNode, ctx map[string]bool, depth int)
transform = func(node *AstNode, parent *[]*AstNode, ctx map[string]bool, depth int) {
switch node.Kind {
case nDeclaration:
if node.Property == "--tw-sort" || node.Undefined {
return
}
if ctx["theme"] && strings.HasPrefix(node.Property, "--") && node.Value == "initial" {
return
}
if ds != nil && strings.Contains(node.Value, "var(") {
if !(ctx["theme"] && strings.HasPrefix(node.Property, "--")) {
ds.trackUsedVariables(node.Value)
}
}
*parent = append(*parent, node)
case nRule:
var nodes []*AstNode
for _, child := range node.Nodes {
transform(child, &nodes, ctx, depth+1)
}
nodes = dedupeDeclarations(nodes)
if len(nodes) == 0 {
return
}
if node.Selector == "&" {
*parent = append(*parent, nodes...)
} else {
*parent = append(*parent, &AstNode{Kind: nRule, Selector: node.Selector, Nodes: nodes})
}
case nAtRule:
if node.Name == "@property" && depth == 0 {
if seenAtProperties[node.Params] {
return
}
seenAtProperties[node.Params] = true
var copyNodes []*AstNode
for _, child := range node.Nodes {
transform(child, &copyNodes, ctx, depth+1)
}
*parent = append(*parent, &AstNode{Kind: nAtRule, Name: node.Name, Params: node.Params, Nodes: copyNodes})
return
}
childCtx := ctx
if node.Name == "@keyframes" {
childCtx = mergeCtx(ctx, "keyframes")
} else if node.Name == "@supports" && strings.Contains(node.Params, "color-mix(") {
childCtx = mergeCtx(ctx, "supportsColorMix")
}
var copyNodes []*AstNode
for _, child := range node.Nodes {
transform(child, &copyNodes, childCtx, depth+1)
}
if len(copyNodes) > 0 ||
node.Name == "@layer" || node.Name == "@charset" || node.Name == "@custom-media" ||
node.Name == "@namespace" || node.Name == "@import" || node.Name == "@apply" {
*parent = append(*parent, &AstNode{Kind: nAtRule, Name: node.Name, Params: node.Params, Nodes: copyNodes})
}
case nAtRoot:
for _, child := range node.Nodes {
var newParent []*AstNode
transform(child, &newParent, ctx, 0)
atRoots = append(atRoots, newParent...)
}
case nContext:
if node.Context["reference"] != "" {
return
}
merged := ctx
for k := range node.Context {
merged = mergeCtx(merged, k)
}
for _, child := range node.Nodes {
transform(child, parent, merged, depth)
}
case nComment:
*parent = append(*parent, node)
}
}
var newAst []*AstNode
for _, node := range ast {
transform(node, &newAst, map[string]bool{}, 0)
}
newAst = append(newAst, atRoots...)
return newAst
}
func mergeCtx(ctx map[string]bool, key string) map[string]bool {
m := make(map[string]bool, len(ctx)+1)
for k, v := range ctx {
m[k] = v
}
m[key] = true
return m
}
func dedupeDeclarations(nodes []*AstNode) []*AstNode {
seen := map[string][]int{}
for i, child := range nodes {
if child.Kind != nDeclaration {
continue
}
key := child.Property + ":" + child.Value + ":"
if child.Important {
key += "!"
}
seen[key] = append(seen[key], i)
}
remove := map[int]bool{}
for _, idxs := range seen {
for i := 0; i < len(idxs)-1; i++ {
remove[idxs[i]] = true
}
}
if len(remove) == 0 {
return nodes
}
out := make([]*AstNode, 0, len(nodes))
for i, n := range nodes {
if !remove[i] {
out = append(out, n)
}
}
return out
}
// ---- serialization ------------------------------------------------------
func toCss(ast []*AstNode) string {
var b strings.Builder
for _, node := range ast {
stringifyNode(&b, node, 0)
}
return b.String()
}
func stringifyNode(b *strings.Builder, node *AstNode, depth int) {
indent := strings.Repeat(" ", depth)
switch node.Kind {
case nDeclaration:
if node.Undefined {
return
}
b.WriteString(indent)
b.WriteString(node.Property)
b.WriteString(": ")
b.WriteString(node.Value)
if node.Important {
b.WriteString(" !important")
}
b.WriteString(";\n")
case nRule:
b.WriteString(indent)
b.WriteString(node.Selector)
b.WriteString(" {\n")
for _, child := range node.Nodes {
stringifyNode(b, child, depth+1)
}
b.WriteString(indent)
b.WriteString("}\n")
case nAtRule:
if len(node.Nodes) == 0 {
b.WriteString(indent)
b.WriteString(node.Name)
if node.Params != "" {
b.WriteString(" ")
b.WriteString(node.Params)
}
b.WriteString(";\n")
return
}
b.WriteString(indent)
b.WriteString(node.Name)
if node.Params != "" {
b.WriteString(" ")
b.WriteString(node.Params)
}
b.WriteString(" {\n")
for _, child := range node.Nodes {
stringifyNode(b, child, depth+1)
}
b.WriteString(indent)
b.WriteString("}\n")
case nComment:
b.WriteString(indent)
b.WriteString("/*")
b.WriteString(node.Value)
b.WriteString("*/\n")
}
}
func extractUsedVariables(value string) []string {
if !strings.Contains(value, "var(") {
return nil
}
var out []string
var rec func(nodes []ValueNode)
rec = func(nodes []ValueNode) {
for _, n := range nodes {
f, ok := n.(*ValueFunction)
if !ok {
continue
}
if f.Value == "var" || strings.HasSuffix(f.Value, "_var") {
if len(f.Nodes) > 0 {
if w, ok := f.Nodes[0].(*ValueWord); ok && strings.HasPrefix(w.Value, "--") {
out = append(out, w.Value)
}
}
}
rec(f.Nodes)
}
}
rec(valueParse(value))
return out
}
// Port of packages/tailwindcss/src/candidate.ts
//
// Parses a class name into structured candidate(s). The TS discriminated unions
// are modeled as structs with a Kind field. parseCandidate may yield multiple
// candidates (e.g. a static match plus functional roots); the caller compiles
// the first that succeeds.
var reValidNamedValue = regexp.MustCompile(`^[a-zA-Z0-9_.%-]+$`)
type candidateKind int
const (
candArbitrary candidateKind = iota
candStatic
candFunctional
)
type modifierKind int
const (
modArbitrary modifierKind = iota
modNamed
)
type CandidateModifier struct {
Kind modifierKind
Value string
}
type utilityValueKind int
const (
uvArbitrary utilityValueKind = iota
uvNamed
)
// UtilityValue is the value of a functional candidate. DataType/Fraction are ""
// when absent (upstream null).
type UtilityValue struct {
Kind utilityValueKind
DataType string // arbitrary only
Value string
Fraction string // named only
}
type variantKind int
const (
varArbitrary variantKind = iota
varStatic
varFunctional
varCompound
)
type variantValueKind int
const (
vvArbitrary variantValueKind = iota
vvNamed
)
type VariantValue struct {
Kind variantValueKind
Value string
}
type Variant struct {
Kind variantKind
// arbitrary
Selector string
Relative bool
// static / functional / compound
Root string
// functional
Value *VariantValue
// functional / compound
Modifier *CandidateModifier
// compound
Variant *Variant
}
type Candidate struct {
Kind candidateKind
// arbitrary
Property string
ArbitraryValue string
// static / functional
Root string
// functional
Value *UtilityValue
// arbitrary / functional
Modifier *CandidateModifier
Variants []*Variant
Important bool
Raw string
}
type rootMatch struct {
root string
value *string // nil = null
}
func parseCandidate(input string, ds *DesignSystem) []*Candidate {
rawVariants := segment(input, ":")
if ds.theme.Prefix != "" {
if len(rawVariants) == 1 {
return nil
}
if rawVariants[0] != ds.theme.Prefix {
return nil
}
rawVariants = rawVariants[1:]
}
base := rawVariants[len(rawVariants)-1]
rawVariants = rawVariants[:len(rawVariants)-1]
var parsedVariants []*Variant
for i := len(rawVariants) - 1; i >= 0; i-- {
pv := ds.parseVariant(rawVariants[i])
if pv == nil {
return nil
}
parsedVariants = append(parsedVariants, pv)
}
important := false
if len(base) > 0 && base[len(base)-1] == '!' {
important = true
base = base[:len(base)-1]
} else if len(base) > 0 && base[0] == '!' {
important = true
base = base[1:]
}
var out []*Candidate
if ds.utilities.has(base, utilStatic) && !strings.Contains(base, "[") {
out = append(out, &Candidate{
Kind: candStatic, Root: base, Variants: parsedVariants, Important: important, Raw: input,
})
}
parts := segment(base, "/")
baseWithoutModifier := parts[0]
var modifierSegment *string
if len(parts) >= 2 {
modifierSegment = &parts[1]
}
if len(parts) >= 3 {
return out // more than one modifier -> invalid
}
var parsedModifier *CandidateModifier
if modifierSegment != nil {
parsedModifier = parseModifier(*modifierSegment)
if parsedModifier == nil {
return out
}
}
// Arbitrary property, e.g. [color:red]
if len(baseWithoutModifier) > 0 && baseWithoutModifier[0] == '[' {
if baseWithoutModifier[len(baseWithoutModifier)-1] != ']' {
return out
}
charCode := baseWithoutModifier[1]
if charCode != '-' && !(charCode >= 'a' && charCode <= 'z') {
return out
}
inner := baseWithoutModifier[1 : len(baseWithoutModifier)-1]
idx := strings.IndexByte(inner, ':')
if idx == -1 || idx == 0 || idx == len(inner)-1 {
return out
}
property := inner[:idx]
value := decodeArbitraryValue(inner[idx+1:])
if !isValidArbitrary(value) {
return out
}
out = append(out, &Candidate{
Kind: candArbitrary, Property: property, ArbitraryValue: value,
Modifier: parsedModifier, Variants: parsedVariants, Important: important, Raw: input,
})
return out
}
var roots []rootMatch
if n := len(baseWithoutModifier); n > 0 && baseWithoutModifier[n-1] == ']' {
idx := strings.Index(baseWithoutModifier, "-[")
if idx == -1 {
return out
}
root := baseWithoutModifier[:idx]
if !ds.utilities.has(root, utilFunctional) {
return out
}
value := baseWithoutModifier[idx+1:]
roots = []rootMatch{{root: root, value: &value}}
} else if n > 0 && baseWithoutModifier[n-1] == ')' {
idx := strings.Index(baseWithoutModifier, "-(")
if idx == -1 {
return out
}
root := baseWithoutModifier[:idx]
if !ds.utilities.has(root, utilFunctional) {
return out
}
value := baseWithoutModifier[idx+2 : len(baseWithoutModifier)-1]
vparts := segment(value, ":")
dataType := ""
if len(vparts) == 2 {
dataType = vparts[0]
value = vparts[1]
}
if len(value) < 2 || value[0] != '-' || value[1] != '-' {
return out
}
if !isValidArbitrary(value) {
return out
}
var wrapped string
if dataType == "" {
wrapped = "[var(" + value + ")]"
} else {
wrapped = "[" + dataType + ":var(" + value + ")]"
}
roots = []rootMatch{{root: root, value: &wrapped}}
} else {
roots = findRoots(baseWithoutModifier, func(r string) bool { return ds.utilities.has(r, utilFunctional) })
}
for _, rm := range roots {
cand := &Candidate{
Kind: candFunctional, Root: rm.root, Modifier: parsedModifier, Value: nil,
Variants: parsedVariants, Important: important, Raw: input,
}
if rm.value == nil {
out = append(out, cand)
continue
}
value := *rm.value
startArb := strings.IndexByte(value, '[')
if startArb != -1 {
if value[len(value)-1] != ']' {
return out
}
arbitraryValue := decodeArbitraryValue(value[startArb+1 : len(value)-1])
if !isValidArbitrary(arbitraryValue) {
continue
}
typehint := ""
typehintFound := false
for i := 0; i < len(arbitraryValue); i++ {
code := arbitraryValue[i]
if code == ':' {
typehint = arbitraryValue[:i]
arbitraryValue = arbitraryValue[i+1:]
typehintFound = true
break
}
if code == '-' || (code >= 'a' && code <= 'z') {
continue
}
break
}
if len(arbitraryValue) == 0 || strings.TrimSpace(arbitraryValue) == "" {
continue
}
if typehintFound && typehint == "" {
continue
}
cand.Value = &UtilityValue{Kind: uvArbitrary, DataType: typehint, Value: arbitraryValue}
} else {
fraction := ""
if modifierSegment != nil && !(parsedModifier != nil && parsedModifier.Kind == modArbitrary) {
fraction = value + "/" + *modifierSegment
}
if !reValidNamedValue.MatchString(value) {
continue
}
cand.Value = &UtilityValue{Kind: uvNamed, Value: value, Fraction: fraction}
}
out = append(out, cand)
}
return out
}
func parseModifier(modifier string) *CandidateModifier {
if len(modifier) >= 2 && modifier[0] == '[' && modifier[len(modifier)-1] == ']' {
arb := decodeArbitraryValue(modifier[1 : len(modifier)-1])
if !isValidArbitrary(arb) {
return nil
}
if len(arb) == 0 || strings.TrimSpace(arb) == "" {
return nil
}
return &CandidateModifier{Kind: modArbitrary, Value: arb}
}
if len(modifier) >= 2 && modifier[0] == '(' && modifier[len(modifier)-1] == ')' {
inner := modifier[1 : len(modifier)-1]
if len(inner) < 2 || inner[0] != '-' || inner[1] != '-' {
return nil
}
if !isValidArbitrary(inner) {
return nil
}
arb := decodeArbitraryValue("var(" + inner + ")")
return &CandidateModifier{Kind: modArbitrary, Value: arb}
}
if !reValidNamedValue.MatchString(modifier) {
return nil
}
return &CandidateModifier{Kind: modNamed, Value: modifier}
}
func parseVariant(variant string, ds *DesignSystem) *Variant {
// Arbitrary variants, e.g. [&_p]
if len(variant) >= 2 && variant[0] == '[' && variant[len(variant)-1] == ']' {
if variant[1] == '@' && strings.Contains(variant, "&") {
return nil
}
selector := decodeArbitraryValue(variant[1 : len(variant)-1])
if !isValidArbitrary(selector) {
return nil
}
if len(selector) == 0 || strings.TrimSpace(selector) == "" {
return nil
}
relative := selector[0] == '>' || selector[0] == '+' || selector[0] == '~'
if !relative && selector[0] != '@' && !strings.Contains(selector, "&") {
selector = "&:is(" + selector + ")"
}
return &Variant{Kind: varArbitrary, Selector: selector, Relative: relative}
}
parts := segment(variant, "/")
variantWithoutModifier := parts[0]
var modifier *string
if len(parts) >= 2 {
modifier = &parts[1]
}
if len(parts) >= 3 {
return nil
}
roots := findRoots(variantWithoutModifier, func(r string) bool { return ds.variants.has(r) })
for _, rm := range roots {
root := rm.root
value := rm.value
switch ds.variants.kind(root) {
case varStatic:
if value != nil {
return nil
}
if modifier != nil {
return nil
}
return &Variant{Kind: varStatic, Root: root}
case varFunctional:
var parsedModifier *CandidateModifier
if modifier != nil {
parsedModifier = parseModifier(*modifier)
if parsedModifier == nil {
return nil
}
}
if value == nil {
return &Variant{Kind: varFunctional, Root: root, Modifier: parsedModifier, Value: nil}
}
v := *value
if v[len(v)-1] == ']' {
if v[0] != '[' {
continue
}
arb := decodeArbitraryValue(v[1 : len(v)-1])
if !isValidArbitrary(arb) {
return nil
}
if len(arb) == 0 || strings.TrimSpace(arb) == "" {
return nil
}
return &Variant{Kind: varFunctional, Root: root, Modifier: parsedModifier, Value: &VariantValue{Kind: vvArbitrary, Value: arb}}
}
if v[len(v)-1] == ')' {
if v[0] != '(' {
continue
}
arb := decodeArbitraryValue(v[1 : len(v)-1])
if !isValidArbitrary(arb) {
return nil
}
if len(arb) == 0 || strings.TrimSpace(arb) == "" {
return nil
}
if len(arb) < 2 || arb[0] != '-' || arb[1] != '-' {
return nil
}
return &Variant{Kind: varFunctional, Root: root, Modifier: parsedModifier, Value: &VariantValue{Kind: vvArbitrary, Value: "var(" + arb + ")"}}
}
if !reValidNamedValue.MatchString(v) {
continue
}
return &Variant{Kind: varFunctional, Root: root, Modifier: parsedModifier, Value: &VariantValue{Kind: vvNamed, Value: v}}
case varCompound:
if value == nil {
return nil
}
v := *value
mod := modifier
if mod != nil && (root == "not" || root == "has" || root == "in") {
v = v + "/" + *mod
mod = nil
}
subVariant := ds.parseVariant(v)
if subVariant == nil {
return nil
}
if !ds.variants.compoundsWith(root, subVariant) {
return nil
}
var parsedModifier *CandidateModifier
if mod != nil {
parsedModifier = parseModifier(*mod)
if parsedModifier == nil {
return nil
}
}
return &Variant{Kind: varCompound, Root: root, Modifier: parsedModifier, Variant: subVariant}
}
}
return nil
}
func findRoots(input string, exists func(string) bool) []rootMatch {
var out []rootMatch
if exists(input) {
out = append(out, rootMatch{root: input, value: nil})
}
idx := strings.LastIndexByte(input, '-')
for idx > 0 {
maybeRoot := input[:idx]
if exists(maybeRoot) {
val := input[idx+1:]
if val == "" {
break
}
if len(maybeRoot) > 0 && maybeRoot[0] == '@' && exists("@") && input[idx] == '-' {
break
}
v := val
out = append(out, rootMatch{root: maybeRoot, value: &v})
}
idx = strings.LastIndexByte(input[:idx], '-')
}
if len(input) > 0 && input[0] == '@' && exists("@") {
v := input[1:]
out = append(out, rootMatch{root: "@", value: &v})
}
return out
}
// Port of packages/tailwindcss/src/utils/compare-breakpoints.ts
var reBpDigits = regexp.MustCompile(`[\d.]+`)
func parseIntJS(s string) (int, bool) {
i := 0
for i < len(s) && (s[i] == ' ' || s[i] == '\t' || s[i] == '\n' || s[i] == '\r' || s[i] == '\f' || s[i] == '\v') {
i++
}
sign := 1
if i < len(s) && (s[i] == '+' || s[i] == '-') {
if s[i] == '-' {
sign = -1
}
i++
}
start := i
n := 0
for i < len(s) && s[i] >= '0' && s[i] <= '9' {
n = n*10 + int(s[i]-'0')
i++
}
if i == start {
return 0, false
}
return sign * n, true
}
func compareBreakpoints(a, z, direction string) int {
if a == z {
return 0
}
aIs := strings.IndexByte(a, '(')
zIs := strings.IndexByte(z, '(')
var aBucket, zBucket string
if aIs == -1 {
aBucket = reBpDigits.ReplaceAllString(a, "")
} else {
aBucket = a[:aIs]
}
if zIs == -1 {
zBucket = reBpDigits.ReplaceAllString(z, "")
} else {
zBucket = z[:zIs]
}
if aBucket != zBucket {
if aBucket < zBucket {
return -1
}
return 1
}
ai, aok := parseIntJS(a)
zi, zok := parseIntJS(z)
if !aok || !zok {
if a < z {
return -1
}
return 1
}
if direction == "asc" {
return ai - zi
}
return zi - ai
}
// Port of packages/tailwindcss/src/compile.ts
type CompileAstFlags int
const (
CompileNone CompileAstFlags = 0
RespectImportant CompileAstFlags = 1 << 0
)
type propertySort struct {
order []int
count int
}
type compiledNode struct {
node *AstNode
propertySort propertySort
}
type nodeSortMeta struct {
properties propertySort
variants *big.Int
candidate string
}
var twPropertyOrderIndex = func() map[string]int {
m := make(map[string]int, len(twPropertyOrder))
for i, p := range twPropertyOrder {
if _, ok := m[p]; !ok {
m[p] = i
}
}
return m
}()
func propOrderIndex(p string) int {
if i, ok := twPropertyOrderIndex[p]; ok {
return i
}
return -1
}
func utilKindMatchesCandidate(uk utilKind, ck candidateKind) bool {
return (uk == utilStatic && ck == candStatic) || (uk == utilFunctional && ck == candFunctional)
}
func isFallbackUtility(u *Utility) bool {
if u.options == nil {
return false
}
types := u.options.Types
if len(types) <= 1 {
return false
}
for _, t := range types {
if t == "any" {
return true
}
}
return false
}
func compileBaseUtility(candidate *Candidate, ds *DesignSystem) [][]*AstNode {
if candidate.Kind == candArbitrary {
value := candidate.ArbitraryValue
if candidate.Modifier != nil {
v, ok := asColor(value, candidate.Modifier, ds.theme)
if !ok {
return nil
}
value = v
}
return [][]*AstNode{{decl(candidate.Property, value)}}
}
utils := ds.utilities.get(candidate.Root)
var asts [][]*AstNode
run := func(list []*Utility) (bail bool) {
for _, utility := range list {
if !utilKindMatchesCandidate(utility.kind, candidate.Kind) {
continue
}
res := utility.compileFn(candidate)
if res == nil {
continue
}
if res.null {
if utility.options != nil && len(utility.options.Types) > 0 {
return true
}
continue
}
asts = append(asts, res.nodes)
}
return false
}
var normal, fallback []*Utility
for _, u := range utils {
if isFallbackUtility(u) {
fallback = append(fallback, u)
} else {
normal = append(normal, u)
}
}
if run(normal) {
return asts
}
if len(asts) > 0 {
return asts
}
if run(fallback) {
return asts
}
return asts
}
func compileAstNodes(candidate *Candidate, ds *DesignSystem, flags CompileAstFlags) []compiledNode {
asts := compileBaseUtility(candidate, ds)
if len(asts) == 0 {
return nil
}
respectImportant := ds.important && (flags&RespectImportant != 0)
selector := "." + escape(candidate.Raw)
var rules []compiledNode
for _, nodes := range asts {
ps := getPropertySort(nodes)
if candidate.Important || respectImportant {
applyImportant(nodes)
}
node := styleRule(selector, nodes...)
ok := true
for _, variant := range candidate.Variants {
if !applyVariant(node, variant, ds.variants, 0) {
ok = false
break
}
}
if !ok {
return nil
}
rules = append(rules, compiledNode{node: node, propertySort: ps})
}
return rules
}
func applyVariant(node *AstNode, variant *Variant, variants *Variants, depth int) bool {
if variant.Kind == varArbitrary {
if variant.Relative && depth == 0 {
return false
}
node.Nodes = []*AstNode{rule(variant.Selector, node.Nodes...)}
return true
}
info := variants.get(variant.Root)
if info == nil {
return false
}
if variant.Kind == varCompound {
isolated := atRule("@slot", "")
if !applyVariant(isolated, variant.Variant, variants, depth+1) {
return false
}
if variant.Root == "not" && len(isolated.Nodes) > 1 {
return false
}
for _, child := range isolated.Nodes {
if child.Kind != nRule && child.Kind != nAtRule {
return false
}
if !info.applyFn(child, variant) {
return false
}
}
nodesCopy := isolated.Nodes
walkAst(&nodesCopy, func(child *AstNode, _ *VisitContext) WalkResult {
if (child.Kind == nRule || child.Kind == nAtRule) && len(child.Nodes) <= 0 {
child.Nodes = node.Nodes
return WSkip
}
return WContinue
})
node.Nodes = isolated.Nodes
return true
}
return info.applyFn(node, variant)
}
func applyImportant(ast []*AstNode) {
for _, node := range ast {
if node.Kind == nAtRoot {
continue
}
if node.Kind == nDeclaration {
node.Important = true
} else if node.Kind == nRule || node.Kind == nAtRule {
applyImportant(node.Nodes)
}
}
}
func getPropertySort(nodes []*AstNode) propertySort {
orderSet := map[int]bool{}
count := 0
q := append([]*AstNode{}, nodes...)
seenTwSort := false
for len(q) > 0 {
node := q[0]
q = q[1:]
if node.Kind == nDeclaration {
if node.Undefined {
continue
}
count++
if seenTwSort {
continue
}
if node.Property == "--tw-sort" {
idx := propOrderIndex(node.Value)
if idx != -1 {
orderSet[idx] = true
seenTwSort = true
continue
}
}
idx := propOrderIndex(node.Property)
if idx != -1 {
orderSet[idx] = true
}
} else if node.Kind == nRule || node.Kind == nAtRule {
q = append(q, node.Nodes...)
}
}
order := make([]int, 0, len(orderSet))
for k := range orderSet {
order = append(order, k)
}
sort.Ints(order)
return propertySort{order: order, count: count}
}
func compileCandidates(rawCandidates []string, ds *DesignSystem, onInvalid func(string), respectImportant bool) ([]*AstNode, map[*AstNode]nodeSortMeta) {
nodeSorting := map[*AstNode]nodeSortMeta{}
var astNodes []*AstNode
matches := map[string][]*Candidate{}
var order []string
for _, raw := range rawCandidates {
if ds.invalidCandidates[raw] {
if onInvalid != nil {
onInvalid(raw)
}
continue
}
cands := ds.parseCandidate(raw)
if len(cands) == 0 {
if onInvalid != nil {
onInvalid(raw)
}
continue
}
if _, ok := matches[raw]; !ok {
order = append(order, raw)
}
matches[raw] = cands
}
flags := CompileNone
if respectImportant {
flags |= RespectImportant
}
variantOrderMap := ds.getVariantOrder()
for _, raw := range order {
cands := matches[raw]
found := false
for _, candidate := range cands {
rules := ds.compileAstNodes(candidate, flags)
if len(rules) == 0 {
continue
}
found = true
for _, cr := range rules {
variantOrder := big.NewInt(0)
for _, variant := range candidate.Variants {
ord := variantOrderMap[variant]
variantOrder.SetBit(variantOrder, ord, 1)
}
nodeSorting[cr.node] = nodeSortMeta{properties: cr.propertySort, variants: variantOrder, candidate: raw}
astNodes = append(astNodes, cr.node)
}
}
if !found && onInvalid != nil {
onInvalid(raw)
}
}
const inf = int(^uint(0) >> 1)
sort.SliceStable(astNodes, func(i, j int) bool {
a := nodeSorting[astNodes[i]]
z := nodeSorting[astNodes[j]]
if cmp := a.variants.Cmp(z.variants); cmp != 0 {
return cmp < 0
}
offset := 0
for offset < len(a.properties.order) && offset < len(z.properties.order) && a.properties.order[offset] == z.properties.order[offset] {
offset++
}
ao := inf
if offset < len(a.properties.order) {
ao = a.properties.order[offset]
}
zo := inf
if offset < len(z.properties.order) {
zo = z.properties.order[offset]
}
if ao != zo {
return ao < zo
}
if a.properties.count != z.properties.count {
return z.properties.count < a.properties.count // most properties first
}
return strings.Compare(a.candidate, z.candidate) < 0
})
return astNodes, nodeSorting
}
// Port of the compile pipeline from packages/tailwindcss/src/index.ts, adapted
// for the bundler: the default theme.css / preflight.css are embedded, local
// @imports are resolved against baseDir, and the scanner (cmd/bundle) supplies
// the candidate list.
//
// @INCOMPLETE Only static @utility blocks are supported (no functional
// @utility/--value()); @custom-variant is not yet wired. -mta
//go:embed tw_theme.css
var defaultThemeCSS string
//go:embed tw_preflight.css
var defaultPreflightCSS string
// kjolThemeCSS is the shared kjol extension layer — the semantic design tokens
// (surface/line/ink/...), the class-based dark variant, the chart palette and the
// :root icon vars. It sits between Tailwind's defaults and an app's brand, and is
// prepended to the app entry stylesheet by CompileApp / CompileAppFiles.
//
//go:embed kjol_theme.css
var kjolThemeCSS string
func parseThemeOptions(params string) ThemeOptions {
o := themeNone
for _, f := range strings.Fields(params) {
switch f {
case "default":
o |= themeDefault
case "inline":
o |= themeInline
case "reference":
o |= themeReference
case "static":
o |= themeStatic
}
}
return o
}
func importSpecifier(params string) string {
p := strings.TrimSpace(params)
// Drop a trailing layer(...)/supports(...)/media query after the string.
if len(p) > 0 && (p[0] == '"' || p[0] == '\'') {
q := p[0]
if end := strings.IndexByte(p[1:], q); end >= 0 {
return p[1 : 1+end]
}
}
return strings.Trim(p, `"'`)
}
// twCompile compiles a Tailwind entry stylesheet to CSS.
func twCompile(input, baseDir string, candidates []string) (string, int, error) {
theme := NewTheme()
var keyframes []*AstNode
var passthrough []*AstNode
var customUtilities []*AstNode
var customVariants []*AstNode
var safelistAdd []string
var safelistRemove []string
hasPreflight := false
hasUtilities := false
processTheme := func(node *AstNode) {
opts := parseThemeOptions(node.Params)
for _, child := range node.Nodes {
if child.Kind == nDeclaration {
theme.add(child.Property, child.Value, opts)
} else if child.Kind == nAtRule && child.Name == "@keyframes" {
keyframes = append(keyframes, child)
}
}
}
// 1. Default theme.
defAst, err := cssParse(defaultThemeCSS)
if err != nil {
return "", 0, err
}
for _, node := range defAst {
if node.Kind == nAtRule && node.Name == "@theme" {
processTheme(node)
}
}
var processInput func(ast []*AstNode, dir string)
processInput = func(ast []*AstNode, dir string) {
for _, node := range ast {
switch {
case node.Kind == nAtRule && node.Name == "@import":
spec := importSpecifier(node.Params)
switch spec {
case "tailwindcss":
hasPreflight = true
hasUtilities = true
case "tailwindcss/preflight", "tailwindcss/preflight.css":
hasPreflight = true
case "tailwindcss/utilities", "tailwindcss/utilities.css":
hasUtilities = true
case "tailwindcss/theme", "tailwindcss/theme.css":
// default theme already loaded
default:
// Local @import: resolve relative to dir.
content, e := os.ReadFile(filepath.Join(dir, spec))
if e == nil {
sub, e2 := cssParse(string(content))
if e2 == nil {
processInput(sub, filepath.Dir(filepath.Join(dir, spec)))
}
}
}
case node.Kind == nAtRule && node.Name == "@theme":
processTheme(node)
case node.Kind == nAtRule && node.Name == "@utility":
customUtilities = append(customUtilities, node)
case node.Kind == nAtRule && node.Name == "@custom-variant":
customVariants = append(customVariants, node)
case node.Kind == nAtRule && node.Name == "@source":
literals, negate := parseSourceDirective(node.Params)
if negate {
safelistRemove = append(safelistRemove, literals...)
} else {
safelistAdd = append(safelistAdd, literals...)
}
default:
passthrough = append(passthrough, node)
}
}
}
inAst, err := cssParse(input)
if err != nil {
return "", 0, err
}
processInput(inAst, baseDir)
if len(safelistAdd) > 0 {
candidates = append(candidates, safelistAdd...)
}
if len(safelistRemove) > 0 {
remove := make(map[string]bool, len(safelistRemove))
for _, c := range safelistRemove {
remove[c] = true
}
filtered := candidates[:0:0]
for _, c := range candidates {
if !remove[c] {
filtered = append(filtered, c)
}
}
candidates = filtered
}
ds := buildDesignSystem(theme)
// Register @custom-variant blocks. This is how a project defines `dark:` as a CLASS
// toggle rather than a media query — the built-in dark variant follows the OS, which
// a site with a theme switch cannot use:
//
// @custom-variant dark (&:where(.dark, .dark *));
//
// Both of Tailwind's forms are accepted: the shorthand above, and the block form
// with an explicit @slot.
for _, cv := range customVariants {
if name, body, ok := parseCustomVariant(cv); ok {
ds.variants.fromAst(name, body, ds)
}
}
// Register @utility blocks as static utilities.
for _, u := range customUtilities {
name := strings.TrimSpace(u.Params)
var decls []*AstNode
for _, child := range u.Nodes {
if child.Kind == nDeclaration {
decls = append(decls, child)
}
}
captured := decls
ds.utilities.static(name, func(_ *Candidate) *utilResult { return uList(cloneAstNodes(captured)) })
}
astNodes, _ := compileCandidates(candidates, ds, nil, true)
// Assemble the output document.
var out []*AstNode
if hasUtilities {
out = append(out, atRule("@layer", "theme, base, components, utilities"))
}
var rootDecls []*AstNode
for _, key := range theme.order {
tv := theme.values[key]
if tv.options&(themeInline|themeReference) != 0 {
continue
}
val := tv.value
if reThemeFnInvocation.MatchString(val) {
if v, ok := substituteFunctionsInValue(val, decl(key, val), ds); ok {
val = v
}
}
rootDecls = append(rootDecls, decl(key, val))
}
if len(rootDecls) > 0 {
out = append(out, atRule("@layer", "theme", styleRule(":root, :host", rootDecls...)))
}
for _, kf := range keyframes {
out = append(out, kf)
}
if hasPreflight {
pfAst, e := cssParse(defaultPreflightCSS)
if e != nil {
return "", 0, e
}
out = append(out, atRule("@layer", "base", pfAst...))
}
out = append(out, passthrough...)
if hasUtilities && len(astNodes) > 0 {
out = append(out, atRule("@layer", "utilities", astNodes...))
}
out = optimizeAst(out, ds)
return toCss(out), len(astNodes), nil
}
// parseSourceDirective parses the params of an `@source` at-rule, supporting
// the safelist form `@source inline("<pattern>")` / `@source not inline("<pattern>")`
// (mirrors upstream Tailwind v4's inline source safelist). Each quoted string
// literal is brace-expanded (e.g. `{text,bg}-{red,blue}-{100,200}`) into the
// literal candidate classes it names. File-glob `@source "./path/**/*.html"`
// directives are not supported and are ignored.
func parseSourceDirective(params string) (literals []string, negate bool) {
trimmed := strings.TrimSpace(params)
if rest, ok := strings.CutPrefix(trimmed, "not "); ok {
negate = true
trimmed = strings.TrimSpace(rest)
}
if !strings.HasPrefix(trimmed, "inline(") || !strings.HasSuffix(trimmed, ")") {
return nil, false
}
inner := trimmed[len("inline(") : len(trimmed)-1]
for _, lit := range extractQuotedLiterals(inner) {
literals = append(literals, expandBraces(lit)...)
}
return literals, negate
}
// extractQuotedLiterals returns the contents of every single- or
// double-quoted string literal found in s.
func extractQuotedLiterals(s string) []string {
var out []string
for i := 0; i < len(s); i++ {
quote := s[i]
if quote != '\'' && quote != '"' {
continue
}
j := i + 1
for j < len(s) && s[j] != quote {
if s[j] == '\\' && j+1 < len(s) {
j++
}
j++
}
out = append(out, s[i+1:min(j, len(s))])
i = j
}
return out
}
// expandBraces expands shell-style brace groups in pattern, e.g.
// "{text,bg}-red-{100,200}" -> ["text-red-100", "text-red-200",
// "bg-red-100", "bg-red-200"]. A brace group may also be a numeric range
// ("{1..3}" -> "1", "2", "3"). Groups are expanded left to right; a pattern
// with no braces expands to itself.
func expandBraces(pattern string) []string {
start := strings.IndexByte(pattern, '{')
if start == -1 {
return []string{pattern}
}
relEnd := strings.IndexByte(pattern[start:], '}')
if relEnd == -1 {
return []string{pattern}
}
end := start + relEnd
prefix, inner, suffix := pattern[:start], pattern[start+1:end], pattern[end+1:]
var parts []string
if a, b, ok := strings.Cut(inner, ".."); ok && !strings.Contains(inner, ",") {
lo, loErr := strconv.Atoi(strings.TrimSpace(a))
hi, hiErr := strconv.Atoi(strings.TrimSpace(b))
if loErr == nil && hiErr == nil {
step := 1
if lo > hi {
step = -1
}
for n := lo; ; n += step {
parts = append(parts, strconv.Itoa(n))
if n == hi {
break
}
}
}
}
if parts == nil {
parts = strings.Split(inner, ",")
}
var out []string
for _, p := range parts {
out = append(out, expandBraces(prefix+p+suffix)...)
}
return out
}
// scanSources scans the given glob/** patterns (relative to baseDir) for
// candidate class names using the bundler's scanner.
func scanSources(baseDir string, patterns []string) []string {
var all []string
seen := make(map[string]bool)
add := func(cands []string) {
for _, c := range cands {
if !seen[c] {
seen[c] = true
all = append(all, c)
}
}
}
for _, pattern := range patterns {
absPattern := filepath.Clean(filepath.Join(baseDir, pattern))
if strings.Contains(pattern, "**") {
parts := strings.SplitN(absPattern, "**", 2)
root := filepath.Clean(parts[0])
suffix := ""
if len(parts) > 1 {
s := strings.TrimLeft(parts[1], string(filepath.Separator))
if idx := strings.LastIndex(s, "."); idx >= 0 {
suffix = s[idx:]
}
}
filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
if suffix != "" && !strings.HasSuffix(path, suffix) {
return nil
}
cands, err := twScanFile(path)
if err != nil {
return nil
}
add(cands)
return nil
})
} else {
matches, err := filepath.Glob(absPattern)
if err != nil {
continue
}
for _, path := range matches {
cands, err := twScanFile(path)
if err != nil {
continue
}
add(cands)
}
}
}
sort.Strings(all)
return all
}
// Port of packages/tailwindcss/src/css-functions.ts
//
// Resolves the inline CSS functions Tailwind emits/accepts in values:
// --spacing(), --alpha(), --theme() and the legacy theme(). Returns ok=false
// when a function cannot be resolved (the candidate then produces no CSS,
// matching the upstream try/catch that drops it).
//
// @INCOMPLETE injectFallbackForInitialFallback nuance for --theme(...) chained
// fallbacks is not modeled. -mta
type cssFnHandler func(ds *DesignSystem, source *AstNode, args []string) (string, bool)
var cssFunctions = map[string]cssFnHandler{
"--alpha": cssAlpha,
"--spacing": cssSpacing,
"--theme": cssTheme,
"theme": cssLegacyTheme,
}
var reThemeFnInvocation = regexp.MustCompile(`--alpha\(|--spacing\(|--theme\(|theme\(`)
func cssAlpha(_ *DesignSystem, _ *AstNode, args []string) (string, bool) {
if len(args) != 1 {
return "", false
}
parts := segment(args[0], "/")
if len(parts) < 2 {
return "", false
}
color := strings.TrimSpace(parts[0])
alpha := strings.TrimSpace(parts[1])
if color == "" || alpha == "" {
return "", false
}
return withAlpha(color, alpha), true
}
func cssSpacing(ds *DesignSystem, _ *AstNode, args []string) (string, bool) {
if len(args) != 1 || args[0] == "" {
return "", false
}
value := args[0]
multiplier, ok := ds.theme.resolve(nil, []string{"--spacing"}, themeNone)
if !ok {
return "", false
}
if n, _, ok := parseDimension(value); ok {
if n == 0 {
return "0", true
}
if n == 1 {
return multiplier, true
}
}
return "calc(" + multiplier + " * " + value + ")", true
}
func cssTheme(ds *DesignSystem, source *AstNode, args []string) (string, bool) {
if len(args) == 0 {
return "", false
}
path := args[0]
fallback := args[1:]
if !strings.HasPrefix(path, "--") {
return "", false
}
inline := false
if strings.HasSuffix(path, " inline") {
inline = true
path = path[:len(path)-7]
}
if source != nil && source.Kind == nAtRule {
inline = true
}
resolved, ok := ds.resolveThemeValue(path, inline)
if !ok {
if len(fallback) > 0 {
return strings.Join(fallback, ", "), true
}
return "", false
}
if len(fallback) == 0 {
return resolved, true
}
joined := strings.Join(fallback, ", ")
if joined == "initial" {
return resolved, true
}
if resolved == "initial" {
return joined, true
}
return resolved, true
}
func cssLegacyTheme(ds *DesignSystem, _ *AstNode, args []string) (string, bool) {
if len(args) == 0 {
return "", false
}
path := eventuallyUnquote(args[0])
fallback := args[1:]
resolved, ok := ds.resolveThemeValue(path, true)
if !ok {
if len(fallback) > 0 {
return strings.Join(fallback, ", "), true
}
return "", false
}
return resolved, true
}
func substituteFunctions(ast []*AstNode, ds *DesignSystem) bool {
okAll := true
a := ast
walkAst(&a, func(node *AstNode, _ *VisitContext) WalkResult {
if node.Kind == nDeclaration && node.Value != "" && reThemeFnInvocation.MatchString(node.Value) {
v, ok := substituteFunctionsInValue(node.Value, node, ds)
if !ok {
okAll = false
return WStop
}
node.Value = v
return WContinue
}
if node.Kind == nAtRule {
if (node.Name == "@media" || node.Name == "@custom-media" || node.Name == "@container" || node.Name == "@supports") &&
reThemeFnInvocation.MatchString(node.Params) {
v, ok := substituteFunctionsInValue(node.Params, node, ds)
if !ok {
okAll = false
return WStop
}
node.Params = v
}
}
return WContinue
})
return okAll
}
func substituteFunctionsInValue(value string, source *AstNode, ds *DesignSystem) (string, bool) {
ast := valueParse(value)
out, ok := valueSubstitute(ast, source, ds)
if !ok {
return "", false
}
return valueToCss(out), true
}
func valueSubstitute(nodes []ValueNode, source *AstNode, ds *DesignSystem) ([]ValueNode, bool) {
var out []ValueNode
for _, n := range nodes {
f, isFn := n.(*ValueFunction)
if !isFn {
out = append(out, n)
continue
}
if handler, isCss := cssFunctions[f.Value]; isCss {
args := segment(strings.TrimSpace(valueToCss(f.Nodes)), ",")
for i := range args {
args[i] = strings.TrimSpace(args[i])
}
result, ok := handler(ds, source, args)
if !ok {
return nil, false
}
out = append(out, valueParse(result)...)
continue
}
sub, ok := valueSubstitute(f.Nodes, source, ds)
if !ok {
return nil, false
}
f.Nodes = sub
out = append(out, f)
}
return out, true
}
func eventuallyUnquote(value string) string {
if len(value) == 0 || (value[0] != '\'' && value[0] != '"') {
return value
}
var b strings.Builder
quote := value[0]
for i := 1; i < len(value)-1; i++ {
cur := value[i]
var next byte
if i+1 < len(value) {
next = value[i+1]
}
if cur == '\\' && (next == quote || next == '\\') {
b.WriteByte(next)
i++
} else {
b.WriteByte(cur)
}
}
return b.String()
}
// Port of packages/tailwindcss/src/css-parser.ts
//
// A single-pass CSS parser producing the AstNode tree. Source-map tracking is
// omitted (the bundler minifies the output). Operates on bytes; all structural
// characters are ASCII so multi-byte UTF-8 content passes through untouched.
func cAt(s string, i int) int {
if i < 0 || i >= len(s) {
return -1
}
return int(s[i])
}
const (
cBackslash = 0x5c
cSlashCh = 0x2f
cAsterisk = 0x2a
cDQuote = 0x22
cSQuote = 0x27
cColon = 0x3a
cSemicolon = 0x3b
cLF = 0x0a
cCR = 0x0d
cSpaceCh = 0x20
cTabCh = 0x09
cLCurly = 0x7b
cRCurly = 0x7d
cLParen = 0x28
cRParen = 0x29
cLBracket = 0x5b
cRBracket = 0x5d
cDash = 0x2d
cAtSign = 0x40
cBang = 0x21
)
func appendChild(parent *AstNode, child *AstNode) {
if ch := nodeChildren(parent); ch != nil {
*ch = append(*ch, child)
}
}
func cssParse(input string) ([]*AstNode, error) {
if len(input) >= 3 && input[0] == 0xEF && input[1] == 0xBB && input[2] == 0xBF {
input = input[3:]
}
var ast []*AstNode
var licenseComments []*AstNode
var stack []*AstNode
var parent *AstNode
var node *AstNode
var buffer []byte
var closingBracketStack []byte
for i := 0; i < len(input); i++ {
currentChar := int(input[i])
// Skip the CR in CRLF.
if currentChar == cCR {
if cAt(input, i+1) == cLF {
continue
}
}
switch {
case currentChar == cBackslash:
if i+1 < len(input) {
buffer = append(buffer, input[i], input[i+1])
i++
} else {
buffer = append(buffer, input[i])
}
case currentChar == cSlashCh && cAt(input, i+1) == cAsterisk:
start := i
for j := i + 2; j < len(input); j++ {
pc := int(input[j])
if pc == cBackslash {
j++
} else if pc == cAsterisk && cAt(input, j+1) == cSlashCh {
i = j + 1
break
}
}
commentString := input[start : i+1]
// Hoist license comments (/*! ... */).
if cAt(commentString, 2) == cBang {
licenseComments = append(licenseComments, comment(commentString[2:len(commentString)-2]))
}
case currentChar == cSQuote || currentChar == cDQuote:
end, err := parseString(input, i, byte(currentChar))
if err != nil {
return nil, err
}
buffer = append(buffer, input[i:end+1]...)
i = end
case (currentChar == cSpaceCh || currentChar == cLF || currentChar == cTabCh) && func() bool {
pc := cAt(input, i+1)
if pc == cSpaceCh || pc == cLF || pc == cTabCh {
return true
}
if pc == cCR && cAt(input, i+2) == cLF {
return true
}
return false
}():
// Collapse consecutive whitespace.
continue
case currentChar == cLF:
if len(buffer) == 0 {
continue
}
last := buffer[len(buffer)-1]
if last != cSpaceCh && last != cLF && last != cTabCh {
buffer = append(buffer, ' ')
}
case currentChar == cDash && cAt(input, i+1) == cDash && len(buffer) == 0:
// Custom property: permissive, balance brackets to find the end.
var localStack []byte
start := i
colonIdx := -1
for j := i + 2; j < len(input); j++ {
pc := int(input[j])
if pc == cBackslash {
j++
} else if pc == cSQuote || pc == cDQuote {
var err error
j, err = parseString(input, j, byte(pc))
if err != nil {
return nil, err
}
} else if pc == cSlashCh && cAt(input, j+1) == cAsterisk {
for k := j + 2; k < len(input); k++ {
pk := int(input[k])
if pk == cBackslash {
k++
} else if pk == cAsterisk && cAt(input, k+1) == cSlashCh {
j = k + 1
break
}
}
} else if colonIdx == -1 && pc == cColon {
colonIdx = len(buffer) + j - start
} else if pc == cSemicolon && len(localStack) == 0 {
buffer = append(buffer, input[start:j]...)
i = j
break
} else if pc == cLParen {
localStack = append(localStack, ')')
} else if pc == cLBracket {
localStack = append(localStack, ']')
} else if pc == cLCurly {
localStack = append(localStack, '}')
} else if (pc == cRCurly || len(input)-1 == j) && len(localStack) == 0 {
i = j - 1
buffer = append(buffer, input[start:j]...)
break
} else if pc == cRParen || pc == cRBracket || pc == cRCurly {
if len(localStack) > 0 && input[j] == localStack[len(localStack)-1] {
localStack = localStack[:len(localStack)-1]
}
}
}
declaration := parseDeclaration(string(buffer), colonIdx)
if declaration == nil {
return nil, fmt.Errorf("invalid custom property, expected a value")
}
if parent != nil {
appendChild(parent, declaration)
} else {
ast = append(ast, declaration)
}
buffer = buffer[:0]
case currentChar == cSemicolon && len(buffer) > 0 && buffer[0] == cAtSign:
node = parseAtRule(string(buffer), nil)
if parent != nil {
appendChild(parent, node)
} else {
ast = append(ast, node)
}
buffer = buffer[:0]
node = nil
case currentChar == cSemicolon && lastByte(closingBracketStack) != ')':
declaration := parseDeclaration(string(buffer), -1)
if declaration == nil {
if len(buffer) == 0 {
continue
}
return nil, fmt.Errorf("invalid declaration: `%s`", strings.TrimSpace(string(buffer)))
}
if parent != nil {
appendChild(parent, declaration)
} else {
ast = append(ast, declaration)
}
buffer = buffer[:0]
case currentChar == cLCurly && lastByte(closingBracketStack) != ')':
closingBracketStack = append(closingBracketStack, '}')
node = rule(strings.TrimSpace(string(buffer)))
if parent != nil {
appendChild(parent, node)
}
stack = append(stack, parent)
parent = node
buffer = buffer[:0]
node = nil
case currentChar == cRCurly && lastByte(closingBracketStack) != ')':
if len(closingBracketStack) == 0 {
return nil, fmt.Errorf("missing opening {")
}
closingBracketStack = closingBracketStack[:len(closingBracketStack)-1]
if len(buffer) > 0 {
if buffer[0] == cAtSign {
node = parseAtRule(string(buffer), nil)
if parent != nil {
appendChild(parent, node)
} else {
ast = append(ast, node)
}
buffer = buffer[:0]
node = nil
} else {
colonIdx := strings.IndexByte(string(buffer), ':')
if parent != nil {
d := parseDeclaration(string(buffer), colonIdx)
if d == nil {
return nil, fmt.Errorf("invalid declaration: `%s`", strings.TrimSpace(string(buffer)))
}
appendChild(parent, d)
}
}
}
var grandParent *AstNode
if len(stack) > 0 {
grandParent = stack[len(stack)-1]
stack = stack[:len(stack)-1]
}
if grandParent == nil && parent != nil {
ast = append(ast, parent)
}
parent = grandParent
buffer = buffer[:0]
node = nil
case currentChar == cLParen:
closingBracketStack = append(closingBracketStack, ')')
buffer = append(buffer, '(')
case currentChar == cRParen:
if lastByte(closingBracketStack) != ')' {
return nil, fmt.Errorf("missing opening (")
}
closingBracketStack = closingBracketStack[:len(closingBracketStack)-1]
buffer = append(buffer, ')')
default:
if len(buffer) == 0 && (currentChar == cSpaceCh || currentChar == cLF || currentChar == cTabCh) {
continue
}
buffer = append(buffer, byte(currentChar))
}
}
if len(buffer) > 0 && buffer[0] == cAtSign {
ast = append(ast, parseAtRule(string(buffer), nil))
}
if len(closingBracketStack) > 0 && parent != nil {
switch parent.Kind {
case nRule:
return nil, fmt.Errorf("missing closing } at %s", parent.Selector)
case nAtRule:
return nil, fmt.Errorf("missing closing } at %s %s", parent.Name, parent.Params)
}
}
if len(licenseComments) > 0 {
return append(licenseComments, ast...), nil
}
return ast, nil
}
func lastByte(b []byte) byte {
if len(b) == 0 {
return 0
}
return b[len(b)-1]
}
func parseAtRule(buffer string, nodes []*AstNode) *AstNode {
name := buffer
params := ""
// Smallest common at-rule is `@page` (5 chars); scan from index 5.
for i := 5; i < len(buffer); i++ {
c := buffer[i]
if c == cSpaceCh || c == cTabCh || c == cLParen {
name = buffer[:i]
params = buffer[i:]
break
}
}
return atRule(strings.TrimSpace(name), strings.TrimSpace(params), nodes...)
}
func parseDeclaration(buffer string, colonIdx int) *AstNode {
if colonIdx == -1 {
colonIdx = strings.IndexByte(buffer, ':')
}
if colonIdx == -1 {
return nil
}
importantIdx := strings.Index(buffer[colonIdx+1:], "!important")
property := strings.TrimSpace(buffer[:colonIdx])
var value string
if importantIdx == -1 {
value = strings.TrimSpace(buffer[colonIdx+1:])
} else {
value = strings.TrimSpace(buffer[colonIdx+1 : colonIdx+1+importantIdx])
}
return &AstNode{Kind: nDeclaration, Property: property, Value: value, Important: importantIdx != -1}
}
func parseString(input string, startIdx int, quoteChar byte) (int, error) {
for i := startIdx + 1; i < len(input); i++ {
pc := input[i]
if pc == cBackslash {
i++
} else if pc == quoteChar {
return i, nil
} else if pc == cSemicolon && (cAt(input, i+1) == cLF || (cAt(input, i+1) == cCR && cAt(input, i+2) == cLF)) {
return 0, fmt.Errorf("unterminated string: %s", input[startIdx:i+1]+string(quoteChar))
} else if pc == cLF || (pc == cCR && cAt(input, i+1) == cLF) {
return 0, fmt.Errorf("unterminated string: %s", input[startIdx:i]+string(quoteChar))
}
}
return startIdx, nil
}
// Port of packages/tailwindcss/src/utils/decode-arbitrary-value.ts
//
// Turns Tailwind's underscore-escaped arbitrary value syntax into real CSS:
// `_` becomes a space (except `\_` which becomes a literal `_`), function
// names are decoded, url()/var()/theme() contents are handled specially, and
// math operators inside calc()-family functions get whitespace normalized.
func decodeArbitraryValue(input string) string {
if !strings.Contains(input, "(") {
return convertUnderscoresToWhitespace(input, false)
}
ast := valueParse(input)
recursivelyDecodeArbitraryValues(ast)
input = valueToCss(ast)
input = addWhitespaceAroundMathOperators(input)
return input
}
// convertUnderscoresToWhitespace converts `_` to ` `, and `\_` to `_`. When
// skipUnderscoreToSpace is true, bare underscores are left untouched (used for
// the first argument of var()/theme()).
func convertUnderscoresToWhitespace(input string, skipUnderscoreToSpace bool) string {
var b strings.Builder
for i := 0; i < len(input); i++ {
ch := input[i]
if ch == '\\' && i+1 < len(input) && input[i+1] == '_' {
b.WriteByte('_')
i++
} else if ch == '_' && !skipUnderscoreToSpace {
b.WriteByte(' ')
} else {
b.WriteByte(ch)
}
}
return b.String()
}
func recursivelyDecodeArbitraryValues(ast []ValueNode) {
for _, node := range ast {
switch n := node.(type) {
case *ValueFunction:
if n.Value == "url" || strings.HasSuffix(n.Value, "_url") {
// Don't decode underscores in url() contents, only the name.
n.Value = convertUnderscoresToWhitespace(n.Value, false)
break
}
if n.Value == "var" || strings.HasSuffix(n.Value, "_var") ||
n.Value == "theme" || strings.HasSuffix(n.Value, "_theme") {
n.Value = convertUnderscoresToWhitespace(n.Value, false)
for i := 0; i < len(n.Nodes); i++ {
// First argument (the variable name) keeps its underscores.
if i == 0 {
if w, ok := n.Nodes[i].(*ValueWord); ok {
w.Value = convertUnderscoresToWhitespace(w.Value, true)
continue
}
}
recursivelyDecodeArbitraryValues([]ValueNode{n.Nodes[i]})
}
break
}
n.Value = convertUnderscoresToWhitespace(n.Value, false)
recursivelyDecodeArbitraryValues(n.Nodes)
case *ValueWord:
n.Value = convertUnderscoresToWhitespace(n.Value, false)
case *ValueSeparator:
n.Value = convertUnderscoresToWhitespace(n.Value, false)
}
}
}
// Port of packages/tailwindcss/src/utils/default-map.ts
//
// A map that lazily computes (and memoizes) a default value for missing keys
// via a factory. The factory receives the map itself to support recursive
// definitions, matching the upstream `DefaultMap`.
type DefaultMap[K comparable, V any] struct {
m map[K]V
order []K
factory func(key K, self *DefaultMap[K, V]) V
}
func NewDefaultMap[K comparable, V any](factory func(key K, self *DefaultMap[K, V]) V) *DefaultMap[K, V] {
return &DefaultMap[K, V]{m: make(map[K]V), factory: factory}
}
func (d *DefaultMap[K, V]) Get(key K) V {
if v, ok := d.m[key]; ok {
return v
}
v := d.factory(key, d)
d.set(key, v)
return v
}
func (d *DefaultMap[K, V]) set(key K, v V) {
if _, ok := d.m[key]; !ok {
d.order = append(d.order, key)
}
d.m[key] = v
}
func (d *DefaultMap[K, V]) Set(key K, v V) { d.set(key, v) }
func (d *DefaultMap[K, V]) Has(key K) bool {
_, ok := d.m[key]
return ok
}
// Values returns the memoized values in insertion order.
func (d *DefaultMap[K, V]) Values() []V {
out := make([]V, 0, len(d.order))
for _, k := range d.order {
out = append(out, d.m[k])
}
return out
}
// Port of packages/tailwindcss/src/design-system.ts
//
// The DesignSystem ties together the theme, utilities and variants, and caches
// parsed candidates/variants and compiled AST nodes. IntelliSense-only methods
// (getClassList/getVariants/canonicalizeCandidates/candidatesToCss) are omitted
// — they don't affect generated CSS. -mta
type DesignSystem struct {
theme *Theme
utilities *Utilities
variants *Variants
invalidCandidates map[string]bool
important bool
parsedVariants *DefaultMap[string, *Variant]
parsedCandidates *DefaultMap[string, []*Candidate]
compiledAstNodes *DefaultMap[CompileAstFlags, *DefaultMap[*Candidate, []compiledNode]]
trackedVariables *DefaultMap[string, bool]
}
func buildDesignSystem(theme *Theme) *DesignSystem {
ds := &DesignSystem{
theme: theme,
utilities: createUtilities(theme),
variants: createVariants(theme),
invalidCandidates: map[string]bool{},
}
ds.parsedVariants = NewDefaultMap(func(v string, _ *DefaultMap[string, *Variant]) *Variant {
return parseVariant(v, ds)
})
ds.parsedCandidates = NewDefaultMap(func(c string, _ *DefaultMap[string, []*Candidate]) []*Candidate {
return parseCandidate(c, ds)
})
ds.compiledAstNodes = NewDefaultMap(func(flags CompileAstFlags, _ *DefaultMap[CompileAstFlags, *DefaultMap[*Candidate, []compiledNode]]) *DefaultMap[*Candidate, []compiledNode] {
return NewDefaultMap(func(cand *Candidate, _ *DefaultMap[*Candidate, []compiledNode]) []compiledNode {
ast := compileAstNodes(cand, ds, flags)
nodes := make([]*AstNode, len(ast))
for i, v := range ast {
nodes[i] = v.node
}
substituteFunctions(nodes, ds)
substituteAtVariant(nodes, ds)
return ast
})
})
ds.trackedVariables = NewDefaultMap(func(raw string, _ *DefaultMap[string, bool]) bool {
for _, variable := range extractUsedVariables(raw) {
theme.markUsedVariable(variable)
}
return true
})
return ds
}
func (ds *DesignSystem) parseCandidate(candidate string) []*Candidate {
return ds.parsedCandidates.Get(candidate)
}
func (ds *DesignSystem) parseVariant(variant string) *Variant {
return ds.parsedVariants.Get(variant)
}
func (ds *DesignSystem) compileAstNodes(candidate *Candidate, flags CompileAstFlags) []compiledNode {
return ds.compiledAstNodes.Get(flags).Get(candidate)
}
func (ds *DesignSystem) trackUsedVariables(raw string) {
ds.trackedVariables.Get(raw)
}
func (ds *DesignSystem) getVariantOrder() map[*Variant]int {
vs := ds.parsedVariants.Values()
sort.SliceStable(vs, func(i, j int) bool { return ds.variants.compare(vs[i], vs[j]) < 0 })
order := map[*Variant]int{}
var prev *Variant
hasPrev := false
index := 0
for _, variant := range vs {
if variant == nil {
continue
}
if hasPrev && ds.variants.compare(prev, variant) != 0 {
index++
}
order[variant] = index
prev = variant
hasPrev = true
}
return order
}
func (ds *DesignSystem) resolveThemeValue(path string, forceInline bool) (string, bool) {
modifier := ""
if lastSlash := strings.LastIndex(path, "/"); lastSlash != -1 {
modifier = strings.TrimSpace(path[lastSlash+1:])
path = strings.TrimSpace(path[:lastSlash])
}
opt := themeNone
if forceInline {
opt = themeInline
}
themeValue, ok := ds.theme.resolve(nil, []string{path}, opt)
if !ok {
return "", false
}
if modifier != "" {
return withAlpha(themeValue, modifier), true
}
return themeValue, true
}
func (ds *DesignSystem) getClassOrder(classes []string) []classOrderEntry {
return getClassOrder(ds, classes)
}
// Port of packages/tailwindcss/src/utils/dimensions.ts
// Parses a dimension like "64rem" into (64, "rem"). unit == "" means no unit.
var reDimension = regexp.MustCompile(`(?i)^([-+]?(?:\d*\.)?\d+)([a-z]+|%)?$`)
func parseDimension(input string) (float64, string, bool) {
m := reDimension.FindStringSubmatch(input)
if m == nil {
return 0, "", false
}
v, ok := jsParseNumber(m[1])
if !ok {
return 0, "", false
}
return v, m[2], true
}
// Port of packages/tailwindcss/src/utils/escape.ts
// https://drafts.csswg.org/cssom/#serialize-an-identifier
func escape(value string) string {
if value == "" {
return value
}
runes := []rune(value)
length := len(runes)
first := runes[0]
// If the character is the first character and is a `-` (U+002D), and there
// is no second character, escape it.
if length == 1 && first == 0x002d {
return "\\" + value
}
var b strings.Builder
for index, codeUnit := range runes {
// NULL (U+0000) -> REPLACEMENT CHARACTER (U+FFFD).
if codeUnit == 0x0000 {
b.WriteRune('<27>')
continue
}
if (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||
codeUnit == 0x007f ||
(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
(index == 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && first == 0x002d) {
// Escape as a code point.
b.WriteByte('\\')
b.WriteString(strconv.FormatInt(int64(codeUnit), 16))
b.WriteByte(' ')
continue
}
if codeUnit >= 0x0080 ||
codeUnit == 0x002d ||
codeUnit == 0x005f ||
(codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
(codeUnit >= 0x0041 && codeUnit <= 0x005a) ||
(codeUnit >= 0x0061 && codeUnit <= 0x007a) {
b.WriteRune(codeUnit)
continue
}
// Otherwise, the escaped character.
b.WriteByte('\\')
b.WriteRune(codeUnit)
}
return b.String()
}
var reUnescape = regexp.MustCompile(`\\([0-9A-Fa-f]{1,6}[\t\n\f\r ]?|[\s\S])`)
func unescape(escaped string) string {
return reUnescape.ReplaceAllStringFunc(escaped, func(match string) string {
r := []rune(match)
if len(r) <= 2 {
return string(r[1])
}
codePoint, err := strconv.ParseInt(strings.TrimSpace(string(r[1:])), 16, 64)
if err != nil {
return "<22>"
}
if codePoint == 0x0000 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff) {
return "<22>"
}
return string(rune(codePoint))
})
}
// Port of packages/tailwindcss/src/utils/infer-data-type.ts
//
// Data types recognised by inferDataType. Used by functional utilities to
// dispatch arbitrary values (e.g. text-[10px] is a length, text-[#fff] a color).
const (
dtColor = "color"
dtLength = "length"
dtPercentage = "percentage"
dtRatio = "ratio"
dtNumber = "number"
dtInteger = "integer"
dtURL = "url"
dtPosition = "position"
dtBgSize = "bg-size"
dtLineWidth = "line-width"
dtImage = "image"
dtFamilyName = "family-name"
dtGenericName = "generic-name"
dtAbsoluteSize = "absolute-size"
dtRelativeSize = "relative-size"
dtAngle = "angle"
dtVector = "vector"
)
var dataTypeChecks = map[string]func(string) bool{
dtColor: isColor,
dtLength: isLength,
dtPercentage: isPercentage,
dtRatio: isFraction,
dtNumber: isNumber,
dtInteger: isPositiveInteger,
dtURL: isURL,
dtPosition: isBackgroundPosition,
dtBgSize: isBackgroundSize,
dtLineWidth: isLineWidth,
dtImage: isImage,
dtFamilyName: isFamilyName,
dtGenericName: isGenericName,
dtAbsoluteSize: isAbsoluteSize,
dtRelativeSize: isRelativeSize,
dtAngle: isAngle,
dtVector: isVector,
}
// inferDataType returns the first matching data type from types, or "" (null).
func inferDataType(value string, types []string) string {
if strings.HasPrefix(value, "var(") {
return ""
}
for _, t := range types {
if check, ok := dataTypeChecks[t]; ok && check(value) {
return t
}
}
return ""
}
// ---- individual checks --------------------------------------------------
var reIsURL = regexp.MustCompile(`^url\(.*\)$`)
func isURL(value string) bool { return reIsURL.MatchString(value) }
func isLineWidth(value string) bool {
for _, v := range segment(value, " ") {
if !(isLength(v) || isNumber(v) || v == "thin" || v == "medium" || v == "thick") {
return false
}
}
return true
}
var (
reIsImageFn = regexp.MustCompile(`^(?:element|image|cross-fade|image-set)\(`)
reIsGradientFn = regexp.MustCompile(`^(repeating-)?(conic|linear|radial)-gradient\(`)
)
func isImage(value string) bool {
count := 0
for _, part := range segment(value, ",") {
if strings.HasPrefix(part, "var(") {
continue
}
if isURL(part) || reIsGradientFn.MatchString(part) || reIsImageFn.MatchString(part) {
count++
continue
}
return false
}
return count > 0
}
func isGenericName(value string) bool {
switch value {
case "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
"ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong":
return true
}
return false
}
func isFamilyName(value string) bool {
count := 0
for _, part := range segment(value, ",") {
if len(part) > 0 && part[0] >= '0' && part[0] <= '9' {
return false
}
if strings.HasPrefix(part, "var(") {
continue
}
count++
}
return count > 0
}
func isAbsoluteSize(value string) bool {
switch value {
case "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large":
return true
}
return false
}
func isRelativeSize(value string) bool {
return value == "larger" || value == "smaller"
}
const hasNumber = `[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?`
var (
reIsNumber = regexp.MustCompile(`^` + hasNumber + `$`)
reIsPercentage = regexp.MustCompile(`^` + hasNumber + `%$`)
reIsFraction = regexp.MustCompile(`^` + hasNumber + `\s*/\s*` + hasNumber + `$`)
)
func isNumber(value string) bool { return reIsNumber.MatchString(value) || hasMathFn(value) }
func isPercentage(value string) bool { return reIsPercentage.MatchString(value) || hasMathFn(value) }
func isFraction(value string) bool { return reIsFraction.MatchString(value) || hasMathFn(value) }
var lengthUnits = []string{
"cm", "mm", "Q", "in", "pc", "pt", "px", "em", "ex", "ch", "rem", "lh", "rlh",
"vw", "vh", "vmin", "vmax", "vb", "vi", "svw", "svh", "lvw", "lvh", "dvw", "dvh",
"cqw", "cqh", "cqi", "cqb", "cqmin", "cqmax",
}
var (
reIsLength = regexp.MustCompile(`^` + hasNumber + `(` + strings.Join(lengthUnits, "|") + `)$`)
reIsLengthFn = regexp.MustCompile(`(?i)^(--spacing)\(`)
)
func isLength(value string) bool {
return reIsLength.MatchString(value) || reIsLengthFn.MatchString(value) || hasMathFn(value)
}
func isBackgroundPosition(value string) bool {
count := 0
for _, part := range segment(value, " ") {
switch part {
case "center", "top", "right", "bottom", "left":
count++
continue
}
if strings.HasPrefix(part, "var(") {
continue
}
if isLength(part) || isPercentage(part) {
count++
continue
}
return false
}
return count > 0
}
func isBackgroundSize(value string) bool {
count := 0
for _, size := range segment(value, ",") {
if size == "cover" || size == "contain" {
count++
continue
}
values := segment(size, " ")
if len(values) != 1 && len(values) != 2 {
return false
}
ok := true
for _, v := range values {
if !(v == "auto" || isLength(v) || isPercentage(v)) {
ok = false
break
}
}
if ok {
count++
}
}
return count > 0
}
var angleUnits = []string{"deg", "rad", "grad", "turn"}
var reIsAngle = regexp.MustCompile(`^` + hasNumber + `(` + strings.Join(angleUnits, "|") + `)$`)
func isAngle(value string) bool { return reIsAngle.MatchString(value) }
var reIsVector = regexp.MustCompile(`^` + hasNumber + ` +` + hasNumber + ` +` + hasNumber + `$`)
func isVector(value string) bool { return reIsVector.MatchString(value) }
// ---- numeric predicates -------------------------------------------------
func jsParseNumber(s string) (float64, bool) {
f, err := strconv.ParseFloat(s, 64)
if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
return 0, false
}
return f, true
}
// jsNumberToString mirrors JS `String(Number(x))` for the small decimal values
// these predicates see (multiples of 0.25, small integers).
func jsNumberToString(f float64) string {
return strconv.FormatFloat(f, 'g', -1, 64)
}
func isPositiveInteger(value string) bool {
f, ok := jsParseNumber(value)
if !ok {
return false
}
return f == math.Trunc(f) && f >= 0 && jsNumberToString(f) == value
}
func isStrictPositiveInteger(value string) bool {
f, ok := jsParseNumber(value)
if !ok {
return false
}
return f == math.Trunc(f) && f > 0 && jsNumberToString(f) == value
}
func isValidSpacingMultiplier(value string) bool { return isMultipleOf(value, 0.25) }
func isValidOpacityValue(value string) bool { return isMultipleOf(value, 0.25) }
func isMultipleOf(value string, divisor float64) bool {
f, ok := jsParseNumber(value)
if !ok || f < 0 {
return false
}
q := f / divisor
if math.Abs(q-math.Round(q)) > 1e-9 {
return false
}
return jsNumberToString(f) == value
}
// Port of packages/tailwindcss/src/utils/is-color.ts
var twNamedColors = func() map[string]bool {
names := []string{
// CSS Level 1
"black", "silver", "gray", "white", "maroon", "red", "purple", "fuchsia",
"green", "lime", "olive", "yellow", "navy", "blue", "teal", "aqua",
// CSS Level 2/3
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque",
"black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
"chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan",
"darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki",
"darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon",
"darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise",
"darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod",
"gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo",
"ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue",
"lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey",
"lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
"lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen",
"mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue",
"mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab",
"orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise",
"palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple",
"rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown",
"seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey",
"snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
"wheat", "white", "whitesmoke", "yellow", "yellowgreen",
// Keywords
"transparent", "currentcolor",
// System colors
"canvas", "canvastext", "linktext", "visitedtext", "activetext", "buttonface", "buttontext",
"buttonborder", "field", "fieldtext", "highlight", "highlighttext", "selecteditem",
"selecteditemtext", "mark", "marktext", "graytext", "accentcolor", "accentcolortext",
}
m := make(map[string]bool, len(names))
for _, n := range names {
m[n] = true
}
return m
}()
var reIsColorFn = regexp.MustCompile(`(?i)^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix|--alpha)\(`)
func isColor(value string) bool {
if len(value) > 0 && value[0] == '#' {
return true
}
return reIsColorFn.MatchString(value) || twNamedColors[strings.ToLower(value)]
}
func isNamedColor(value string) bool {
return twNamedColors[strings.ToLower(value)]
}
// Port of packages/tailwindcss/src/utils/is-valid-arbitrary.ts
//
// An arbitrary value is valid when parens/brackets are balanced and there is no
// top-level `;`. Note `{` intentionally does not push the stack, so a candidate
// like `[&{color:red}]` is rejected.
func isValidArbitrary(input string) bool {
var stack []byte
for i := 0; i < len(input); i++ {
c := input[i]
switch c {
case '\\':
i++
case '\'', '"':
for i++; i < len(input); i++ {
nc := input[i]
if nc == '\\' {
i++
continue
}
if nc == c {
break
}
}
case '(':
stack = append(stack, ')')
case '[':
stack = append(stack, ']')
case ')', ']', '}':
if len(stack) == 0 {
return false
}
if c == stack[len(stack)-1] {
stack = stack[:len(stack)-1]
}
case ';':
if len(stack) == 0 {
return false
}
}
}
return true
}
// Port of packages/tailwindcss/src/utils/math-operators.ts
var mathFunctions = []string{
"calc", "min", "max", "clamp", "mod", "rem", "sin", "cos", "tan",
"asin", "acos", "atan", "atan2", "pow", "sqrt", "hypot", "log", "exp", "round",
}
func hasMathFn(input string) bool {
if !strings.Contains(input, "(") {
return false
}
for _, fn := range mathFunctions {
if strings.Contains(input, fn+"(") {
return true
}
}
return false
}
func isMathFunctionName(fn string) bool {
for _, m := range mathFunctions {
if m == fn {
return true
}
}
return false
}
const (
mLowerA = 0x61
mLowerZ = 0x7a
mUpperA = 0x41
mUpperZ = 0x5a
mLowerE = 0x65
mUpperE = 0x45
mZero = 0x30
mNine = 0x39
mAdd = '+'
mSub = '-'
mMul = '*'
mDiv = '/'
mLParen = '('
mRParen = ')'
mComma = ','
mSpace = ' '
mPct = '%'
)
func addWhitespaceAroundMathOperators(input string) string {
containsAny := false
for _, fn := range mathFunctions {
if strings.Contains(input, fn) {
containsAny = true
break
}
}
if !containsAny {
return input
}
var result []byte
var formattable []bool // stack; index 0 == top
valuePos := -1
lastValuePos := -1
for i := 0; i < len(input); i++ {
char := input[i]
// Track number-then-unit so we know it's a value, not a function call.
if char >= mZero && char <= mNine {
valuePos = i
} else if valuePos != -1 &&
(char == mPct || (char >= mLowerA && char <= mLowerZ) || (char >= mUpperA && char <= mUpperZ)) {
valuePos = i
} else {
lastValuePos = valuePos
valuePos = -1
}
switch {
case char == mLParen:
result = append(result, char)
// Scan backwards for the function name (lowercase alnum).
start := i
for j := i - 1; j >= 0; j-- {
inner := input[j]
if inner >= mZero && inner <= mNine {
start = j
} else if inner >= mLowerA && inner <= mLowerZ {
start = j
} else {
break
}
}
fn := input[start:i]
if isMathFunctionName(fn) {
formattable = append([]bool{true}, formattable...)
} else if len(formattable) > 0 && formattable[0] && fn == "" {
formattable = append([]bool{true}, formattable...)
} else {
formattable = append([]bool{false}, formattable...)
}
case char == mRParen:
result = append(result, char)
if len(formattable) > 0 {
formattable = formattable[1:]
}
case char == mComma && len(formattable) > 0 && formattable[0]:
result = append(result, ',', ' ')
case char == mSpace && len(formattable) > 0 && formattable[0] && len(result) > 0 && result[len(result)-1] == mSpace:
// Skip consecutive whitespace.
case (char == mAdd || char == mMul || char == mDiv || char == mSub) && len(formattable) > 0 && formattable[0]:
trimmed := trimRightSpace(result)
var prev, prevPrev byte
if len(trimmed) >= 1 {
prev = trimmed[len(trimmed)-1]
}
if len(trimmed) >= 2 {
prevPrev = trimmed[len(trimmed)-2]
}
var next byte
if i+1 < len(input) {
next = input[i+1]
}
switch {
case (prev == mLowerE || prev == mUpperE) && prevPrev >= mZero && prevPrev <= mNine:
// Scientific notation, e.g. -3.4e-2.
result = append(result, char)
case prev == mAdd || prev == mMul || prev == mDiv || prev == mSub:
result = append(result, char)
case prev == mLParen || prev == mComma:
result = append(result, char)
case i-1 >= 0 && input[i-1] == mSpace:
result = append(result, char, ' ')
case (prev >= mZero && prev <= mNine) ||
(next >= mZero && next <= mNine) ||
prev == mRParen ||
next == mLParen ||
next == mAdd || next == mMul || next == mDiv || next == mSub ||
(lastValuePos != -1 && lastValuePos == i-1):
result = append(result, ' ', char, ' ')
default:
result = append(result, char)
}
default:
result = append(result, char)
}
}
return string(result)
}
func trimRightSpace(b []byte) []byte {
end := len(b)
for end > 0 {
c := b[end-1]
if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' {
end--
continue
}
break
}
return b[:end]
}
// Port of packages/tailwindcss/src/property-order.ts
// The canonical order in which CSS properties are sorted within a rule.
var twPropertyOrder = []string{
"container-type",
"pointer-events",
"visibility",
"position",
"inset",
"inset-inline",
"inset-block",
"inset-inline-start",
"inset-inline-end",
"inset-block-start",
"inset-block-end",
"top",
"right",
"bottom",
"left",
"isolation",
"z-index",
"order",
"grid-column",
"grid-column-start",
"grid-column-end",
"grid-row",
"grid-row-start",
"grid-row-end",
"float",
"clear",
"--tw-container-component",
"margin",
"margin-inline",
"margin-block",
"margin-inline-start",
"margin-inline-end",
"margin-block-start",
"margin-block-end",
"margin-top",
"margin-right",
"margin-bottom",
"margin-left",
"box-sizing",
"display",
"field-sizing",
"aspect-ratio",
"height",
"max-height",
"min-height",
"width",
"max-width",
"min-width",
"flex",
"flex-shrink",
"flex-grow",
"flex-basis",
"table-layout",
"caption-side",
"border-collapse",
"border-spacing",
"--tw-border-spacing-x",
"--tw-border-spacing-y",
"transform-origin",
"translate",
"--tw-translate-x",
"--tw-translate-y",
"--tw-translate-z",
"scale",
"--tw-scale-x",
"--tw-scale-y",
"--tw-scale-z",
"rotate",
"--tw-rotate-x",
"--tw-rotate-y",
"--tw-rotate-z",
"--tw-skew-x",
"--tw-skew-y",
"transform",
"zoom",
"animation",
"cursor",
"touch-action",
"--tw-pan-x",
"--tw-pan-y",
"--tw-pinch-zoom",
"resize",
"scroll-snap-type",
"--tw-scroll-snap-strictness",
"scroll-snap-align",
"scroll-snap-stop",
"scroll-margin",
"scroll-margin-inline",
"scroll-margin-block",
"scroll-margin-inline-start",
"scroll-margin-inline-end",
"scroll-margin-block-start",
"scroll-margin-block-end",
"scroll-margin-top",
"scroll-margin-right",
"scroll-margin-bottom",
"scroll-margin-left",
"scroll-padding",
"scroll-padding-inline",
"scroll-padding-block",
"scroll-padding-inline-start",
"scroll-padding-inline-end",
"scroll-padding-block-start",
"scroll-padding-block-end",
"scroll-padding-top",
"scroll-padding-right",
"scroll-padding-bottom",
"scroll-padding-left",
"scrollbar-width",
"scrollbar-color",
"scrollbar-gutter",
"list-style-position",
"list-style-type",
"list-style-image",
"appearance",
"columns",
"break-before",
"break-inside",
"break-after",
"grid-auto-columns",
"grid-auto-flow",
"grid-auto-rows",
"grid-template-columns",
"grid-template-rows",
"flex-direction",
"flex-wrap",
"place-content",
"place-items",
"align-content",
"align-items",
"justify-content",
"justify-items",
"gap",
"column-gap",
"row-gap",
"--tw-space-x-reverse",
"--tw-space-y-reverse",
"divide-x-width",
"divide-y-width",
"--tw-divide-y-reverse",
"divide-style",
"divide-color",
"place-self",
"align-self",
"justify-self",
"overflow",
"overflow-x",
"overflow-y",
"overscroll-behavior",
"overscroll-behavior-x",
"overscroll-behavior-y",
"scroll-behavior",
"border-radius",
"border-start-radius",
"border-end-radius",
"border-top-radius",
"border-right-radius",
"border-bottom-radius",
"border-left-radius",
"border-start-start-radius",
"border-start-end-radius",
"border-end-end-radius",
"border-end-start-radius",
"border-top-left-radius",
"border-top-right-radius",
"border-bottom-right-radius",
"border-bottom-left-radius",
"border-width",
"border-inline-width",
"border-block-width",
"border-inline-start-width",
"border-inline-end-width",
"border-block-start-width",
"border-block-end-width",
"border-top-width",
"border-right-width",
"border-bottom-width",
"border-left-width",
"border-style",
"border-inline-style",
"border-block-style",
"border-inline-start-style",
"border-inline-end-style",
"border-block-start-style",
"border-block-end-style",
"border-top-style",
"border-right-style",
"border-bottom-style",
"border-left-style",
"border-color",
"border-inline-color",
"border-block-color",
"border-inline-start-color",
"border-inline-end-color",
"border-block-start-color",
"border-block-end-color",
"border-top-color",
"border-right-color",
"border-bottom-color",
"border-left-color",
"background-color",
"background-image",
"--tw-gradient-position",
"--tw-gradient-stops",
"--tw-gradient-via-stops",
"--tw-gradient-from",
"--tw-gradient-from-position",
"--tw-gradient-via",
"--tw-gradient-via-position",
"--tw-gradient-to",
"--tw-gradient-to-position",
"mask-image",
"--tw-mask-top",
"--tw-mask-top-from-color",
"--tw-mask-top-from-position",
"--tw-mask-top-to-color",
"--tw-mask-top-to-position",
"--tw-mask-right",
"--tw-mask-right-from-color",
"--tw-mask-right-from-position",
"--tw-mask-right-to-color",
"--tw-mask-right-to-position",
"--tw-mask-bottom",
"--tw-mask-bottom-from-color",
"--tw-mask-bottom-from-position",
"--tw-mask-bottom-to-color",
"--tw-mask-bottom-to-position",
"--tw-mask-left",
"--tw-mask-left-from-color",
"--tw-mask-left-from-position",
"--tw-mask-left-to-color",
"--tw-mask-left-to-position",
"--tw-mask-linear",
"--tw-mask-linear-position",
"--tw-mask-linear-from-color",
"--tw-mask-linear-from-position",
"--tw-mask-linear-to-color",
"--tw-mask-linear-to-position",
"--tw-mask-radial",
"--tw-mask-radial-shape",
"--tw-mask-radial-size",
"--tw-mask-radial-position",
"--tw-mask-radial-from-color",
"--tw-mask-radial-from-position",
"--tw-mask-radial-to-color",
"--tw-mask-radial-to-position",
"--tw-mask-conic",
"--tw-mask-conic-position",
"--tw-mask-conic-from-color",
"--tw-mask-conic-from-position",
"--tw-mask-conic-to-color",
"--tw-mask-conic-to-position",
"box-decoration-break",
"background-size",
"background-attachment",
"background-clip",
"background-position",
"background-repeat",
"background-origin",
"mask-composite",
"mask-mode",
"mask-type",
"mask-size",
"mask-clip",
"mask-position",
"mask-repeat",
"mask-origin",
"fill",
"stroke",
"stroke-width",
"object-fit",
"object-position",
"padding",
"padding-inline",
"padding-block",
"padding-inline-start",
"padding-inline-end",
"padding-block-start",
"padding-block-end",
"padding-top",
"padding-right",
"padding-bottom",
"padding-left",
"text-align",
"text-indent",
"vertical-align",
"font-family",
"font-feature-settings",
"font-size",
"line-height",
"font-weight",
"letter-spacing",
"text-wrap",
"overflow-wrap",
"word-break",
"text-overflow",
"hyphens",
"white-space",
"tab-size",
"color",
"text-transform",
"font-style",
"font-stretch",
"font-variant-numeric",
"text-decoration-line",
"text-decoration-color",
"text-decoration-style",
"text-decoration-thickness",
"text-underline-offset",
"-webkit-font-smoothing",
"placeholder-color",
"caret-color",
"accent-color",
"color-scheme",
"opacity",
"background-blend-mode",
"mix-blend-mode",
"box-shadow",
"--tw-shadow",
"--tw-shadow-color",
"--tw-ring-shadow",
"--tw-ring-color",
"--tw-inset-shadow",
"--tw-inset-shadow-color",
"--tw-inset-ring-shadow",
"--tw-inset-ring-color",
"--tw-ring-offset-width",
"--tw-ring-offset-color",
"outline",
"outline-width",
"outline-offset",
"outline-color",
"--tw-blur",
"--tw-brightness",
"--tw-contrast",
"--tw-drop-shadow",
"--tw-grayscale",
"--tw-hue-rotate",
"--tw-invert",
"--tw-saturate",
"--tw-sepia",
"filter",
"--tw-backdrop-blur",
"--tw-backdrop-brightness",
"--tw-backdrop-contrast",
"--tw-backdrop-grayscale",
"--tw-backdrop-hue-rotate",
"--tw-backdrop-invert",
"--tw-backdrop-opacity",
"--tw-backdrop-saturate",
"--tw-backdrop-sepia",
"backdrop-filter",
"transition-property",
"transition-behavior",
"transition-delay",
"transition-duration",
"transition-timing-function",
"will-change",
"contain",
"content",
"forced-color-adjust",
}
// twProseCSS returns the complete set of CSSRule nodes for the `prose` utility,
// matching the output of @tailwindcss/typography's default (base) configuration.
func proseCSS() []*AstNode {
p := func(sel string, decls ...*AstNode) *AstNode {
return styleRule(sel, decls...)
}
d := decl
return []*AstNode{
// Root
p("&",
d("color", "var(--tw-prose-body)"),
d("max-width", "65ch"),
d("font-size", "1rem"),
d("line-height", "1.75"),
// Gray theme variables (default)
d("--tw-prose-body", "#374151"),
d("--tw-prose-headings", "#111827"),
d("--tw-prose-lead", "#4b5563"),
d("--tw-prose-links", "#111827"),
d("--tw-prose-bold", "#111827"),
d("--tw-prose-counters", "#6b7280"),
d("--tw-prose-bullets", "#d1d5db"),
d("--tw-prose-hr", "#e5e7eb"),
d("--tw-prose-quotes", "#111827"),
d("--tw-prose-quote-borders", "#e5e7eb"),
d("--tw-prose-captions", "#6b7280"),
d("--tw-prose-kbd", "#111827"),
d("--tw-prose-kbd-shadows", "17 24 39"),
d("--tw-prose-code", "#111827"),
d("--tw-prose-pre-code", "#e5e7eb"),
d("--tw-prose-pre-bg", "#1f2937"),
d("--tw-prose-th-borders", "#d1d5db"),
d("--tw-prose-td-borders", "#e5e7eb"),
// Invert variables
d("--tw-prose-invert-body", "#d1d5db"),
d("--tw-prose-invert-headings", "#fff"),
d("--tw-prose-invert-lead", "#9ca3af"),
d("--tw-prose-invert-links", "#fff"),
d("--tw-prose-invert-bold", "#fff"),
d("--tw-prose-invert-counters", "#9ca3af"),
d("--tw-prose-invert-bullets", "#4b5563"),
d("--tw-prose-invert-hr", "#374151"),
d("--tw-prose-invert-quotes", "#f3f4f6"),
d("--tw-prose-invert-quote-borders", "#374151"),
d("--tw-prose-invert-captions", "#9ca3af"),
d("--tw-prose-invert-kbd", "#fff"),
d("--tw-prose-invert-kbd-shadows", "255 255 255"),
d("--tw-prose-invert-code", "#fff"),
d("--tw-prose-invert-pre-code", "#d1d5db"),
d("--tw-prose-invert-pre-bg", "rgb(0 0 0 / 50%)"),
d("--tw-prose-invert-th-borders", "#4b5563"),
d("--tw-prose-invert-td-borders", "#374151"),
),
// Lead text
p("& [class~=\"lead\"]",
d("color", "var(--tw-prose-lead)"),
d("font-size", "1.25em"),
d("line-height", "1.6"),
d("margin-top", "1.2em"),
d("margin-bottom", "1.2em"),
),
// Links
p("& a",
d("color", "var(--tw-prose-links)"),
d("text-decoration", "underline"),
d("font-weight", "500"),
),
// Strong
p("& strong",
d("color", "var(--tw-prose-bold)"),
d("font-weight", "600"),
),
p("& a strong, & blockquote strong, & thead th strong",
d("color", "inherit"),
),
// Lists
p("& ol",
d("list-style-type", "decimal"),
d("margin-top", "1.25em"),
d("margin-bottom", "1.25em"),
d("padding-inline-start", "1.625em"),
),
p("& ul",
d("list-style-type", "disc"),
d("margin-top", "1.25em"),
d("margin-bottom", "1.25em"),
d("padding-inline-start", "1.625em"),
),
p("& li",
d("margin-top", "0.5em"),
d("margin-bottom", "0.5em"),
),
p("& ol > li",
d("padding-inline-start", "0.375em"),
),
p("& ul > li",
d("padding-inline-start", "0.375em"),
),
p("& ol > li::marker",
d("font-weight", "400"),
d("color", "var(--tw-prose-counters)"),
),
p("& ul > li::marker",
d("color", "var(--tw-prose-bullets)"),
),
p("& > ul > li p",
d("margin-top", "0.75em"),
d("margin-bottom", "0.75em"),
),
p("& > ul > li > p:first-child",
d("margin-top", "1.25em"),
),
p("& > ul > li > p:last-child",
d("margin-bottom", "1.25em"),
),
p("& > ol > li > p:first-child",
d("margin-top", "1.25em"),
),
p("& > ol > li > p:last-child",
d("margin-bottom", "1.25em"),
),
p("& ul ul, & ul ol, & ol ul, & ol ol",
d("margin-top", "0.75em"),
d("margin-bottom", "0.75em"),
),
// Definition lists
p("& dl",
d("margin-top", "1.25em"),
d("margin-bottom", "1.25em"),
),
p("& dt",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "600"),
d("margin-top", "1.25em"),
),
p("& dd",
d("margin-top", "0.5em"),
d("padding-inline-start", "1.625em"),
),
// Paragraphs
p("& p",
d("margin-top", "1.25em"),
d("margin-bottom", "1.25em"),
),
// Headings
p("& h1",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "800"),
d("font-size", "2.25em"),
d("margin-top", "0"),
d("margin-bottom", "0.8888889em"),
d("line-height", "1.1111111"),
),
p("& h1 strong",
d("font-weight", "900"),
d("color", "inherit"),
),
p("& h2",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "700"),
d("font-size", "1.5em"),
d("margin-top", "2em"),
d("margin-bottom", "1em"),
d("line-height", "1.3333333"),
),
p("& h2 strong",
d("font-weight", "800"),
d("color", "inherit"),
),
p("& h3",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "600"),
d("font-size", "1.25em"),
d("margin-top", "1.6em"),
d("margin-bottom", "0.6em"),
d("line-height", "1.6"),
),
p("& h3 strong",
d("font-weight", "700"),
d("color", "inherit"),
),
p("& h4",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "600"),
d("margin-top", "1.5em"),
d("margin-bottom", "0.5em"),
d("line-height", "1.5"),
),
p("& h4 strong",
d("font-weight", "700"),
d("color", "inherit"),
),
// Horizontal rule
p("& hr",
d("border-color", "var(--tw-prose-hr)"),
d("border-top-width", "1px"),
d("margin-top", "3em"),
d("margin-bottom", "3em"),
),
p("& hr + *",
d("margin-top", "0"),
),
p("& h2 + *",
d("margin-top", "0"),
),
p("& h3 + *",
d("margin-top", "0"),
),
p("& h4 + *",
d("margin-top", "0"),
),
// Blockquote
p("& blockquote",
d("font-weight", "500"),
d("font-style", "italic"),
d("color", "var(--tw-prose-quotes)"),
d("border-inline-start-width", "0.25rem"),
d("border-inline-start-color", "var(--tw-prose-quote-borders)"),
d("quotes", "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\""),
d("margin-top", "1.6em"),
d("margin-bottom", "1.6em"),
d("padding-inline-start", "1em"),
),
p("& blockquote p:first-of-type::before",
d("content", "open-quote"),
),
p("& blockquote p:last-of-type::after",
d("content", "close-quote"),
),
// Images and media
p("& img",
d("margin-top", "2em"),
d("margin-bottom", "2em"),
),
p("& picture",
d("display", "block"),
d("margin-top", "2em"),
d("margin-bottom", "2em"),
),
p("& picture > img",
d("margin-top", "0"),
d("margin-bottom", "0"),
),
p("& video",
d("margin-top", "2em"),
d("margin-bottom", "2em"),
),
// Figures
p("& figure",
d("margin-top", "2em"),
d("margin-bottom", "2em"),
),
p("& figure > *",
d("margin-top", "0"),
d("margin-bottom", "0"),
),
p("& figcaption",
d("color", "var(--tw-prose-captions)"),
d("font-size", "0.875em"),
d("line-height", "1.4285714"),
d("margin-top", "0.8571429em"),
),
// Keyboard
p("& kbd",
d("font-weight", "500"),
d("font-family", "inherit"),
d("color", "var(--tw-prose-kbd)"),
d("box-shadow", "0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%)"),
d("font-size", "0.875em"),
d("border-radius", "0.3125rem"),
d("padding-top", "0.1875em"),
d("padding-inline-end", "0.375em"),
d("padding-bottom", "0.1875em"),
d("padding-inline-start", "0.375em"),
),
// Code
p("& code",
d("color", "var(--tw-prose-code)"),
d("font-weight", "600"),
d("font-size", "0.875em"),
),
p("& code::before",
d("content", "\"`\""),
),
p("& code::after",
d("content", "\"`\""),
),
p("& a code, & h1 code, & h2 code, & h3 code, & h4 code, & blockquote code, & thead th code",
d("color", "inherit"),
),
// Pre
p("& pre",
d("color", "var(--tw-prose-pre-code)"),
d("background-color", "var(--tw-prose-pre-bg)"),
d("overflow-x", "auto"),
d("font-weight", "400"),
d("font-size", "0.875em"),
d("line-height", "1.7142857"),
d("margin-top", "1.7142857em"),
d("margin-bottom", "1.7142857em"),
d("border-radius", "0.375rem"),
d("padding-top", "0.8571429em"),
d("padding-inline-end", "1.1428571em"),
d("padding-bottom", "0.8571429em"),
d("padding-inline-start", "1.1428571em"),
),
p("& pre code",
d("background-color", "transparent"),
d("border-width", "0"),
d("border-radius", "0"),
d("padding", "0"),
d("font-weight", "inherit"),
d("color", "inherit"),
d("font-size", "inherit"),
d("font-family", "inherit"),
d("line-height", "inherit"),
),
p("& pre code::before",
d("content", "none"),
),
p("& pre code::after",
d("content", "none"),
),
// Tables
p("& table",
d("width", "100%"),
d("table-layout", "auto"),
d("margin-top", "2em"),
d("margin-bottom", "2em"),
d("font-size", "0.875em"),
d("line-height", "1.7142857"),
),
p("& thead",
d("border-bottom-width", "1px"),
d("border-bottom-color", "var(--tw-prose-th-borders)"),
),
p("& thead th",
d("color", "var(--tw-prose-headings)"),
d("font-weight", "600"),
d("vertical-align", "bottom"),
d("padding-inline-end", "0.5714286em"),
d("padding-bottom", "0.5714286em"),
d("padding-inline-start", "0.5714286em"),
),
p("& thead th:first-child",
d("padding-inline-start", "0"),
),
p("& thead th:last-child",
d("padding-inline-end", "0"),
),
p("& tbody tr",
d("border-bottom-width", "1px"),
d("border-bottom-color", "var(--tw-prose-td-borders)"),
),
p("& tbody tr:last-child",
d("border-bottom-width", "0"),
),
p("& tbody td, & tfoot td",
d("vertical-align", "baseline"),
d("padding-top", "0.5714286em"),
d("padding-inline-end", "0.5714286em"),
d("padding-bottom", "0.5714286em"),
d("padding-inline-start", "0.5714286em"),
),
p("& tbody td:first-child, & tfoot td:first-child",
d("padding-inline-start", "0"),
),
p("& tbody td:last-child, & tfoot td:last-child",
d("padding-inline-end", "0"),
),
p("& tfoot",
d("border-top-width", "1px"),
d("border-top-color", "var(--tw-prose-th-borders)"),
),
p("& th, & td",
d("text-align", "start"),
),
// h2/h3 code sizes
p("& h2 code",
d("font-size", "0.875em"),
),
p("& h3 code",
d("font-size", "0.9em"),
),
// First/last child margin reset
p("& > :first-child",
d("margin-top", "0"),
),
p("& > :last-child",
d("margin-bottom", "0"),
),
}
}
// twProseInvertCSS returns CSSRule nodes for the `prose-invert` modifier.
func proseInvertCSS() []*AstNode {
vars := []string{
"body", "headings", "lead", "links", "bold", "counters", "bullets",
"hr", "quotes", "quote-borders", "captions", "kbd", "kbd-shadows",
"code", "pre-code", "pre-bg", "th-borders", "td-borders",
}
var decls []*AstNode
for _, v := range vars {
decls = append(decls, decl("--tw-prose-"+v, "var(--tw-prose-invert-"+v+")"))
}
return []*AstNode{
styleRule("&", decls...),
}
}
// registerProse registers the non-core `prose`/`prose-invert` typography
// utilities (ported from @tailwindcss/typography's base output). -mta
func registerProse(c *utilCtx) {
c.utilities.static("prose", func(_ *Candidate) *utilResult { return uList(proseCSS()) })
c.utilities.static("prose-invert", func(_ *Candidate) *utilResult { return uList(proseInvertCSS()) })
}
// Port of packages/tailwindcss/src/utils/replace-shadow-colors.ts
//
// The upstream walks the value AST; since every branch returns Skip/ReplaceStop
// (functions are never recursed into), only top-level value nodes matter, so
// this iterates them directly.
var shadowKeywords = map[string]bool{"inset": true, "inherit": true, "initial": true, "revert": true, "unset": true}
var shadowLengthFns = map[string]bool{"calc": true, "clamp": true, "max": true, "min": true, "--spacing": true}
var shadowColorFns = map[string]bool{
"color": true, "color-mix": true, "contrast-color": true, "device-cmyk": true,
"hsl": true, "hsla": true, "hwb": true, "lab": true, "lch": true, "light-dark": true,
"oklab": true, "oklch": true, "rgb": true, "rgba": true, "--alpha": true,
}
var reShadowLength = regexp.MustCompile(`^-?(\d+|\.\d+)(.*?)$`)
func replaceShadowColors(input string, replacement func(color string) string) string {
replaceAst := func(node ValueNode) []ValueNode {
color := valueToCss([]ValueNode{node})
return valueParse(replacement(color))
}
parts := segment(input, ",")
out := make([]string, len(parts))
for pi, shadow := range parts {
shadow = strings.TrimSpace(shadow)
ast := valueParse(shadow)
unknownIdx := -1
unknowns := 0
lengths := 0
replaced := false
for i := 0; i < len(ast); i++ {
switch n := ast[i].(type) {
case *ValueWord:
lw := strings.ToLower(n.Value)
if shadowKeywords[lw] {
continue
}
if reShadowLength.MatchString(lw) {
lengths++
continue
}
if (len(n.Value) > 0 && n.Value[0] == '#') || isNamedColor(n.Value) {
repl := replaceAst(ast[i])
ast = spliceValueNodes(ast, i, repl)
replaced = true
}
if replaced {
break
}
unknownIdx = i
unknowns++
case *ValueFunction:
lf := strings.ToLower(n.Value)
if shadowColorFns[lf] {
repl := replaceAst(ast[i])
ast = spliceValueNodes(ast, i, repl)
replaced = true
break
}
if shadowLengthFns[lf] {
lengths++
continue
}
unknownIdx = i
unknowns++
case *ValueSeparator:
continue
}
if replaced {
break
}
}
if replaced {
out[pi] = valueToCss(ast)
continue
}
if lengths < 2 {
out[pi] = shadow
continue
}
if unknowns == 0 {
out[pi] = shadow + " " + replacement("currentcolor")
continue
}
if unknowns == 1 {
repl := replaceAst(ast[unknownIdx])
ast = spliceValueNodes(ast, unknownIdx, repl)
replaced = true
}
if replaced {
out[pi] = valueToCss(ast)
} else {
out[pi] = shadow
}
}
return strings.Join(out, ", ")
}
func spliceValueNodes(nodes []ValueNode, idx int, repl []ValueNode) []ValueNode {
out := make([]ValueNode, 0, len(nodes)-1+len(repl))
out = append(out, nodes[:idx]...)
out = append(out, repl...)
out = append(out, nodes[idx+1:]...)
return out
}
func twScanFile(path string) ([]string, error) {
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
src := string(content)
// For HTML/template files, also extract tokens from raw content
// to catch class names inside Go template directives ({{ }})
// which break the JS-oriented quote-based extraction.
if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".gohtml") || strings.HasSuffix(path, ".tmpl") {
return extractCandidatesHTML(src), nil
}
return extractCandidates(src), nil
}
func extractCandidatesHTML(src string) []string {
seen := make(map[string]bool)
var results []string
add := func(candidates []string) {
for _, c := range candidates {
if !seen[c] {
seen[c] = true
results = append(results, c)
}
}
}
// Standard extraction from quoted strings
add(extractCandidates(src))
// Also extract tokens from the entire raw content — this catches
// class names inside Go template blocks like {{if ...}}class{{end}}
// where embedded quotes break the string-based extraction.
add(extractTokens(src))
return results
}
func twScanFiles(dir string) ([]string, error) {
var candidates []string
seen := make(map[string]bool)
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
if !strings.HasSuffix(path, ".js") {
return nil
}
content, err := os.ReadFile(path)
if err != nil {
return nil
}
for _, c := range extractCandidates(string(content)) {
if !seen[c] {
seen[c] = true
candidates = append(candidates, c)
}
}
return nil
})
if err != nil {
return nil, err
}
sort.Strings(candidates)
return candidates, nil
}
func extractCandidates(src string) []string {
var results []string
n := len(src)
i := 0
// prevValue is true when the previous significant token can end an
// expression. It tells a `/` apart: division when it follows a value,
// a regex literal otherwise. Without this, a quote inside a regex (e.g.
// /it's/) reads as a string and desyncs the scanner, just like an
// apostrophe in a comment would.
prevValue := false
for i < n {
ch := src[i]
switch {
case ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r':
// Whitespace is insignificant; prevValue carries over.
i++
case ch == '/' && i+1 < n && (src[i+1] == '/' || src[i+1] == '*'):
// Comment — skip so quotes/apostrophes inside (e.g. "don't",
// "button's") don't desync the string scanner. A single stray
// apostrophe in a comment otherwise flips quote parity and swallows
// every class literal until the next quote. Insignificant, so
// prevValue carries over. Strings are matched after this, so a `//`
// inside a quoted URL is never reached here.
i, _ = skipComment(src, i, n)
case ch == '"' || ch == '\'':
// String literal — where class names live.
i++
start := i
for i < n && src[i] != ch {
if src[i] == '\\' && i+1 < n {
i += 2
continue
}
i++
}
results = append(results, extractTokens(src[start:i])...)
if i < n {
i++
}
prevValue = true
case ch == '`':
i++
results = append(results, extractFromTemplate(src, &i, n)...)
prevValue = true
case ch == '/':
// Not a comment (handled above): a regex literal unless it follows
// a value, in which case it's the division operator.
if prevValue {
i++
prevValue = false
} else {
i = skipRegexLiteral(src, i, n)
prevValue = true
}
case isIdentByte(ch):
start := i
for i < n && isIdentByte(src[i]) {
i++
}
prevValue = !keywordExpectsRegex(src[start:i])
case ch == ')' || ch == ']':
prevValue = true
i++
default:
// Any other operator/punctuation ({ } ( = : ; , . ...): a `/` that
// follows is a regex, not division.
prevValue = false
i++
}
}
return results
}
func extractFromTemplate(src string, pos *int, n int) []string {
var results []string
i := *pos
for i < n {
if src[i] == '\\' && i+1 < n {
i += 2
continue
}
if src[i] == '`' {
i++
*pos = i
return results
}
if src[i] == '$' && i+1 < n && src[i+1] == '{' {
i += 2
depth := 1
// Same regex/division disambiguation as extractCandidates, scoped to
// this interpolation. Tracking prevValue (and skipping comments and
// regex literals) keeps quotes inside either from miscounting the
// braces that delimit the ${...} expression.
prevValue := false
for i < n && depth > 0 {
c := src[i]
switch {
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
i++
case c == '/' && i+1 < n && (src[i+1] == '/' || src[i+1] == '*'):
i, _ = skipComment(src, i, n)
case c == '{':
depth++
i++
prevValue = false
case c == '}':
depth--
i++
prevValue = false
case c == '"' || c == '\'':
i++
start := i
for i < n && src[i] != c {
if src[i] == '\\' && i+1 < n {
i += 2
continue
}
i++
}
results = append(results, extractTokens(src[start:i])...)
if i < n {
i++
}
prevValue = true
case c == '`':
i++
results = append(results, extractFromTemplate(src, &i, n)...)
prevValue = true
case c == '/':
if prevValue {
i++
prevValue = false
} else {
i = skipRegexLiteral(src, i, n)
prevValue = true
}
case isIdentByte(c):
start := i
for i < n && isIdentByte(src[i]) {
i++
}
prevValue = !keywordExpectsRegex(src[start:i])
case c == ')' || c == ']':
prevValue = true
i++
default:
prevValue = false
i++
}
}
continue
}
// Accumulate template text content.
start := i
for i < n && src[i] != '`' && src[i] != '\\' && !(src[i] == '$' && i+1 < n && src[i+1] == '{') {
i++
}
if i > start {
results = append(results, extractTokens(src[start:i])...)
}
}
*pos = i
return results
}
func extractTokens(s string) []string {
var tokens []string
n := len(s)
i := 0
for i < n {
// Skip non-candidate characters.
for i < n && !isCandidateChar(s[i]) {
i++
}
if i >= n {
break
}
start := i
for i < n && isCandidateChar(s[i]) {
// Handle bracket groups [...]
if s[i] == '[' {
i++
for i < n && s[i] != ']' {
i++
}
if i < n {
i++
}
continue
}
i++
}
token := s[start:i]
if looksLikeTWCandidate(token) {
tokens = append(tokens, token)
}
}
return tokens
}
func isCandidateChar(ch byte) bool {
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') ||
ch == '-' || ch == '_' || ch == '/' || ch == ':' || ch == '!' || ch == '[' || ch == ']' ||
ch == '#' || ch == '.' || ch == '%' || ch == '(' || ch == ')' || ch == ','
}
func looksLikeTWCandidate(s string) bool {
if len(s) == 0 || len(s) > 200 {
return false
}
// Must start with a letter, ! or -. Also allow `[` to support bare
// arbitrary-property syntax like `[transition:opacity_300ms]` and arbitrary
// variants like `[&_td]:p-4`.
ch := s[0]
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '!' || ch == '-' || ch == '[') {
return false
}
// Filter out things that are clearly not utilities.
if strings.ContainsAny(s, "{}()=<>&|+*~^") {
// Allow parens only inside brackets
if !strings.Contains(s, "[") {
return false
}
}
return true
}
func isASCIILetter(b byte) bool {
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
}
// isIdentByte reports whether b can appear in a JS identifier or number, used
// to consume barewords whole so a keyword can be told from a plain value.
func isIdentByte(b byte) bool {
return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '_' || b == '$'
}
// keywordExpectsRegex reports whether a `/` directly following this bareword is
// a regex literal (the word is a keyword that expects an expression next)
// rather than the division operator.
func keywordExpectsRegex(word string) bool {
switch word {
case "return", "typeof", "instanceof", "in", "of", "new", "delete",
"void", "do", "else", "case", "yield", "await", "throw":
return true
}
return false
}
// skipComment skips a // line comment or /* */ block comment that starts at i
// (src[i] must be '/'). ok is false when i is not the start of a comment.
func skipComment(src string, i, n int) (int, bool) {
if i+1 >= n || src[i] != '/' {
return i, false
}
switch src[i+1] {
case '/':
i += 2
for i < n && src[i] != '\n' {
i++
}
return i, true
case '*':
i += 2
for i+1 < n && !(src[i] == '*' && src[i+1] == '/') {
i++
}
i += 2 // consume the closing */
if i > n {
i = n
}
return i, true
}
return i, false
}
// skipRegexLiteral skips a /.../flags regex literal whose opening slash is at i.
// Character classes [...] are honored so a `/` inside them doesn't end the
// literal; a newline aborts (a real regex can't span one) to bound any runaway
// from a mis-detected division.
func skipRegexLiteral(src string, i, n int) int {
i++ // opening /
inClass := false
for i < n {
c := src[i]
switch {
case c == '\\' && i+1 < n:
i += 2
case c == '\n':
return i
case c == '[':
inClass = true
i++
case c == ']':
inClass = false
i++
case c == '/' && !inClass:
i++ // closing /
for i < n && isASCIILetter(src[i]) {
i++ // flags
}
return i
default:
i++
}
}
return i
}
// Port of packages/tailwindcss/src/utils/segment.ts
//
// Splits a string on a top-level occurrence of a single-character separator,
// ignoring separators that appear inside (), [], {} or quoted strings. Regex
// can't do balanced matching, so this is a tiny state machine — identical in
// behaviour to the upstream implementation. Operates on bytes; all structural
// characters are ASCII and UTF-8 continuation bytes are always >= 0x80, so
// multi-byte runes never collide with them.
func segment(input, separator string) []string {
if separator == "" {
return []string{input}
}
sep := separator[0]
var stack []byte // expected closing brackets
var parts []string
lastPos := 0
for i := 0; i < len(input); i++ {
c := input[i]
if len(stack) == 0 && c == sep {
parts = append(parts, input[lastPos:i])
lastPos = i + 1
continue
}
switch c {
case '\\':
// Next character is escaped; skip it.
i++
case '\'', '"':
// Consume the whole string literal.
for i++; i < len(input); i++ {
nc := input[i]
if nc == '\\' {
i++
continue
}
if nc == c {
break
}
}
case '(':
stack = append(stack, ')')
case '[':
stack = append(stack, ']')
case '{':
stack = append(stack, '}')
case ')', ']', '}':
if len(stack) > 0 && c == stack[len(stack)-1] {
stack = stack[:len(stack)-1]
}
}
}
parts = append(parts, input[lastPos:])
return parts
}
// Port of packages/tailwindcss/src/sort.ts
type classOrderEntry struct {
class string
order *big.Int // nil for non-Tailwind classes
}
func getClassOrder(ds *DesignSystem, classes []string) []classOrderEntry {
astNodes, nodeSorting := compileCandidates(classes, ds, nil, true)
sorted := map[string]*big.Int{}
for _, c := range classes {
sorted[c] = nil
}
idx := big.NewInt(0)
for _, node := range astNodes {
meta, ok := nodeSorting[node]
if !ok || meta.candidate == "" {
continue
}
if sorted[meta.candidate] == nil {
sorted[meta.candidate] = new(big.Int).Set(idx)
idx.Add(idx, big.NewInt(1))
}
}
out := make([]classOrderEntry, len(classes))
for i, c := range classes {
out[i] = classOrderEntry{class: c, order: sorted[c]}
}
return out
}
// Port of packages/tailwindcss/src/theme.ts
//
// The theme stores `--namespace-key` design tokens (from the default theme.css
// and the project's @theme block) and resolves utility values against them.
// Insertion order is preserved (the upstream code relies on JS Map order for
// deterministic emission and namespace iteration).
type ThemeOptions int
const (
themeNone ThemeOptions = 0
themeInline ThemeOptions = 1 << 0
themeReference ThemeOptions = 1 << 1
themeDefault ThemeOptions = 1 << 2
themeStatic ThemeOptions = 1 << 3
themeUsed ThemeOptions = 1 << 4
)
var ignoredThemeKeyMap = map[string][]string{
"--font": {"--font-weight", "--font-size"},
"--inset": {"--inset-shadow", "--inset-ring"},
"--text": {"--text-color", "--text-decoration-color", "--text-decoration-thickness", "--text-indent", "--text-shadow", "--text-underline-offset"},
"--grid-column": {"--grid-column-start", "--grid-column-end"},
"--grid-row": {"--grid-row-start", "--grid-row-end"},
}
func isIgnoredThemeKey(themeKey, namespace string) bool {
for _, ig := range ignoredThemeKeyMap[namespace] {
if themeKey == ig || strings.HasPrefix(themeKey, ig+"-") {
return true
}
}
return false
}
type themeValue struct {
value string
options ThemeOptions
}
type Theme struct {
Prefix string
values map[string]*themeValue
order []string
keyframes []*AstNode
}
func NewTheme() *Theme {
return &Theme{values: make(map[string]*themeValue)}
}
func sptr(s string) *string { return &s }
func (t *Theme) setVal(key string, v *themeValue) {
if _, ok := t.values[key]; !ok {
t.order = append(t.order, key)
}
t.values[key] = v
}
func (t *Theme) delVal(key string) {
if _, ok := t.values[key]; !ok {
return
}
delete(t.values, key)
for i, k := range t.order {
if k == key {
t.order = append(t.order[:i], t.order[i+1:]...)
break
}
}
}
func (t *Theme) Size() int { return len(t.values) }
func (t *Theme) add(key, value string, options ThemeOptions) {
if strings.HasSuffix(key, "-*") {
if value != "initial" {
// Invalid usage upstream throws; we ignore to stay non-fatal.
return
}
if key == "--*" {
t.values = make(map[string]*themeValue)
t.order = nil
} else {
t.clearNamespace(key[:len(key)-2], themeNone)
}
}
if options&themeDefault != 0 {
if existing, ok := t.values[key]; ok && existing.options&themeDefault == 0 {
return
}
}
if value == "initial" {
t.delVal(key)
} else {
t.setVal(key, &themeValue{value: value, options: options})
}
}
func (t *Theme) keysInNamespaces(themeKeys []string) []string {
var keys []string
for _, namespace := range themeKeys {
prefix := namespace + "-"
for _, key := range t.order {
if !strings.HasPrefix(key, prefix) {
continue
}
if strings.Contains(key[2:], "--") {
continue
}
if isIgnoredThemeKey(key, namespace) {
continue
}
keys = append(keys, key[len(prefix):])
}
}
return keys
}
func (t *Theme) Get(themeKeys []string) (string, bool) {
for _, key := range themeKeys {
if v, ok := t.values[key]; ok {
return v.value, true
}
}
return "", false
}
func (t *Theme) Has(key string) bool {
_, ok := t.values[key]
return ok
}
func (t *Theme) hasDefault(key string) bool {
return t.getOptions(key)&themeDefault == themeDefault
}
func (t *Theme) getOptions(key string) ThemeOptions {
key = unescape(t.unprefixKey(key))
if v, ok := t.values[key]; ok {
return v.options
}
return themeNone
}
func (t *Theme) prefixKey(key string) string {
if t.Prefix == "" {
return key
}
return "--" + t.Prefix + "-" + key[2:]
}
func (t *Theme) unprefixKey(key string) string {
if t.Prefix == "" {
return key
}
return "--" + key[3+len(t.Prefix):]
}
func (t *Theme) clearNamespace(namespace string, clearOptions ThemeOptions) {
ignored := ignoredThemeKeyMap[namespace]
var toDelete []string
outer:
for _, key := range t.order {
if strings.HasPrefix(key, namespace) {
if clearOptions != themeNone {
options := t.getOptions(key)
if options&clearOptions != clearOptions {
continue
}
}
for _, ig := range ignored {
if strings.HasPrefix(key, ig) {
continue outer
}
}
toDelete = append(toDelete, key)
}
}
for _, key := range toDelete {
t.delVal(key)
}
}
func (t *Theme) resolveKey(candidateValue *string, themeKeys []string) string {
for _, namespace := range themeKeys {
var themeKey string
if candidateValue != nil {
themeKey = namespace + "-" + *candidateValue
} else {
themeKey = namespace
}
if _, ok := t.values[themeKey]; !ok {
if candidateValue != nil && strings.Contains(*candidateValue, ".") {
themeKey = namespace + "-" + strings.ReplaceAll(*candidateValue, ".", "_")
if _, ok := t.values[themeKey]; !ok {
continue
}
} else {
continue
}
}
if isIgnoredThemeKey(themeKey, namespace) {
continue
}
return themeKey
}
return ""
}
func (t *Theme) varFn(themeKey string) string {
v, ok := t.values[themeKey]
if !ok {
return ""
}
fallback := ""
if v.options&themeReference != 0 {
fallback = v.value
}
s := "var(" + escape(t.prefixKey(themeKey))
if fallback != "" {
s += ", " + fallback
}
s += ")"
return s
}
func (t *Theme) markUsedVariable(themeKey string) bool {
key := unescape(t.unprefixKey(themeKey))
v, ok := t.values[key]
if !ok {
return false
}
wasUsed := v.options&themeUsed != 0
v.options |= themeUsed
return !wasUsed
}
func (t *Theme) resolve(candidateValue *string, themeKeys []string, options ThemeOptions) (string, bool) {
key := t.resolveKey(candidateValue, themeKeys)
if key == "" {
return "", false
}
v := t.values[key]
if (options|v.options)&themeInline != 0 {
return v.value, true
}
return t.varFn(key), true
}
func (t *Theme) resolveValue(candidateValue *string, themeKeys []string) (string, bool) {
key := t.resolveKey(candidateValue, themeKeys)
if key == "" {
return "", false
}
return t.values[key].value, true
}
func (t *Theme) resolveWith(candidateValue string, themeKeys []string, nestedKeys []string) (string, map[string]string, bool) {
key := t.resolveKey(&candidateValue, themeKeys)
if key == "" {
return "", nil, false
}
extra := map[string]string{}
for _, name := range nestedKeys {
nestedKey := key + name
nv, ok := t.values[nestedKey]
if !ok {
continue
}
if nv.options&themeInline != 0 {
extra[name] = nv.value
} else {
extra[name] = t.varFn(nestedKey)
}
}
v := t.values[key]
if v.options&themeInline != 0 {
return v.value, extra, true
}
return t.varFn(key), extra, true
}
// ThemeNamespace is the flattened view of a single theme namespace.
type ThemeNamespace struct {
m map[string]string
order []string
hasNull bool
nullVal string
}
func (n *ThemeNamespace) Get(key string) (string, bool) {
v, ok := n.m[key]
return v, ok
}
func (n *ThemeNamespace) GetNull() (string, bool) {
return n.nullVal, n.hasNull
}
func (n *ThemeNamespace) set(key, value string) {
if _, ok := n.m[key]; !ok {
n.order = append(n.order, key)
}
n.m[key] = value
}
func (n *ThemeNamespace) Keys() []string { return n.order }
// Values returns the namespace values in insertion order (including the null
// entry's value, if present, matching JS Map iteration order).
func (n *ThemeNamespace) Values() []string {
var out []string
if n.hasNull {
out = append(out, n.nullVal)
}
for _, k := range n.order {
out = append(out, n.m[k])
}
return out
}
func (t *Theme) namespace(namespace string) *ThemeNamespace {
ns := &ThemeNamespace{m: map[string]string{}}
prefix := namespace + "-"
for _, key := range t.order {
v := t.values[key]
switch {
case key == namespace:
ns.hasNull = true
ns.nullVal = v.value
case strings.HasPrefix(key, prefix+"-"):
// Preserve `--` prefix for sub-variables (e.g. --text-sm--line-height).
ns.set(key[len(namespace):], v.value)
case strings.HasPrefix(key, prefix):
ns.set(key[len(prefix):], v.value)
}
}
return ns
}
func (t *Theme) addKeyframes(value *AstNode) {
t.keyframes = append(t.keyframes, value)
}
func (t *Theme) getKeyframes() []*AstNode {
return t.keyframes
}
// Port of packages/tailwindcss/src/utilities.ts
//
// The Utilities registry plus the helper builders (staticUtility,
// functionalUtility, colorUtility, spacingUtility) and the full utility
// catalog registered by createUtilities.
//
// Deviation: the IntelliSense suggestion layer (suggest/getCompletions) is
// stubbed to no-ops — it does not affect generated CSS, only IDE autocomplete.
// -mta
type utilKind int
const (
utilStatic utilKind = iota
utilFunctional
)
type UtilityOptions struct {
Types []string
}
// utilResult models the TS tri-state return of a compile fn:
//
// nil -> undefined (skip this utility, try the next)
// {null:true} -> null (invalid; bail to fallback utilities if typed)
// {nodes:[...]} -> the produced AST nodes
type utilResult struct {
nodes []*AstNode
null bool
}
func uNodes(nodes ...*AstNode) *utilResult { return &utilResult{nodes: nodes} }
func uList(nodes []*AstNode) *utilResult { return &utilResult{nodes: nodes} }
func uNull() *utilResult { return &utilResult{null: true} }
type Utility struct {
kind utilKind
compileFn func(*Candidate) *utilResult
options *UtilityOptions
}
type Utilities struct {
m map[string][]*Utility
order []string
}
func NewUtilities() *Utilities {
return &Utilities{m: make(map[string][]*Utility)}
}
func (u *Utilities) addUtility(name string, util *Utility) {
if _, ok := u.m[name]; !ok {
u.order = append(u.order, name)
}
u.m[name] = append(u.m[name], util)
}
func (u *Utilities) static(name string, fn func(*Candidate) *utilResult) {
u.addUtility(name, &Utility{kind: utilStatic, compileFn: fn})
}
func (u *Utilities) functional(name string, fn func(*Candidate) *utilResult, options *UtilityOptions) {
u.addUtility(name, &Utility{kind: utilFunctional, compileFn: fn, options: options})
}
func (u *Utilities) has(name string, kind utilKind) bool {
fns, ok := u.m[name]
if !ok {
return false
}
for _, f := range fns {
if f.kind == kind {
return true
}
}
return false
}
func (u *Utilities) get(name string) []*Utility { return u.m[name] }
func (u *Utilities) keys(kind utilKind) []string {
var keys []string
for _, key := range u.order {
for _, f := range u.m[key] {
if f.kind == kind {
keys = append(keys, key)
break
}
}
}
return keys
}
// ---- color/alpha helpers ------------------------------------------------
func withAlpha(value, alpha string) string {
if f, ok := jsParseNumber(alpha); ok {
alpha = jsNumberToString(f*100) + "%"
}
if alpha == "100%" {
return value
}
return "color-mix(in oklab, " + value + " " + alpha + ", transparent)"
}
func replaceAlpha(value, alpha string) string {
if f, ok := jsParseNumber(alpha); ok {
alpha = jsNumberToString(f*100) + "%"
}
return "oklab(from " + value + " l a b / " + alpha + ")"
}
// asColor resolves a color value plus an optional opacity modifier. Returns
// (value, true) or ("", false) when the modifier is an invalid opacity.
func asColor(value string, modifier *CandidateModifier, theme *Theme) (string, bool) {
if modifier == nil {
return value, true
}
if modifier.Kind == modArbitrary {
return withAlpha(value, modifier.Value), true
}
if alpha, ok := theme.resolve(&modifier.Value, []string{"--opacity"}, themeNone); ok && alpha != "" {
return withAlpha(value, alpha), true
}
if !isValidOpacityValue(modifier.Value) {
return "", false
}
return withAlpha(value, modifier.Value+"%"), true
}
func resolveThemeColor(candidate *Candidate, theme *Theme, themeKeys []string) (string, bool) {
var value string
var ok bool
switch candidate.Value.Value {
case "inherit":
value, ok = "inherit", true
case "transparent":
value, ok = "transparent", true
case "current":
value, ok = "currentcolor", true
default:
value, ok = theme.resolve(&candidate.Value.Value, themeKeys, themeNone)
}
if !ok {
return "", false
}
return asColor(value, candidate.Modifier, theme)
}
func property(ident, initialValue, syntax string) *AstNode {
syntaxStr := `"*"`
if syntax != "" {
syntaxStr = `"` + syntax + `"`
}
nodes := []*AstNode{decl("syntax", syntaxStr), decl("inherits", "false")}
if initialValue != "" {
nodes = append(nodes, decl("initial-value", initialValue))
}
return atRule("@property", ident, nodes...)
}
// ---- utility registration helpers --------------------------------------
// utilCtx carries the theme + registry through the catalog registration helpers
// (the upstream closures over `theme`/`utilities` inside createUtilities).
type utilCtx struct {
theme *Theme
utilities *Utilities
}
// staticDecl is one entry of a static utility: either a property/value pair or
// a node-producing function.
type staticDecl struct {
prop string
val string
fn func() *AstNode
}
func sd(prop, val string) staticDecl { return staticDecl{prop: prop, val: val} }
func sdFn(fn func() *AstNode) staticDecl { return staticDecl{fn: fn} }
func (c *utilCtx) staticUtility(className string, decls []staticDecl) {
c.utilities.static(className, func(_ *Candidate) *utilResult {
nodes := make([]*AstNode, len(decls))
for i, d := range decls {
if d.fn != nil {
nodes[i] = d.fn()
} else {
nodes[i] = decl(d.prop, d.val)
}
}
return uList(nodes)
})
}
// suggest is a no-op: IntelliSense suggestions don't affect generated CSS.
func (c *utilCtx) suggest(classRoot string, defns func() []any) {}
type utilityDescription struct {
supportsNegative bool
supportsFractions bool
themeKeys []string
// defaultValueSet distinguishes "undefined" (false) from an explicit
// default (true). When set, defaultValue==nil means null.
defaultValueSet bool
defaultValue *string
staticValues map[string][]*AstNode
handleBareValue func(*UtilityValue) (string, bool)
handleNegativeBareValue func(*UtilityValue) (string, bool)
handle func(value, dataType string) *utilResult
}
func (c *utilCtx) functionalUtility(classRoot string, desc utilityDescription) {
make := func(negative bool) func(*Candidate) *utilResult {
return func(candidate *Candidate) *utilResult {
var value *string
dataType := ""
if candidate.Value == nil {
if candidate.Modifier != nil {
return nil
}
if desc.defaultValueSet {
value = desc.defaultValue
} else {
if v, ok := c.theme.resolve(nil, desc.themeKeys, themeNone); ok {
value = &v
}
}
} else if candidate.Value.Kind == uvArbitrary {
if candidate.Modifier != nil {
return nil
}
v := candidate.Value.Value
value = &v
dataType = candidate.Value.DataType
} else {
key := candidate.Value.Value
if candidate.Value.Fraction != "" {
key = candidate.Value.Fraction
}
if v, ok := c.theme.resolve(&key, desc.themeKeys, themeNone); ok {
value = &v
}
if value == nil && desc.supportsFractions && candidate.Value.Fraction != "" {
fparts := segment(candidate.Value.Fraction, "/")
if len(fparts) != 2 || !isPositiveInteger(fparts[0]) || !isPositiveInteger(fparts[1]) {
return nil
}
s := "calc(" + fparts[0] + " / " + fparts[1] + " * 100%)"
value = &s
}
if value == nil && negative && desc.handleNegativeBareValue != nil {
v, ok := desc.handleNegativeBareValue(candidate.Value)
var vv *string
if ok {
vv = &v
}
includesSlash := vv != nil && strings.Contains(*vv, "/")
if !includesSlash && candidate.Modifier != nil {
return nil
}
if vv != nil {
return desc.handle(*vv, "")
}
}
if value == nil && desc.handleBareValue != nil {
if v, ok := desc.handleBareValue(candidate.Value); ok {
value = &v
}
includesSlash := value != nil && strings.Contains(*value, "/")
if !includesSlash && candidate.Modifier != nil {
return nil
}
}
if value == nil && !negative && desc.staticValues != nil && candidate.Modifier == nil {
if fb, ok := desc.staticValues[candidate.Value.Value]; ok {
return uList(cloneAstNodes(fb))
}
}
}
if value == nil {
return nil
}
handleVal := *value
if negative {
handleVal = addWhitespaceAroundMathOperators("calc(" + *value + " * -1)")
}
return desc.handle(handleVal, dataType)
}
}
if desc.supportsNegative {
c.utilities.functional("-"+classRoot, make(true), nil)
}
c.utilities.functional(classRoot, make(false), nil)
}
type colorUtilityDescription struct {
themeKeys []string
handle func(value string) *utilResult
}
func (c *utilCtx) colorUtility(classRoot string, desc colorUtilityDescription) {
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
var value string
var ok bool
if candidate.Value.Kind == uvArbitrary {
value, ok = asColor(candidate.Value.Value, candidate.Modifier, c.theme)
} else {
value, ok = resolveThemeColor(candidate, c.theme, desc.themeKeys)
}
if !ok {
return nil
}
return desc.handle(value)
}, nil)
}
func (c *utilCtx) spacingUtility(name string, themeKeys []string, handle func(value string) *utilResult, opts spacingOpts) {
if opts.supportsNegative {
c.utilities.static("-"+name+"-px", func(_ *Candidate) *utilResult { return handle("-1px") })
}
c.utilities.static(name+"-px", func(_ *Candidate) *utilResult { return handle("1px") })
nullDefault := (*string)(nil)
c.functionalUtility(name, utilityDescription{
themeKeys: themeKeys,
supportsFractions: opts.supportsFractions,
supportsNegative: opts.supportsNegative,
defaultValueSet: true,
defaultValue: nullDefault,
handleBareValue: func(v *UtilityValue) (string, bool) {
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
return "", false
}
if !isValidSpacingMultiplier(v.Value) {
return "", false
}
return "--spacing(" + v.Value + ")", true
},
handleNegativeBareValue: func(v *UtilityValue) (string, bool) {
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
return "", false
}
if !isValidSpacingMultiplier(v.Value) {
return "", false
}
return "--spacing(-" + v.Value + ")", true
},
handle: func(value, _ string) *utilResult { return handle(value) },
staticValues: opts.staticValues,
})
}
type spacingOpts struct {
supportsNegative bool
supportsFractions bool
staticValues map[string][]*AstNode
}
// createUtilities builds the full utility registry for a theme.
func createUtilities(theme *Theme) *Utilities {
c := &utilCtx{theme: theme, utilities: NewUtilities()}
registerUtilities(c)
registerProse(c)
return c.utilities
}
// Port of the createUtilities() catalog from
// packages/tailwindcss/src/utilities.ts. Each registration mirrors the upstream
// staticUtility/functionalUtility/colorUtility/spacingUtility calls. suggest()
// calls are omitted (IntelliSense only).
// bareInteger is the common handleBareValue that accepts a positive integer.
func bareInteger(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value, true
}
func registerUtilities(c *utilCtx) {
d := decl
c.staticUtility("sr-only", []staticDecl{
sd("position", "absolute"), sd("width", "1px"), sd("height", "1px"),
sd("padding", "0"), sd("margin", "-1px"), sd("overflow", "hidden"),
sd("clip-path", "inset(50%)"), sd("white-space", "nowrap"), sd("border-width", "0"),
})
c.staticUtility("not-sr-only", []staticDecl{
sd("position", "static"), sd("width", "auto"), sd("height", "auto"),
sd("padding", "0"), sd("margin", "0"), sd("overflow", "visible"),
sd("clip-path", "none"), sd("white-space", "normal"),
})
c.staticUtility("pointer-events-none", []staticDecl{sd("pointer-events", "none")})
c.staticUtility("pointer-events-auto", []staticDecl{sd("pointer-events", "auto")})
c.staticUtility("visible", []staticDecl{sd("visibility", "visible")})
c.staticUtility("invisible", []staticDecl{sd("visibility", "hidden")})
c.staticUtility("collapse", []staticDecl{sd("visibility", "collapse")})
c.staticUtility("static", []staticDecl{sd("position", "static")})
c.staticUtility("fixed", []staticDecl{sd("position", "fixed")})
c.staticUtility("absolute", []staticDecl{sd("position", "absolute")})
c.staticUtility("relative", []staticDecl{sd("position", "relative")})
c.staticUtility("sticky", []staticDecl{sd("position", "sticky")})
for _, pair := range [][2]string{
{"inset", "inset"}, {"inset-x", "inset-inline"}, {"inset-y", "inset-block"},
{"inset-s", "inset-inline-start"}, {"inset-e", "inset-inline-end"},
{"inset-bs", "inset-block-start"}, {"inset-be", "inset-block-end"},
{"top", "top"}, {"right", "right"}, {"bottom", "bottom"}, {"left", "left"},
} {
name, prop := pair[0], pair[1]
c.staticUtility(name+"-auto", []staticDecl{sd(prop, "auto")})
c.staticUtility(name+"-full", []staticDecl{sd(prop, "100%")})
c.staticUtility("-"+name+"-full", []staticDecl{sd(prop, "-100%")})
c.spacingUtility(name, []string{"--inset", "--spacing"},
func(value string) *utilResult { return uNodes(d(prop, value)) },
spacingOpts{supportsNegative: true, supportsFractions: true})
}
c.staticUtility("isolate", []staticDecl{sd("isolation", "isolate")})
c.staticUtility("isolation-auto", []staticDecl{sd("isolation", "auto")})
c.functionalUtility("z", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--z-index"},
handle: func(value, _ string) *utilResult { return uNodes(d("z-index", value)) },
staticValues: map[string][]*AstNode{"auto": {d("z-index", "auto")}},
})
c.functionalUtility("order", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--order"},
handle: func(value, _ string) *utilResult { return uNodes(d("order", value)) },
staticValues: map[string][]*AstNode{
"first": {d("order", "-9999")},
"last": {d("order", "9999")},
},
})
c.functionalUtility("col", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-column"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-column", "auto")}},
})
c.functionalUtility("col-span", utilityDescription{
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult {
return uNodes(d("grid-column", "span "+value+" / span "+value))
},
staticValues: map[string][]*AstNode{"full": {d("grid-column", "1 / -1")}},
})
c.functionalUtility("col-start", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-column-start"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column-start", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-column-start", "auto")}},
})
c.functionalUtility("col-end", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-column-end"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column-end", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-column-end", "auto")}},
})
c.functionalUtility("row", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-row"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-row", "auto")}},
})
c.functionalUtility("row-span", utilityDescription{
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult {
return uNodes(d("grid-row", "span "+value+" / span "+value))
},
staticValues: map[string][]*AstNode{"full": {d("grid-row", "1 / -1")}},
})
c.functionalUtility("row-start", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-row-start"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row-start", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-row-start", "auto")}},
})
c.functionalUtility("row-end", utilityDescription{
supportsNegative: true,
handleBareValue: bareInteger,
themeKeys: []string{"--grid-row-end"},
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row-end", value)) },
staticValues: map[string][]*AstNode{"auto": {d("grid-row-end", "auto")}},
})
c.staticUtility("float-start", []staticDecl{sd("float", "inline-start")})
c.staticUtility("float-end", []staticDecl{sd("float", "inline-end")})
c.staticUtility("float-right", []staticDecl{sd("float", "right")})
c.staticUtility("float-left", []staticDecl{sd("float", "left")})
c.staticUtility("float-none", []staticDecl{sd("float", "none")})
c.staticUtility("clear-start", []staticDecl{sd("clear", "inline-start")})
c.staticUtility("clear-end", []staticDecl{sd("clear", "inline-end")})
c.staticUtility("clear-right", []staticDecl{sd("clear", "right")})
c.staticUtility("clear-left", []staticDecl{sd("clear", "left")})
c.staticUtility("clear-both", []staticDecl{sd("clear", "both")})
c.staticUtility("clear-none", []staticDecl{sd("clear", "none")})
for _, pair := range [][2]string{
{"m", "margin"}, {"mx", "margin-inline"}, {"my", "margin-block"},
{"ms", "margin-inline-start"}, {"me", "margin-inline-end"},
{"mbs", "margin-block-start"}, {"mbe", "margin-block-end"},
{"mt", "margin-top"}, {"mr", "margin-right"}, {"mb", "margin-bottom"}, {"ml", "margin-left"},
} {
namespace, prop := pair[0], pair[1]
c.staticUtility(namespace+"-auto", []staticDecl{sd(prop, "auto")})
c.spacingUtility(namespace, []string{"--margin", "--spacing"},
func(value string) *utilResult { return uNodes(d(prop, value)) },
spacingOpts{supportsNegative: true})
}
c.staticUtility("box-border", []staticDecl{sd("box-sizing", "border-box")})
c.staticUtility("box-content", []staticDecl{sd("box-sizing", "content-box")})
registerUtilities2(c)
}
func registerUtilities2(c *utilCtx) {
d := decl
c.functionalUtility("line-clamp", utilityDescription{
themeKeys: []string{"--line-clamp"},
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult {
return uNodes(
d("overflow", "hidden"),
d("display", "-webkit-box"),
d("-webkit-box-orient", "vertical"),
d("-webkit-line-clamp", value),
)
},
staticValues: map[string][]*AstNode{
"none": {
d("overflow", "visible"),
d("display", "block"),
d("-webkit-box-orient", "horizontal"),
d("-webkit-line-clamp", "unset"),
},
},
})
for _, pair := range [][2]string{
{"block", "block"}, {"inline-block", "inline-block"}, {"inline", "inline"},
{"hidden", "none"}, {"inline-flex", "inline-flex"}, {"table", "table"},
{"inline-table", "inline-table"}, {"table-caption", "table-caption"},
{"table-cell", "table-cell"}, {"table-column", "table-column"},
{"table-column-group", "table-column-group"}, {"table-footer-group", "table-footer-group"},
{"table-header-group", "table-header-group"}, {"table-row-group", "table-row-group"},
{"table-row", "table-row"}, {"flow-root", "flow-root"}, {"flex", "flex"},
{"grid", "grid"}, {"inline-grid", "inline-grid"}, {"contents", "contents"},
{"list-item", "list-item"},
} {
c.staticUtility(pair[0], []staticDecl{sd("display", pair[1])})
}
c.staticUtility("field-sizing-content", []staticDecl{sd("field-sizing", "content")})
c.staticUtility("field-sizing-fixed", []staticDecl{sd("field-sizing", "fixed")})
c.functionalUtility("aspect", utilityDescription{
themeKeys: []string{"--aspect"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if v.Fraction == "" {
return "", false
}
fparts := segment(v.Fraction, "/")
if len(fparts) != 2 || !isValidSpacingMultiplier(fparts[0]) || !isValidSpacingMultiplier(fparts[1]) {
return "", false
}
return v.Fraction, true
},
handle: func(value, _ string) *utilResult { return uNodes(d("aspect-ratio", value)) },
staticValues: map[string][]*AstNode{
"auto": {d("aspect-ratio", "auto")},
"square": {d("aspect-ratio", "1 / 1")},
},
})
// size / w / h / min / max statics
for _, pair := range [][2]string{
{"full", "100%"}, {"svw", "100svw"}, {"lvw", "100lvw"}, {"dvw", "100dvw"},
{"svh", "100svh"}, {"lvh", "100lvh"}, {"dvh", "100dvh"},
{"min", "min-content"}, {"max", "max-content"}, {"fit", "fit-content"},
} {
key, value := pair[0], pair[1]
c.staticUtility("size-"+key, []staticDecl{sd("--tw-sort", "size"), sd("width", value), sd("height", value)})
c.staticUtility("w-"+key, []staticDecl{sd("width", value)})
c.staticUtility("h-"+key, []staticDecl{sd("height", value)})
c.staticUtility("min-w-"+key, []staticDecl{sd("min-width", value)})
c.staticUtility("min-h-"+key, []staticDecl{sd("min-height", value)})
c.staticUtility("max-w-"+key, []staticDecl{sd("max-width", value)})
c.staticUtility("max-h-"+key, []staticDecl{sd("max-height", value)})
}
c.staticUtility("size-auto", []staticDecl{sd("--tw-sort", "size"), sd("width", "auto"), sd("height", "auto")})
c.staticUtility("w-auto", []staticDecl{sd("width", "auto")})
c.staticUtility("h-auto", []staticDecl{sd("height", "auto")})
c.staticUtility("min-w-auto", []staticDecl{sd("min-width", "auto")})
c.staticUtility("min-h-auto", []staticDecl{sd("min-height", "auto")})
c.staticUtility("h-lh", []staticDecl{sd("height", "1lh")})
c.staticUtility("min-h-lh", []staticDecl{sd("min-height", "1lh")})
c.staticUtility("max-h-lh", []staticDecl{sd("max-height", "1lh")})
c.staticUtility("w-screen", []staticDecl{sd("width", "100vw")})
c.staticUtility("min-w-screen", []staticDecl{sd("min-width", "100vw")})
c.staticUtility("max-w-screen", []staticDecl{sd("max-width", "100vw")})
c.staticUtility("h-screen", []staticDecl{sd("height", "100vh")})
c.staticUtility("min-h-screen", []staticDecl{sd("min-height", "100vh")})
c.staticUtility("max-h-screen", []staticDecl{sd("max-height", "100vh")})
c.staticUtility("max-w-none", []staticDecl{sd("max-width", "none")})
c.staticUtility("max-h-none", []staticDecl{sd("max-height", "none")})
c.spacingUtility("size", []string{"--size", "--spacing"},
func(value string) *utilResult {
return uNodes(d("--tw-sort", "size"), d("width", value), d("height", value))
}, spacingOpts{supportsFractions: true})
for _, e := range []struct {
name string
namespaces []string
property string
}{
{"w", []string{"--width", "--spacing", "--container"}, "width"},
{"min-w", []string{"--min-width", "--spacing", "--container"}, "min-width"},
{"max-w", []string{"--max-width", "--spacing", "--container"}, "max-width"},
{"h", []string{"--height", "--spacing"}, "height"},
{"min-h", []string{"--min-height", "--height", "--spacing"}, "min-height"},
{"max-h", []string{"--max-height", "--height", "--spacing"}, "max-height"},
} {
prop := e.property
c.spacingUtility(e.name, e.namespaces,
func(value string) *utilResult { return uNodes(d(prop, value)) },
spacingOpts{supportsFractions: true})
}
// inline-size / block-size
for _, pair := range [][2]string{
{"full", "100%"}, {"min", "min-content"}, {"max", "max-content"}, {"fit", "fit-content"},
} {
key, value := pair[0], pair[1]
c.staticUtility("inline-"+key, []staticDecl{sd("inline-size", value)})
c.staticUtility("block-"+key, []staticDecl{sd("block-size", value)})
c.staticUtility("min-inline-"+key, []staticDecl{sd("min-inline-size", value)})
c.staticUtility("min-block-"+key, []staticDecl{sd("min-block-size", value)})
c.staticUtility("max-inline-"+key, []staticDecl{sd("max-inline-size", value)})
c.staticUtility("max-block-"+key, []staticDecl{sd("max-block-size", value)})
}
for _, pair := range [][2]string{{"svw", "100svw"}, {"lvw", "100lvw"}, {"dvw", "100dvw"}} {
key, value := pair[0], pair[1]
c.staticUtility("inline-"+key, []staticDecl{sd("inline-size", value)})
c.staticUtility("min-inline-"+key, []staticDecl{sd("min-inline-size", value)})
c.staticUtility("max-inline-"+key, []staticDecl{sd("max-inline-size", value)})
}
for _, pair := range [][2]string{{"svh", "100svh"}, {"lvh", "100lvh"}, {"dvh", "100dvh"}} {
key, value := pair[0], pair[1]
c.staticUtility("block-"+key, []staticDecl{sd("block-size", value)})
c.staticUtility("min-block-"+key, []staticDecl{sd("min-block-size", value)})
c.staticUtility("max-block-"+key, []staticDecl{sd("max-block-size", value)})
}
c.staticUtility("inline-auto", []staticDecl{sd("inline-size", "auto")})
c.staticUtility("block-auto", []staticDecl{sd("block-size", "auto")})
c.staticUtility("min-inline-auto", []staticDecl{sd("min-inline-size", "auto")})
c.staticUtility("min-block-auto", []staticDecl{sd("min-block-size", "auto")})
c.staticUtility("block-lh", []staticDecl{sd("block-size", "1lh")})
c.staticUtility("min-block-lh", []staticDecl{sd("min-block-size", "1lh")})
c.staticUtility("max-block-lh", []staticDecl{sd("max-block-size", "1lh")})
c.staticUtility("inline-screen", []staticDecl{sd("inline-size", "100vw")})
c.staticUtility("min-inline-screen", []staticDecl{sd("min-inline-size", "100vw")})
c.staticUtility("max-inline-screen", []staticDecl{sd("max-inline-size", "100vw")})
c.staticUtility("block-screen", []staticDecl{sd("block-size", "100vh")})
c.staticUtility("min-block-screen", []staticDecl{sd("min-block-size", "100vh")})
c.staticUtility("max-block-screen", []staticDecl{sd("max-block-size", "100vh")})
c.staticUtility("max-inline-none", []staticDecl{sd("max-inline-size", "none")})
c.staticUtility("max-block-none", []staticDecl{sd("max-block-size", "none")})
for _, e := range []struct {
name string
namespaces []string
property string
}{
{"inline", []string{"--spacing", "--container"}, "inline-size"},
{"min-inline", []string{"--spacing", "--container"}, "min-inline-size"},
{"max-inline", []string{"--spacing", "--container"}, "max-inline-size"},
{"block", []string{"--spacing"}, "block-size"},
{"min-block", []string{"--spacing"}, "min-block-size"},
{"max-block", []string{"--spacing"}, "max-block-size"},
} {
prop := e.property
c.spacingUtility(e.name, e.namespaces,
func(value string) *utilResult { return uNodes(d(prop, value)) },
spacingOpts{supportsFractions: true})
}
c.utilities.static("container", func(_ *Candidate) *utilResult {
breakpoints := c.theme.namespace("--breakpoint").Values()
sort.SliceStable(breakpoints, func(i, j int) bool {
return compareBreakpoints(breakpoints[i], breakpoints[j], "asc") < 0
})
decls := []*AstNode{d("--tw-sort", "--tw-container-component"), d("width", "100%")}
for _, bp := range breakpoints {
decls = append(decls, atRule("@media", "(width >= "+bp+")", d("max-width", bp)))
}
return uList(decls)
})
c.staticUtility("flex-auto", []staticDecl{sd("flex", "auto")})
c.staticUtility("flex-initial", []staticDecl{sd("flex", "0 auto")})
c.staticUtility("flex-none", []staticDecl{sd("flex", "none")})
c.utilities.functional("flex", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("flex", candidate.Value.Value))
}
if candidate.Value.Fraction != "" {
fparts := segment(candidate.Value.Fraction, "/")
if len(fparts) != 2 || !isPositiveInteger(fparts[0]) || !isPositiveInteger(fparts[1]) {
return nil
}
return uNodes(d("flex", "calc("+candidate.Value.Fraction+" * 100%)"))
}
if isPositiveInteger(candidate.Value.Value) {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("flex", candidate.Value.Value))
}
return nil
}, nil)
c.functionalUtility("shrink", utilityDescription{
defaultValueSet: true,
defaultValue: sptr("1"),
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult { return uNodes(d("flex-shrink", value)) },
})
c.functionalUtility("grow", utilityDescription{
defaultValueSet: true,
defaultValue: sptr("1"),
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult { return uNodes(d("flex-grow", value)) },
})
c.staticUtility("basis-auto", []staticDecl{sd("flex-basis", "auto")})
c.staticUtility("basis-full", []staticDecl{sd("flex-basis", "100%")})
c.spacingUtility("basis", []string{"--flex-basis", "--spacing", "--container"},
func(value string) *utilResult { return uNodes(d("flex-basis", value)) },
spacingOpts{supportsFractions: true})
c.staticUtility("table-auto", []staticDecl{sd("table-layout", "auto")})
c.staticUtility("table-fixed", []staticDecl{sd("table-layout", "fixed")})
c.staticUtility("caption-top", []staticDecl{sd("caption-side", "top")})
c.staticUtility("caption-bottom", []staticDecl{sd("caption-side", "bottom")})
c.staticUtility("border-collapse", []staticDecl{sd("border-collapse", "collapse")})
c.staticUtility("border-separate", []staticDecl{sd("border-collapse", "separate")})
borderSpacingProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-border-spacing-x", "0", "<length>"),
property("--tw-border-spacing-y", "0", "<length>"),
})
}
c.spacingUtility("border-spacing", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
return uNodes(borderSpacingProperties(),
d("--tw-border-spacing-x", value), d("--tw-border-spacing-y", value),
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
}, spacingOpts{})
c.spacingUtility("border-spacing-x", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
return uNodes(borderSpacingProperties(),
d("--tw-border-spacing-x", value),
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
}, spacingOpts{})
c.spacingUtility("border-spacing-y", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
return uNodes(borderSpacingProperties(),
d("--tw-border-spacing-y", value),
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
}, spacingOpts{})
registerUtilities3(c)
}
func registerUtilities3(c *utilCtx) {
d := decl
originStatics := func(prop string) map[string][]*AstNode {
return map[string][]*AstNode{
"center": {d(prop, "center")},
"top": {d(prop, "top")},
"top-right": {d(prop, "100% 0")},
"right": {d(prop, "100%")},
"bottom-right": {d(prop, "100% 100%")},
"bottom": {d(prop, "bottom")},
"bottom-left": {d(prop, "0 100%")},
"left": {d(prop, "0")},
"top-left": {d(prop, "0 0")},
}
}
c.functionalUtility("origin", utilityDescription{
themeKeys: []string{"--transform-origin"},
handle: func(value, _ string) *utilResult { return uNodes(d("transform-origin", value)) },
staticValues: originStatics("transform-origin"),
})
c.functionalUtility("perspective-origin", utilityDescription{
themeKeys: []string{"--perspective-origin"},
handle: func(value, _ string) *utilResult { return uNodes(d("perspective-origin", value)) },
staticValues: originStatics("perspective-origin"),
})
c.functionalUtility("perspective", utilityDescription{
themeKeys: []string{"--perspective"},
handle: func(value, _ string) *utilResult { return uNodes(d("perspective", value)) },
staticValues: map[string][]*AstNode{"none": {d("perspective", "none")}},
})
translateProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-translate-x", "0", ""),
property("--tw-translate-y", "0", ""),
property("--tw-translate-z", "0", ""),
})
}
c.staticUtility("translate-none", []staticDecl{sd("translate", "none")})
c.staticUtility("-translate-full", []staticDecl{
sdFn(translateProperties),
sd("--tw-translate-x", "-100%"), sd("--tw-translate-y", "-100%"),
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
})
c.staticUtility("translate-full", []staticDecl{
sdFn(translateProperties),
sd("--tw-translate-x", "100%"), sd("--tw-translate-y", "100%"),
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
})
c.spacingUtility("translate", []string{"--translate", "--spacing"},
func(value string) *utilResult {
return uNodes(translateProperties(),
d("--tw-translate-x", value), d("--tw-translate-y", value),
d("translate", "var(--tw-translate-x) var(--tw-translate-y)"))
}, spacingOpts{supportsNegative: true, supportsFractions: true})
for _, axis := range []string{"x", "y"} {
ax := axis
c.staticUtility("-translate-"+ax+"-full", []staticDecl{
sdFn(translateProperties),
sd("--tw-translate-"+ax, "-100%"),
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
})
c.staticUtility("translate-"+ax+"-full", []staticDecl{
sdFn(translateProperties),
sd("--tw-translate-"+ax, "100%"),
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
})
c.spacingUtility("translate-"+ax, []string{"--translate", "--spacing"},
func(value string) *utilResult {
return uNodes(translateProperties(),
d("--tw-translate-"+ax, value),
d("translate", "var(--tw-translate-x) var(--tw-translate-y)"))
}, spacingOpts{supportsNegative: true, supportsFractions: true})
}
c.spacingUtility("translate-z", []string{"--translate", "--spacing"},
func(value string) *utilResult {
return uNodes(translateProperties(),
d("--tw-translate-z", value),
d("translate", "var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)"))
}, spacingOpts{supportsNegative: true})
c.staticUtility("translate-3d", []staticDecl{
sdFn(translateProperties),
sd("translate", "var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)"),
})
scaleProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-scale-x", "1", ""),
property("--tw-scale-y", "1", ""),
property("--tw-scale-z", "1", ""),
})
}
c.staticUtility("scale-none", []staticDecl{sd("scale", "none")})
handleScale := func(negative bool) func(*Candidate) *utilResult {
return func(candidate *Candidate) *utilResult {
if candidate.Value == nil || candidate.Modifier != nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
if negative {
value = "calc(" + value + " * -1)"
}
return uNodes(d("scale", value))
}
value, ok := c.theme.resolve(&candidate.Value.Value, []string{"--scale"}, themeNone)
if !ok && isPositiveInteger(candidate.Value.Value) {
value = candidate.Value.Value + "%"
ok = true
}
if !ok {
return nil
}
if negative {
value = "calc(" + value + " * -1)"
}
return uNodes(scaleProperties(),
d("--tw-scale-x", value), d("--tw-scale-y", value), d("--tw-scale-z", value),
d("scale", "var(--tw-scale-x) var(--tw-scale-y)"))
}
}
c.utilities.functional("-scale", handleScale(true), nil)
c.utilities.functional("scale", handleScale(false), nil)
for _, axis := range []string{"x", "y", "z"} {
ax := axis
zSuffix := ""
if ax == "z" {
zSuffix = " var(--tw-scale-z)"
}
c.functionalUtility("scale-"+ax, utilityDescription{
supportsNegative: true,
themeKeys: []string{"--scale"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "%", true
},
handle: func(value, _ string) *utilResult {
return uNodes(scaleProperties(),
d("--tw-scale-"+ax, value),
d("scale", "var(--tw-scale-x) var(--tw-scale-y)"+zSuffix))
},
})
}
c.staticUtility("scale-3d", []staticDecl{
sdFn(scaleProperties),
sd("scale", "var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)"),
})
registerUtilities4(c)
}
func registerUtilities4(c *utilCtx) {
d := decl
c.staticUtility("rotate-none", []staticDecl{sd("rotate", "none")})
handleRotate := func(negative bool) func(*Candidate) *utilResult {
return func(candidate *Candidate) *utilResult {
if candidate.Value == nil || candidate.Modifier != nil {
return nil
}
var value string
if candidate.Value.Kind == uvArbitrary {
value = candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"angle", "vector"})
}
if typ == "vector" {
return uNodes(d("rotate", value+" var(--tw-rotate)"))
} else if typ != "angle" {
if negative {
return uNodes(d("rotate", "calc("+value+" * -1)"))
}
return uNodes(d("rotate", value))
}
} else {
v, ok := c.theme.resolve(&candidate.Value.Value, []string{"--rotate"}, themeNone)
if !ok && isPositiveInteger(candidate.Value.Value) {
v = candidate.Value.Value + "deg"
ok = true
}
if !ok {
return nil
}
value = v
}
if negative {
return uNodes(d("rotate", "calc("+value+" * -1)"))
}
return uNodes(d("rotate", value))
}
}
c.utilities.functional("-rotate", handleRotate(true), nil)
c.utilities.functional("rotate", handleRotate(false), nil)
transformValue := strings.Join([]string{
"var(--tw-rotate-x,)", "var(--tw-rotate-y,)", "var(--tw-rotate-z,)",
"var(--tw-skew-x,)", "var(--tw-skew-y,)",
}, " ")
transformProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-rotate-x", "", ""), property("--tw-rotate-y", "", ""),
property("--tw-rotate-z", "", ""), property("--tw-skew-x", "", ""),
property("--tw-skew-y", "", ""),
})
}
for _, axis := range []string{"x", "y", "z"} {
ax := axis
up := strings.ToUpper(ax)
c.functionalUtility("rotate-"+ax, utilityDescription{
supportsNegative: true,
themeKeys: []string{"--rotate"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "deg", true
},
handle: func(value, _ string) *utilResult {
return uNodes(transformProperties(),
d("--tw-rotate-"+ax, "rotate"+up+"("+value+")"),
d("transform", transformValue))
},
})
}
skewBare := func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "deg", true
}
c.functionalUtility("skew", utilityDescription{
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
handle: func(value, _ string) *utilResult {
return uNodes(transformProperties(),
d("--tw-skew-x", "skewX("+value+")"), d("--tw-skew-y", "skewY("+value+")"),
d("transform", transformValue))
},
})
c.functionalUtility("skew-x", utilityDescription{
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
handle: func(value, _ string) *utilResult {
return uNodes(transformProperties(), d("--tw-skew-x", "skewX("+value+")"), d("transform", transformValue))
},
})
c.functionalUtility("skew-y", utilityDescription{
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
handle: func(value, _ string) *utilResult {
return uNodes(transformProperties(), d("--tw-skew-y", "skewY("+value+")"), d("transform", transformValue))
},
})
c.utilities.functional("transform", func(candidate *Candidate) *utilResult {
if candidate.Modifier != nil {
return nil
}
value := ""
set := false
if candidate.Value == nil {
value = transformValue
set = true
} else if candidate.Value.Kind == uvArbitrary {
value = candidate.Value.Value
set = true
}
if !set {
return nil
}
return uNodes(transformProperties(), d("transform", value))
}, nil)
c.staticUtility("transform-cpu", []staticDecl{sd("transform", transformValue)})
c.staticUtility("transform-gpu", []staticDecl{sd("transform", "translateZ(0) "+transformValue)})
c.staticUtility("transform-none", []staticDecl{sd("transform", "none")})
c.functionalUtility("zoom", utilityDescription{
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "%", true
},
handle: func(value, _ string) *utilResult { return uNodes(d("zoom", value)) },
})
c.staticUtility("transform-flat", []staticDecl{sd("transform-style", "flat")})
c.staticUtility("transform-3d", []staticDecl{sd("transform-style", "preserve-3d")})
c.staticUtility("transform-content", []staticDecl{sd("transform-box", "content-box")})
c.staticUtility("transform-border", []staticDecl{sd("transform-box", "border-box")})
c.staticUtility("transform-fill", []staticDecl{sd("transform-box", "fill-box")})
c.staticUtility("transform-stroke", []staticDecl{sd("transform-box", "stroke-box")})
c.staticUtility("transform-view", []staticDecl{sd("transform-box", "view-box")})
c.staticUtility("backface-visible", []staticDecl{sd("backface-visibility", "visible")})
c.staticUtility("backface-hidden", []staticDecl{sd("backface-visibility", "hidden")})
for _, value := range []string{
"auto", "default", "pointer", "wait", "text", "move", "help", "not-allowed", "none",
"context-menu", "progress", "cell", "crosshair", "vertical-text", "alias", "copy",
"no-drop", "grab", "grabbing", "all-scroll", "col-resize", "row-resize", "n-resize",
"e-resize", "s-resize", "w-resize", "ne-resize", "nw-resize", "se-resize", "sw-resize",
"ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "zoom-in", "zoom-out",
} {
c.staticUtility("cursor-"+value, []staticDecl{sd("cursor", value)})
}
c.functionalUtility("cursor", utilityDescription{
themeKeys: []string{"--cursor"},
handle: func(value, _ string) *utilResult { return uNodes(d("cursor", value)) },
})
for _, value := range []string{"auto", "none", "manipulation"} {
c.staticUtility("touch-"+value, []staticDecl{sd("touch-action", value)})
}
touchProperties := func() *AstNode {
return atRoot([]*AstNode{property("--tw-pan-x", "", ""), property("--tw-pan-y", "", ""), property("--tw-pinch-zoom", "", "")})
}
touchAction := "var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)"
for _, value := range []string{"x", "left", "right"} {
c.staticUtility("touch-pan-"+value, []staticDecl{sdFn(touchProperties), sd("--tw-pan-x", "pan-"+value), sd("touch-action", touchAction)})
}
for _, value := range []string{"y", "up", "down"} {
c.staticUtility("touch-pan-"+value, []staticDecl{sdFn(touchProperties), sd("--tw-pan-y", "pan-"+value), sd("touch-action", touchAction)})
}
c.staticUtility("touch-pinch-zoom", []staticDecl{sdFn(touchProperties), sd("--tw-pinch-zoom", "pinch-zoom"), sd("touch-action", touchAction)})
for _, value := range []string{"none", "text", "all", "auto"} {
c.staticUtility("select-"+value, []staticDecl{sd("-webkit-user-select", value), sd("user-select", value)})
}
c.staticUtility("resize-none", []staticDecl{sd("resize", "none")})
c.staticUtility("resize-x", []staticDecl{sd("resize", "horizontal")})
c.staticUtility("resize-y", []staticDecl{sd("resize", "vertical")})
c.staticUtility("resize", []staticDecl{sd("resize", "both")})
c.staticUtility("snap-none", []staticDecl{sd("scroll-snap-type", "none")})
snapProperties := func() *AstNode {
return atRoot([]*AstNode{property("--tw-scroll-snap-strictness", "proximity", "*")})
}
for _, value := range []string{"x", "y", "both"} {
c.staticUtility("snap-"+value, []staticDecl{sdFn(snapProperties), sd("scroll-snap-type", value+" var(--tw-scroll-snap-strictness)")})
}
c.staticUtility("snap-mandatory", []staticDecl{sdFn(snapProperties), sd("--tw-scroll-snap-strictness", "mandatory")})
c.staticUtility("snap-proximity", []staticDecl{sdFn(snapProperties), sd("--tw-scroll-snap-strictness", "proximity")})
c.staticUtility("snap-align-none", []staticDecl{sd("scroll-snap-align", "none")})
c.staticUtility("snap-start", []staticDecl{sd("scroll-snap-align", "start")})
c.staticUtility("snap-end", []staticDecl{sd("scroll-snap-align", "end")})
c.staticUtility("snap-center", []staticDecl{sd("scroll-snap-align", "center")})
c.staticUtility("snap-normal", []staticDecl{sd("scroll-snap-stop", "normal")})
c.staticUtility("snap-always", []staticDecl{sd("scroll-snap-stop", "always")})
for _, pair := range [][2]string{
{"scroll-m", "scroll-margin"}, {"scroll-mx", "scroll-margin-inline"}, {"scroll-my", "scroll-margin-block"},
{"scroll-ms", "scroll-margin-inline-start"}, {"scroll-me", "scroll-margin-inline-end"},
{"scroll-mbs", "scroll-margin-block-start"}, {"scroll-mbe", "scroll-margin-block-end"},
{"scroll-mt", "scroll-margin-top"}, {"scroll-mr", "scroll-margin-right"},
{"scroll-mb", "scroll-margin-bottom"}, {"scroll-ml", "scroll-margin-left"},
} {
prop := pair[1]
c.spacingUtility(pair[0], []string{"--scroll-margin", "--spacing"},
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{supportsNegative: true})
}
for _, pair := range [][2]string{
{"scroll-p", "scroll-padding"}, {"scroll-px", "scroll-padding-inline"}, {"scroll-py", "scroll-padding-block"},
{"scroll-ps", "scroll-padding-inline-start"}, {"scroll-pe", "scroll-padding-inline-end"},
{"scroll-pbs", "scroll-padding-block-start"}, {"scroll-pbe", "scroll-padding-block-end"},
{"scroll-pt", "scroll-padding-top"}, {"scroll-pr", "scroll-padding-right"},
{"scroll-pb", "scroll-padding-bottom"}, {"scroll-pl", "scroll-padding-left"},
} {
prop := pair[1]
c.spacingUtility(pair[0], []string{"--scroll-padding", "--spacing"},
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{})
}
c.staticUtility("list-inside", []staticDecl{sd("list-style-position", "inside")})
c.staticUtility("list-outside", []staticDecl{sd("list-style-position", "outside")})
c.functionalUtility("list", utilityDescription{
themeKeys: []string{"--list-style-type"},
handle: func(value, _ string) *utilResult { return uNodes(d("list-style-type", value)) },
staticValues: map[string][]*AstNode{
"none": {d("list-style-type", "none")}, "disc": {d("list-style-type", "disc")}, "decimal": {d("list-style-type", "decimal")},
},
})
c.functionalUtility("list-image", utilityDescription{
themeKeys: []string{"--list-style-image"},
handle: func(value, _ string) *utilResult { return uNodes(d("list-style-image", value)) },
staticValues: map[string][]*AstNode{"none": {d("list-style-image", "none")}},
})
c.staticUtility("appearance-none", []staticDecl{sd("appearance", "none")})
c.staticUtility("appearance-auto", []staticDecl{sd("appearance", "auto")})
c.staticUtility("scheme-normal", []staticDecl{sd("color-scheme", "normal")})
c.staticUtility("scheme-dark", []staticDecl{sd("color-scheme", "dark")})
c.staticUtility("scheme-light", []staticDecl{sd("color-scheme", "light")})
c.staticUtility("scheme-light-dark", []staticDecl{sd("color-scheme", "light dark")})
c.staticUtility("scheme-only-dark", []staticDecl{sd("color-scheme", "only dark")})
c.staticUtility("scheme-only-light", []staticDecl{sd("color-scheme", "only light")})
c.functionalUtility("columns", utilityDescription{
themeKeys: []string{"--columns", "--container"},
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult { return uNodes(d("columns", value)) },
staticValues: map[string][]*AstNode{"auto": {d("columns", "auto")}},
})
for _, value := range []string{"auto", "avoid", "all", "avoid-page", "page", "left", "right", "column"} {
c.staticUtility("break-before-"+value, []staticDecl{sd("break-before", value)})
}
for _, value := range []string{"auto", "avoid", "avoid-page", "avoid-column"} {
c.staticUtility("break-inside-"+value, []staticDecl{sd("break-inside", value)})
}
for _, value := range []string{"auto", "avoid", "all", "avoid-page", "page", "left", "right", "column"} {
c.staticUtility("break-after-"+value, []staticDecl{sd("break-after", value)})
}
c.staticUtility("grid-flow-row", []staticDecl{sd("grid-auto-flow", "row")})
c.staticUtility("grid-flow-col", []staticDecl{sd("grid-auto-flow", "column")})
c.staticUtility("grid-flow-dense", []staticDecl{sd("grid-auto-flow", "dense")})
c.staticUtility("grid-flow-row-dense", []staticDecl{sd("grid-auto-flow", "row dense")})
c.staticUtility("grid-flow-col-dense", []staticDecl{sd("grid-auto-flow", "column dense")})
autoTrackBare := func(v *UtilityValue) (string, bool) {
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
return "", false
}
if !isValidSpacingMultiplier(v.Value) {
return "", false
}
return "--spacing(" + v.Value + ")", true
}
c.functionalUtility("auto-cols", utilityDescription{
themeKeys: []string{"--grid-auto-columns"},
handleBareValue: autoTrackBare,
handle: func(value, _ string) *utilResult { return uNodes(d("grid-auto-columns", value)) },
staticValues: map[string][]*AstNode{
"auto": {d("grid-auto-columns", "auto")}, "min": {d("grid-auto-columns", "min-content")},
"max": {d("grid-auto-columns", "max-content")}, "fr": {d("grid-auto-columns", "minmax(0, 1fr)")},
},
})
c.functionalUtility("auto-rows", utilityDescription{
themeKeys: []string{"--grid-auto-rows"},
handleBareValue: autoTrackBare,
handle: func(value, _ string) *utilResult { return uNodes(d("grid-auto-rows", value)) },
staticValues: map[string][]*AstNode{
"auto": {d("grid-auto-rows", "auto")}, "min": {d("grid-auto-rows", "min-content")},
"max": {d("grid-auto-rows", "max-content")}, "fr": {d("grid-auto-rows", "minmax(0, 1fr)")},
},
})
registerUtilities5(c)
}
func registerUtilities5(c *utilCtx) {
d := decl
gridTemplateBare := func(v *UtilityValue) (string, bool) {
if !isStrictPositiveInteger(v.Value) {
return "", false
}
return "repeat(" + v.Value + ", minmax(0, 1fr))", true
}
c.functionalUtility("grid-cols", utilityDescription{
themeKeys: []string{"--grid-template-columns"},
handleBareValue: gridTemplateBare,
handle: func(value, _ string) *utilResult { return uNodes(d("grid-template-columns", value)) },
staticValues: map[string][]*AstNode{
"none": {d("grid-template-columns", "none")}, "subgrid": {d("grid-template-columns", "subgrid")},
},
})
c.functionalUtility("grid-rows", utilityDescription{
themeKeys: []string{"--grid-template-rows"},
handleBareValue: gridTemplateBare,
handle: func(value, _ string) *utilResult { return uNodes(d("grid-template-rows", value)) },
staticValues: map[string][]*AstNode{
"none": {d("grid-template-rows", "none")}, "subgrid": {d("grid-template-rows", "subgrid")},
},
})
c.staticUtility("flex-row", []staticDecl{sd("flex-direction", "row")})
c.staticUtility("flex-row-reverse", []staticDecl{sd("flex-direction", "row-reverse")})
c.staticUtility("flex-col", []staticDecl{sd("flex-direction", "column")})
c.staticUtility("flex-col-reverse", []staticDecl{sd("flex-direction", "column-reverse")})
c.staticUtility("flex-wrap", []staticDecl{sd("flex-wrap", "wrap")})
c.staticUtility("flex-nowrap", []staticDecl{sd("flex-wrap", "nowrap")})
c.staticUtility("flex-wrap-reverse", []staticDecl{sd("flex-wrap", "wrap-reverse")})
statics := func(prop string, pairs [][2]string) {
for _, p := range pairs {
c.staticUtility(p[0], []staticDecl{sd(prop, p[1])})
}
}
statics("place-content", [][2]string{
{"place-content-center", "center"}, {"place-content-start", "start"}, {"place-content-end", "end"},
{"place-content-center-safe", "safe center"}, {"place-content-end-safe", "safe end"},
{"place-content-between", "space-between"}, {"place-content-around", "space-around"},
{"place-content-evenly", "space-evenly"}, {"place-content-baseline", "baseline"}, {"place-content-stretch", "stretch"},
})
statics("place-items", [][2]string{
{"place-items-center", "center"}, {"place-items-start", "start"}, {"place-items-end", "end"},
{"place-items-center-safe", "safe center"}, {"place-items-end-safe", "safe end"},
{"place-items-baseline", "baseline"}, {"place-items-stretch", "stretch"},
})
statics("align-content", [][2]string{
{"content-normal", "normal"}, {"content-center", "center"}, {"content-start", "flex-start"},
{"content-end", "flex-end"}, {"content-center-safe", "safe center"}, {"content-end-safe", "safe flex-end"},
{"content-between", "space-between"}, {"content-around", "space-around"}, {"content-evenly", "space-evenly"},
{"content-baseline", "baseline"}, {"content-stretch", "stretch"},
})
statics("align-items", [][2]string{
{"items-center", "center"}, {"items-start", "flex-start"}, {"items-end", "flex-end"},
{"items-center-safe", "safe center"}, {"items-end-safe", "safe flex-end"},
{"items-baseline", "baseline"}, {"items-baseline-last", "last baseline"}, {"items-stretch", "stretch"},
})
statics("justify-content", [][2]string{
{"justify-normal", "normal"}, {"justify-center", "center"}, {"justify-start", "flex-start"},
{"justify-end", "flex-end"}, {"justify-center-safe", "safe center"}, {"justify-end-safe", "safe flex-end"},
{"justify-between", "space-between"}, {"justify-around", "space-around"}, {"justify-evenly", "space-evenly"},
{"justify-baseline", "baseline"}, {"justify-stretch", "stretch"},
})
statics("justify-items", [][2]string{
{"justify-items-normal", "normal"}, {"justify-items-center", "center"}, {"justify-items-start", "start"},
{"justify-items-end", "end"}, {"justify-items-center-safe", "safe center"}, {"justify-items-end-safe", "safe end"},
{"justify-items-stretch", "stretch"},
})
c.spacingUtility("gap", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("gap", value)) }, spacingOpts{})
c.spacingUtility("gap-x", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("column-gap", value)) }, spacingOpts{})
c.spacingUtility("gap-y", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("row-gap", value)) }, spacingOpts{})
spaceZero := func(value string) bool {
if value == "--spacing(0)" || value == "--spacing(-0)" {
return true
}
n, unit, ok := parseDimension(value)
if ok && n == 0 && (unit == "" || isLength(value)) {
return true
}
return false
}
c.spacingUtility("space-x", []string{"--space", "--spacing"}, func(value string) *utilResult {
zero := spaceZero(value)
ms, me := "calc("+value+" * var(--tw-space-x-reverse))", "calc("+value+" * calc(1 - var(--tw-space-x-reverse)))"
if zero {
ms, me = "0", "0"
}
return uNodes(
atRoot([]*AstNode{property("--tw-space-x-reverse", "0", "")}),
styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "row-gap"), d("--tw-space-x-reverse", "0"),
d("margin-inline-start", ms), d("margin-inline-end", me)),
)
}, spacingOpts{supportsNegative: true})
c.spacingUtility("space-y", []string{"--space", "--spacing"}, func(value string) *utilResult {
zero := spaceZero(value)
ms, me := "calc("+value+" * var(--tw-space-y-reverse))", "calc("+value+" * calc(1 - var(--tw-space-y-reverse)))"
if zero {
ms, me = "0", "0"
}
return uNodes(
atRoot([]*AstNode{property("--tw-space-y-reverse", "0", "")}),
styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "column-gap"), d("--tw-space-y-reverse", "0"),
d("margin-block-start", ms), d("margin-block-end", me)),
)
}, spacingOpts{supportsNegative: true})
c.staticUtility("space-x-reverse", []staticDecl{
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-space-x-reverse", "0", "")}) }),
sdFn(func() *AstNode {
return styleRule(":where(& > :not(:last-child))", d("--tw-sort", "row-gap"), d("--tw-space-x-reverse", "1"))
}),
})
c.staticUtility("space-y-reverse", []staticDecl{
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-space-y-reverse", "0", "")}) }),
sdFn(func() *AstNode {
return styleRule(":where(& > :not(:last-child))", d("--tw-sort", "column-gap"), d("--tw-space-y-reverse", "1"))
}),
})
c.staticUtility("accent-auto", []staticDecl{sd("accent-color", "auto")})
c.colorUtility("accent", colorUtilityDescription{
themeKeys: []string{"--accent-color", "--color"},
handle: func(value string) *utilResult { return uNodes(d("accent-color", value)) },
})
c.colorUtility("caret", colorUtilityDescription{
themeKeys: []string{"--caret-color", "--color"},
handle: func(value string) *utilResult { return uNodes(d("caret-color", value)) },
})
c.colorUtility("divide", colorUtilityDescription{
themeKeys: []string{"--divide-color", "--border-color", "--color"},
handle: func(value string) *utilResult {
return uNodes(styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "divide-color"), d("border-color", value)))
},
})
statics("place-self", [][2]string{
{"place-self-auto", "auto"}, {"place-self-start", "start"}, {"place-self-end", "end"},
{"place-self-center", "center"}, {"place-self-end-safe", "safe end"}, {"place-self-center-safe", "safe center"},
{"place-self-stretch", "stretch"},
})
statics("align-self", [][2]string{
{"self-auto", "auto"}, {"self-start", "flex-start"}, {"self-end", "flex-end"}, {"self-center", "center"},
{"self-end-safe", "safe flex-end"}, {"self-center-safe", "safe center"}, {"self-stretch", "stretch"},
{"self-baseline", "baseline"}, {"self-baseline-last", "last baseline"},
})
statics("justify-self", [][2]string{
{"justify-self-auto", "auto"}, {"justify-self-start", "flex-start"}, {"justify-self-end", "flex-end"},
{"justify-self-center", "center"}, {"justify-self-end-safe", "safe flex-end"}, {"justify-self-center-safe", "safe center"},
{"justify-self-stretch", "stretch"},
})
for _, value := range []string{"auto", "hidden", "clip", "visible", "scroll"} {
c.staticUtility("overflow-"+value, []staticDecl{sd("overflow", value)})
c.staticUtility("overflow-x-"+value, []staticDecl{sd("overflow-x", value)})
c.staticUtility("overflow-y-"+value, []staticDecl{sd("overflow-y", value)})
}
for _, value := range []string{"auto", "contain", "none"} {
c.staticUtility("overscroll-"+value, []staticDecl{sd("overscroll-behavior", value)})
c.staticUtility("overscroll-x-"+value, []staticDecl{sd("overscroll-behavior-x", value)})
c.staticUtility("overscroll-y-"+value, []staticDecl{sd("overscroll-behavior-y", value)})
}
c.staticUtility("scroll-auto", []staticDecl{sd("scroll-behavior", "auto")})
c.staticUtility("scroll-smooth", []staticDecl{sd("scroll-behavior", "smooth")})
c.staticUtility("scrollbar-auto", []staticDecl{sd("scrollbar-width", "auto")})
c.staticUtility("scrollbar-thin", []staticDecl{sd("scrollbar-width", "thin")})
c.staticUtility("scrollbar-none", []staticDecl{sd("scrollbar-width", "none")})
scrollbarColorProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-scrollbar-thumb", "#0000", "<color>"),
property("--tw-scrollbar-track", "#0000", "<color>"),
})
}
c.colorUtility("scrollbar-thumb", colorUtilityDescription{
themeKeys: []string{"--color"},
handle: func(value string) *utilResult {
return uNodes(scrollbarColorProperties(), d("--tw-scrollbar-thumb", value),
d("scrollbar-color", "var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)"))
},
})
c.colorUtility("scrollbar-track", colorUtilityDescription{
themeKeys: []string{"--color"},
handle: func(value string) *utilResult {
return uNodes(scrollbarColorProperties(), d("--tw-scrollbar-track", value),
d("scrollbar-color", "var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)"))
},
})
registerUtilities6(c)
}
func registerUtilities6(c *utilCtx) {
d := decl
c.staticUtility("scrollbar-gutter-auto", []staticDecl{sd("scrollbar-gutter", "auto")})
c.staticUtility("scrollbar-gutter-stable", []staticDecl{sd("scrollbar-gutter", "stable")})
c.staticUtility("scrollbar-gutter-both", []staticDecl{sd("scrollbar-gutter", "stable both-edges")})
c.staticUtility("truncate", []staticDecl{sd("overflow", "hidden"), sd("text-overflow", "ellipsis"), sd("white-space", "nowrap")})
c.staticUtility("text-ellipsis", []staticDecl{sd("text-overflow", "ellipsis")})
c.staticUtility("text-clip", []staticDecl{sd("text-overflow", "clip")})
c.staticUtility("hyphens-none", []staticDecl{sd("-webkit-hyphens", "none"), sd("hyphens", "none")})
c.staticUtility("hyphens-manual", []staticDecl{sd("-webkit-hyphens", "manual"), sd("hyphens", "manual")})
c.staticUtility("hyphens-auto", []staticDecl{sd("-webkit-hyphens", "auto"), sd("hyphens", "auto")})
c.staticUtility("whitespace-normal", []staticDecl{sd("white-space", "normal")})
c.staticUtility("whitespace-nowrap", []staticDecl{sd("white-space", "nowrap")})
c.staticUtility("whitespace-pre", []staticDecl{sd("white-space", "pre")})
c.staticUtility("whitespace-pre-line", []staticDecl{sd("white-space", "pre-line")})
c.staticUtility("whitespace-pre-wrap", []staticDecl{sd("white-space", "pre-wrap")})
c.staticUtility("whitespace-break-spaces", []staticDecl{sd("white-space", "break-spaces")})
c.functionalUtility("tab", utilityDescription{
handleBareValue: bareInteger,
handle: func(value, _ string) *utilResult { return uNodes(d("tab-size", value)) },
})
c.staticUtility("text-wrap", []staticDecl{sd("text-wrap", "wrap")})
c.staticUtility("text-nowrap", []staticDecl{sd("text-wrap", "nowrap")})
c.staticUtility("text-balance", []staticDecl{sd("text-wrap", "balance")})
c.staticUtility("text-pretty", []staticDecl{sd("text-wrap", "pretty")})
c.staticUtility("break-normal", []staticDecl{sd("overflow-wrap", "normal"), sd("word-break", "normal")})
c.staticUtility("break-all", []staticDecl{sd("word-break", "break-all")})
c.staticUtility("break-keep", []staticDecl{sd("word-break", "keep-all")})
c.staticUtility("wrap-anywhere", []staticDecl{sd("overflow-wrap", "anywhere")})
c.staticUtility("wrap-break-word", []staticDecl{sd("overflow-wrap", "break-word")})
c.staticUtility("wrap-normal", []staticDecl{sd("overflow-wrap", "normal")})
for _, e := range []struct {
root string
props []string
}{
{"rounded", []string{"border-radius"}},
{"rounded-s", []string{"border-start-start-radius", "border-end-start-radius"}},
{"rounded-e", []string{"border-start-end-radius", "border-end-end-radius"}},
{"rounded-t", []string{"border-top-left-radius", "border-top-right-radius"}},
{"rounded-r", []string{"border-top-right-radius", "border-bottom-right-radius"}},
{"rounded-b", []string{"border-bottom-right-radius", "border-bottom-left-radius"}},
{"rounded-l", []string{"border-top-left-radius", "border-bottom-left-radius"}},
{"rounded-ss", []string{"border-start-start-radius"}},
{"rounded-se", []string{"border-start-end-radius"}},
{"rounded-ee", []string{"border-end-end-radius"}},
{"rounded-es", []string{"border-end-start-radius"}},
{"rounded-tl", []string{"border-top-left-radius"}},
{"rounded-tr", []string{"border-top-right-radius"}},
{"rounded-br", []string{"border-bottom-right-radius"}},
{"rounded-bl", []string{"border-bottom-left-radius"}},
} {
props := e.props
mk := func(v string) []*AstNode {
nodes := make([]*AstNode, len(props))
for i, p := range props {
nodes[i] = d(p, v)
}
return nodes
}
c.functionalUtility(e.root, utilityDescription{
themeKeys: []string{"--radius"},
handle: func(value, _ string) *utilResult { return uList(mk(value)) },
staticValues: map[string][]*AstNode{
"none": mk("0"),
"full": mk("calc(infinity * 1px)"),
},
})
}
c.staticUtility("border-solid", []staticDecl{sd("--tw-border-style", "solid"), sd("border-style", "solid")})
c.staticUtility("border-dashed", []staticDecl{sd("--tw-border-style", "dashed"), sd("border-style", "dashed")})
c.staticUtility("border-dotted", []staticDecl{sd("--tw-border-style", "dotted"), sd("border-style", "dotted")})
c.staticUtility("border-double", []staticDecl{sd("--tw-border-style", "double"), sd("border-style", "double")})
c.staticUtility("border-hidden", []staticDecl{sd("--tw-border-style", "hidden"), sd("border-style", "hidden")})
c.staticUtility("border-none", []staticDecl{sd("--tw-border-style", "none"), sd("border-style", "none")})
registerUtilities7(c)
}
func registerUtilities7(c *utilCtx) {
d := decl
borderProperties := func() *AstNode {
return atRoot([]*AstNode{property("--tw-border-style", "solid", "")})
}
borderSideUtility := func(classRoot string, width, color func(string) []*AstNode) {
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
if candidate.Modifier != nil {
return nil
}
value, ok := c.theme.Get([]string{"--default-border-width"})
if !ok {
value = "1px"
}
decls := width(value)
if decls == nil {
return nil
}
return uList(append([]*AstNode{borderProperties()}, decls...))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "line-width", "length"})
}
switch typ {
case "line-width", "length":
if candidate.Modifier != nil {
return nil
}
decls := width(value)
if decls == nil {
return nil
}
return uList(append([]*AstNode{borderProperties()}, decls...))
default:
cv, ok := asColor(value, candidate.Modifier, c.theme)
if !ok {
return nil
}
return uList(color(cv))
}
}
if v, ok := resolveThemeColor(candidate, c.theme, []string{"--border-color", "--color"}); ok {
return uList(color(v))
}
if candidate.Modifier != nil {
return nil
}
if v, ok := c.theme.resolve(&candidate.Value.Value, []string{"--border-width"}, themeNone); ok {
decls := width(v)
if decls == nil {
return nil
}
return uList(append([]*AstNode{borderProperties()}, decls...))
}
if isPositiveInteger(candidate.Value.Value) {
decls := width(candidate.Value.Value + "px")
if decls == nil {
return nil
}
return uList(append([]*AstNode{borderProperties()}, decls...))
}
return nil
}, nil)
}
type bs struct {
root string
styleProp string
widthProp string
colorProps []string // properties to set for color
}
for _, e := range []bs{
{"border", "border-style", "border-width", []string{"border-color"}},
{"border-x", "border-inline-style", "border-inline-width", []string{"border-inline-color"}},
{"border-y", "border-block-style", "border-block-width", []string{"border-block-color"}},
{"border-s", "border-inline-start-style", "border-inline-start-width", []string{"border-inline-start-color"}},
{"border-e", "border-inline-end-style", "border-inline-end-width", []string{"border-inline-end-color"}},
{"border-bs", "border-block-start-style", "border-block-start-width", []string{"border-block-start-color"}},
{"border-be", "border-block-end-style", "border-block-end-width", []string{"border-block-end-color"}},
{"border-t", "border-top-style", "border-top-width", []string{"border-top-color"}},
{"border-r", "border-right-style", "border-right-width", []string{"border-right-color"}},
{"border-b", "border-bottom-style", "border-bottom-width", []string{"border-bottom-color"}},
{"border-l", "border-left-style", "border-left-width", []string{"border-left-color"}},
} {
e := e
width := func(value string) []*AstNode {
return []*AstNode{d(e.styleProp, "var(--tw-border-style)"), d(e.widthProp, value)}
}
color := func(value string) []*AstNode {
nodes := make([]*AstNode, len(e.colorProps))
for i, p := range e.colorProps {
nodes[i] = d(p, value)
}
return nodes
}
borderSideUtility(e.root, width, color)
}
defaultBorderWidth := func() *string {
v, ok := c.theme.Get([]string{"--default-border-width"})
if !ok {
v = "1px"
}
return &v
}
c.functionalUtility("divide-x", utilityDescription{
defaultValueSet: true,
defaultValue: defaultBorderWidth(),
themeKeys: []string{"--divide-width", "--border-width"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "px", true
},
handle: func(value, _ string) *utilResult {
return uNodes(
atRoot([]*AstNode{property("--tw-divide-x-reverse", "0", "")}),
styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "divide-x-width"), borderProperties(), d("--tw-divide-x-reverse", "0"),
d("border-inline-style", "var(--tw-border-style)"),
d("border-inline-start-width", "calc("+value+" * var(--tw-divide-x-reverse))"),
d("border-inline-end-width", "calc("+value+" * calc(1 - var(--tw-divide-x-reverse)))")),
)
},
})
c.functionalUtility("divide-y", utilityDescription{
defaultValueSet: true,
defaultValue: defaultBorderWidth(),
themeKeys: []string{"--divide-width", "--border-width"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "px", true
},
handle: func(value, _ string) *utilResult {
return uNodes(
atRoot([]*AstNode{property("--tw-divide-y-reverse", "0", "")}),
styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "divide-y-width"), borderProperties(), d("--tw-divide-y-reverse", "0"),
d("border-bottom-style", "var(--tw-border-style)"), d("border-top-style", "var(--tw-border-style)"),
d("border-top-width", "calc("+value+" * var(--tw-divide-y-reverse))"),
d("border-bottom-width", "calc("+value+" * calc(1 - var(--tw-divide-y-reverse)))")),
)
},
})
c.staticUtility("divide-x-reverse", []staticDecl{
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-divide-x-reverse", "0", "")}) }),
sdFn(func() *AstNode {
return styleRule(":where(& > :not(:last-child))", d("--tw-divide-x-reverse", "1"))
}),
})
c.staticUtility("divide-y-reverse", []staticDecl{
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-divide-y-reverse", "0", "")}) }),
sdFn(func() *AstNode {
return styleRule(":where(& > :not(:last-child))", d("--tw-divide-y-reverse", "1"))
}),
})
for _, value := range []string{"solid", "dashed", "dotted", "double", "none"} {
v := value
c.staticUtility("divide-"+v, []staticDecl{
sdFn(func() *AstNode {
return styleRule(":where(& > :not(:last-child))",
d("--tw-sort", "divide-style"), d("--tw-border-style", v), d("border-style", v))
}),
})
}
c.staticUtility("bg-auto", []staticDecl{sd("background-size", "auto")})
c.staticUtility("bg-cover", []staticDecl{sd("background-size", "cover")})
c.staticUtility("bg-contain", []staticDecl{sd("background-size", "contain")})
c.functionalUtility("bg-size", utilityDescription{
handle: func(value, _ string) *utilResult {
if value == "" {
return nil
}
return uNodes(d("background-size", value))
},
})
c.staticUtility("bg-fixed", []staticDecl{sd("background-attachment", "fixed")})
c.staticUtility("bg-local", []staticDecl{sd("background-attachment", "local")})
c.staticUtility("bg-scroll", []staticDecl{sd("background-attachment", "scroll")})
c.staticUtility("bg-top", []staticDecl{sd("background-position", "top")})
c.staticUtility("bg-top-left", []staticDecl{sd("background-position", "left top")})
c.staticUtility("bg-top-right", []staticDecl{sd("background-position", "right top")})
c.staticUtility("bg-bottom", []staticDecl{sd("background-position", "bottom")})
c.staticUtility("bg-bottom-left", []staticDecl{sd("background-position", "left bottom")})
c.staticUtility("bg-bottom-right", []staticDecl{sd("background-position", "right bottom")})
c.staticUtility("bg-left", []staticDecl{sd("background-position", "left")})
c.staticUtility("bg-right", []staticDecl{sd("background-position", "right")})
c.staticUtility("bg-center", []staticDecl{sd("background-position", "center")})
c.functionalUtility("bg-position", utilityDescription{
handle: func(value, _ string) *utilResult {
if value == "" {
return nil
}
return uNodes(d("background-position", value))
},
})
c.staticUtility("bg-repeat", []staticDecl{sd("background-repeat", "repeat")})
c.staticUtility("bg-no-repeat", []staticDecl{sd("background-repeat", "no-repeat")})
c.staticUtility("bg-repeat-x", []staticDecl{sd("background-repeat", "repeat-x")})
c.staticUtility("bg-repeat-y", []staticDecl{sd("background-repeat", "repeat-y")})
c.staticUtility("bg-repeat-round", []staticDecl{sd("background-repeat", "round")})
c.staticUtility("bg-repeat-space", []staticDecl{sd("background-repeat", "space")})
c.staticUtility("bg-none", []staticDecl{sd("background-image", "none")})
registerUtilities8(c)
}
func registerUtilities8(c *utilCtx) {
d := decl
theme := c.theme
linearGradientDirections := map[string]string{
"to-t": "to top", "to-tr": "to top right", "to-r": "to right", "to-br": "to bottom right",
"to-b": "to bottom", "to-bl": "to bottom left", "to-l": "to left", "to-tl": "to top left",
}
resolveInterpolationModifier := func(modifier *CandidateModifier) string {
method := "in oklab"
if modifier != nil {
if modifier.Kind == modNamed {
switch modifier.Value {
case "longer", "shorter", "increasing", "decreasing":
method = "in oklch " + modifier.Value + " hue"
default:
method = "in " + modifier.Value
}
} else {
method = modifier.Value
}
}
return method
}
handleBgLinear := func(negative bool) func(*Candidate) *utilResult {
return func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
if candidate.Modifier != nil {
return nil
}
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"angle"})
}
if typ == "angle" {
if negative {
value = "calc(" + value + " * -1)"
}
return uNodes(d("--tw-gradient-position", value),
d("background-image", "linear-gradient(var(--tw-gradient-stops,"+value+"))"))
}
if negative {
return nil
}
return uNodes(d("--tw-gradient-position", value),
d("background-image", "linear-gradient(var(--tw-gradient-stops,"+value+"))"))
}
value := candidate.Value.Value
if !negative {
if dir, ok := linearGradientDirections[value]; ok {
value = dir
} else if isPositiveInteger(value) {
value = value + "deg"
} else {
return nil
}
} else if isPositiveInteger(value) {
value = "calc(" + value + "deg * -1)"
} else {
return nil
}
interp := resolveInterpolationModifier(candidate.Modifier)
return uNodes(
d("--tw-gradient-position", value),
rule("@supports (background-image: linear-gradient(in lab, red, red))",
d("--tw-gradient-position", value+" "+interp)),
d("background-image", "linear-gradient(var(--tw-gradient-stops))"),
)
}
}
c.utilities.functional("-bg-linear", handleBgLinear(true), nil)
c.utilities.functional("bg-linear", handleBgLinear(false), nil)
handleBgConic := func(negative bool) func(*Candidate) *utilResult {
return func(candidate *Candidate) *utilResult {
if candidate.Value != nil && candidate.Value.Kind == uvArbitrary {
if candidate.Modifier != nil {
return nil
}
value := candidate.Value.Value
return uNodes(d("--tw-gradient-position", value),
d("background-image", "conic-gradient(var(--tw-gradient-stops,"+value+"))"))
}
interp := resolveInterpolationModifier(candidate.Modifier)
if candidate.Value == nil {
return uNodes(d("--tw-gradient-position", interp),
d("background-image", "conic-gradient(var(--tw-gradient-stops))"))
}
value := candidate.Value.Value
if !isPositiveInteger(value) {
return nil
}
if negative {
value = "calc(" + value + "deg * -1)"
} else {
value = value + "deg"
}
return uNodes(d("--tw-gradient-position", "from "+value+" "+interp),
d("background-image", "conic-gradient(var(--tw-gradient-stops))"))
}
}
c.utilities.functional("-bg-conic", handleBgConic(true), nil)
c.utilities.functional("bg-conic", handleBgConic(false), nil)
c.utilities.functional("bg-radial", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
interp := resolveInterpolationModifier(candidate.Modifier)
return uNodes(d("--tw-gradient-position", interp),
d("background-image", "radial-gradient(var(--tw-gradient-stops))"))
}
if candidate.Value.Kind == uvArbitrary {
if candidate.Modifier != nil {
return nil
}
value := candidate.Value.Value
return uNodes(d("--tw-gradient-position", value),
d("background-image", "radial-gradient(var(--tw-gradient-stops,"+value+"))"))
}
return nil
}, nil)
c.utilities.functional("bg", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"image", "color", "percentage", "position", "bg-size", "length", "url"})
}
switch typ {
case "percentage", "position":
if candidate.Modifier != nil {
return nil
}
return uNodes(d("background-position", value))
case "bg-size", "length", "size":
if candidate.Modifier != nil {
return nil
}
return uNodes(d("background-size", value))
case "image", "url":
if candidate.Modifier != nil {
return nil
}
return uNodes(d("background-image", value))
default:
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("background-color", cv))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
return uNodes(d("background-color", v))
}
if candidate.Modifier != nil {
return nil
}
if v, ok := theme.resolve(&candidate.Value.Value, []string{"--background-image"}, themeNone); ok {
return uNodes(d("background-image", v))
}
return nil
}, nil)
gradientStopProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-gradient-position", "", ""),
property("--tw-gradient-from", "#0000", "<color>"),
property("--tw-gradient-via", "#0000", "<color>"),
property("--tw-gradient-to", "#0000", "<color>"),
property("--tw-gradient-stops", "", ""),
property("--tw-gradient-via-stops", "", ""),
property("--tw-gradient-from-position", "0%", "<length-percentage>"),
property("--tw-gradient-via-position", "50%", "<length-percentage>"),
property("--tw-gradient-to-position", "100%", "<length-percentage>"),
})
}
gradientStopUtility := func(classRoot string, color, position func(string) []*AstNode) {
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length", "percentage"})
}
switch typ {
case "length", "percentage":
if candidate.Modifier != nil {
return nil
}
return uList(position(value))
default:
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uList(color(cv))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
return uList(color(v))
}
if candidate.Modifier != nil {
return nil
}
if v, ok := theme.resolve(&candidate.Value.Value, []string{"--gradient-color-stop-positions"}, themeNone); ok {
return uList(position(v))
} else if strings.HasSuffix(candidate.Value.Value, "%") && isPositiveInteger(candidate.Value.Value[:len(candidate.Value.Value)-1]) {
return uList(position(candidate.Value.Value))
}
return nil
}, nil)
}
gradientStopUtility("from",
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-from"), d("--tw-gradient-from", value),
d("--tw-gradient-stops", "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))")}
},
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-gradient-from-position", value)}
})
c.staticUtility("via-none", []staticDecl{sd("--tw-gradient-via-stops", "initial")})
gradientStopUtility("via",
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-via"), d("--tw-gradient-via", value),
d("--tw-gradient-via-stops", "var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)"),
d("--tw-gradient-stops", "var(--tw-gradient-via-stops)")}
},
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-gradient-via-position", value)}
})
gradientStopUtility("to",
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-to"), d("--tw-gradient-to", value),
d("--tw-gradient-stops", "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))")}
},
func(value string) []*AstNode {
return []*AstNode{gradientStopProperties(), d("--tw-gradient-to-position", value)}
})
registerUtilities9(c)
}
func registerUtilities9(c *utilCtx) {
d := decl
c.staticUtility("mask-none", []staticDecl{sd("mask-image", "none")})
c.utilities.functional("mask", func(candidate *Candidate) *utilResult {
if candidate.Value == nil || candidate.Modifier != nil {
return nil
}
if candidate.Value.Kind != uvArbitrary {
return nil
}
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"image", "percentage", "position", "bg-size", "length", "url"})
}
switch typ {
case "percentage", "position":
return uNodes(d("mask-position", value))
case "bg-size", "length", "size":
return uNodes(d("mask-size", value))
default:
return uNodes(d("mask-image", value))
}
}, nil)
c.staticUtility("mask-add", []staticDecl{sd("mask-composite", "add")})
c.staticUtility("mask-subtract", []staticDecl{sd("mask-composite", "subtract")})
c.staticUtility("mask-intersect", []staticDecl{sd("mask-composite", "intersect")})
c.staticUtility("mask-exclude", []staticDecl{sd("mask-composite", "exclude")})
c.staticUtility("mask-alpha", []staticDecl{sd("mask-mode", "alpha")})
c.staticUtility("mask-luminance", []staticDecl{sd("mask-mode", "luminance")})
c.staticUtility("mask-match", []staticDecl{sd("mask-mode", "match-source")})
c.staticUtility("mask-type-alpha", []staticDecl{sd("mask-type", "alpha")})
c.staticUtility("mask-type-luminance", []staticDecl{sd("mask-type", "luminance")})
c.staticUtility("mask-auto", []staticDecl{sd("mask-size", "auto")})
c.staticUtility("mask-cover", []staticDecl{sd("mask-size", "cover")})
c.staticUtility("mask-contain", []staticDecl{sd("mask-size", "contain")})
c.functionalUtility("mask-size", utilityDescription{
handle: func(value, _ string) *utilResult {
if value == "" {
return nil
}
return uNodes(d("mask-size", value))
},
})
c.staticUtility("mask-top", []staticDecl{sd("mask-position", "top")})
c.staticUtility("mask-top-left", []staticDecl{sd("mask-position", "left top")})
c.staticUtility("mask-top-right", []staticDecl{sd("mask-position", "right top")})
c.staticUtility("mask-bottom", []staticDecl{sd("mask-position", "bottom")})
c.staticUtility("mask-bottom-left", []staticDecl{sd("mask-position", "left bottom")})
c.staticUtility("mask-bottom-right", []staticDecl{sd("mask-position", "right bottom")})
c.staticUtility("mask-left", []staticDecl{sd("mask-position", "left")})
c.staticUtility("mask-right", []staticDecl{sd("mask-position", "right")})
c.staticUtility("mask-center", []staticDecl{sd("mask-position", "center")})
c.functionalUtility("mask-position", utilityDescription{
handle: func(value, _ string) *utilResult {
if value == "" {
return nil
}
return uNodes(d("mask-position", value))
},
})
c.staticUtility("mask-repeat", []staticDecl{sd("mask-repeat", "repeat")})
c.staticUtility("mask-no-repeat", []staticDecl{sd("mask-repeat", "no-repeat")})
c.staticUtility("mask-repeat-x", []staticDecl{sd("mask-repeat", "repeat-x")})
c.staticUtility("mask-repeat-y", []staticDecl{sd("mask-repeat", "repeat-y")})
c.staticUtility("mask-repeat-round", []staticDecl{sd("mask-repeat", "round")})
c.staticUtility("mask-repeat-space", []staticDecl{sd("mask-repeat", "space")})
c.staticUtility("mask-clip-border", []staticDecl{sd("mask-clip", "border-box")})
c.staticUtility("mask-clip-padding", []staticDecl{sd("mask-clip", "padding-box")})
c.staticUtility("mask-clip-content", []staticDecl{sd("mask-clip", "content-box")})
c.staticUtility("mask-clip-fill", []staticDecl{sd("mask-clip", "fill-box")})
c.staticUtility("mask-clip-stroke", []staticDecl{sd("mask-clip", "stroke-box")})
c.staticUtility("mask-clip-view", []staticDecl{sd("mask-clip", "view-box")})
c.staticUtility("mask-no-clip", []staticDecl{sd("mask-clip", "no-clip")})
c.staticUtility("mask-origin-border", []staticDecl{sd("mask-origin", "border-box")})
c.staticUtility("mask-origin-padding", []staticDecl{sd("mask-origin", "padding-box")})
c.staticUtility("mask-origin-content", []staticDecl{sd("mask-origin", "content-box")})
c.staticUtility("mask-origin-fill", []staticDecl{sd("mask-origin", "fill-box")})
c.staticUtility("mask-origin-stroke", []staticDecl{sd("mask-origin", "stroke-box")})
c.staticUtility("mask-origin-view", []staticDecl{sd("mask-origin", "view-box")})
registerUtilities10(c)
}
// registerUtilities10: mask-image gradients (edge / linear / radial / conic).
func registerUtilities10(c *utilCtx) {
d := decl
theme := c.theme
maskImage := func() *AstNode {
return d("mask-image", "var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic)")
}
maskComposite := func() *AstNode { return d("mask-composite", "intersect") }
maskPropertiesGradient := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-mask-linear", "linear-gradient(#fff, #fff)", ""),
property("--tw-mask-radial", "linear-gradient(#fff, #fff)", ""),
property("--tw-mask-conic", "linear-gradient(#fff, #fff)", ""),
})
}
maskStopUtility := func(classRoot string, colorFn, positionFn func(string) []*AstNode) {
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"length", "percentage", "color"})
}
switch typ {
case "color":
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uList(colorFn(cv))
case "percentage":
if candidate.Modifier != nil {
return nil
}
if len(value) == 0 || !isPositiveInteger(value[:len(value)-1]) {
return nil
}
return uList(positionFn(value))
default:
if candidate.Modifier != nil {
return nil
}
return uList(positionFn(value))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
return uList(colorFn(v))
}
if candidate.Modifier != nil {
return nil
}
typ := inferDataType(candidate.Value.Value, []string{"number", "percentage"})
switch typ {
case "number":
if _, ok := theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
return nil
}
if !isValidSpacingMultiplier(candidate.Value.Value) {
return nil
}
return uList(positionFn("--spacing(" + candidate.Value.Value + ")"))
case "percentage":
v := candidate.Value.Value
if len(v) == 0 || !isPositiveInteger(v[:len(v)-1]) {
return nil
}
return uList(positionFn(v))
}
return nil
}, nil)
}
// --- Edge masks ---
maskPropertiesEdge := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-mask-left", "linear-gradient(#fff, #fff)", ""),
property("--tw-mask-right", "linear-gradient(#fff, #fff)", ""),
property("--tw-mask-bottom", "linear-gradient(#fff, #fff)", ""),
property("--tw-mask-top", "linear-gradient(#fff, #fff)", ""),
})
}
maskEdgeUtility := func(name, stop string, top, right, bottom, left bool) {
edges := []struct {
name string
on bool
}{{"top", top}, {"right", right}, {"bottom", bottom}, {"left", left}}
build := func(value, kind string) []*AstNode {
nodes := []*AstNode{
maskPropertiesGradient(), maskPropertiesEdge(), maskImage(), maskComposite(),
d("--tw-mask-linear", "var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top)"),
}
for _, e := range edges {
if !e.on {
continue
}
nodes = append(nodes,
d("--tw-mask-"+e.name, "linear-gradient(to "+e.name+", var(--tw-mask-"+e.name+"-from-color) var(--tw-mask-"+e.name+"-from-position), var(--tw-mask-"+e.name+"-to-color) var(--tw-mask-"+e.name+"-to-position))"),
atRoot([]*AstNode{
property("--tw-mask-"+e.name+"-from-position", "0%", ""),
property("--tw-mask-"+e.name+"-to-position", "100%", ""),
property("--tw-mask-"+e.name+"-from-color", "black", ""),
property("--tw-mask-"+e.name+"-to-color", "transparent", ""),
}),
d("--tw-mask-"+e.name+"-"+stop+"-"+kind, value),
)
}
return nodes
}
maskStopUtility(name,
func(value string) []*AstNode { return build(value, "color") },
func(value string) []*AstNode { return build(value, "position") })
}
maskEdgeUtility("mask-x-from", "from", false, true, false, true)
maskEdgeUtility("mask-x-to", "to", false, true, false, true)
maskEdgeUtility("mask-y-from", "from", true, false, true, false)
maskEdgeUtility("mask-y-to", "to", true, false, true, false)
maskEdgeUtility("mask-t-from", "from", true, false, false, false)
maskEdgeUtility("mask-t-to", "to", true, false, false, false)
maskEdgeUtility("mask-r-from", "from", false, true, false, false)
maskEdgeUtility("mask-r-to", "to", false, true, false, false)
maskEdgeUtility("mask-b-from", "from", false, false, true, false)
maskEdgeUtility("mask-b-to", "to", false, false, true, false)
maskEdgeUtility("mask-l-from", "from", false, false, false, true)
maskEdgeUtility("mask-l-to", "to", false, false, false, true)
// --- Linear masks ---
maskPropertiesLinear := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-mask-linear-position", "0deg", ""),
property("--tw-mask-linear-from-position", "0%", ""),
property("--tw-mask-linear-to-position", "100%", ""),
property("--tw-mask-linear-from-color", "black", ""),
property("--tw-mask-linear-to-color", "transparent", ""),
})
}
degBare := func(neg bool) func(*UtilityValue) (string, bool) {
return func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
if v.Value == "0" {
return "0deg", true
}
if v.Value == "1" {
if neg {
return "-1deg", true
}
return "1deg", true
}
if neg {
return "calc(1deg * -" + v.Value + ")", true
}
return "calc(1deg * " + v.Value + ")", true
}
}
c.functionalUtility("mask-linear", utilityDescription{
defaultValueSet: true,
defaultValue: nil,
supportsNegative: true,
handleBareValue: degBare(false),
handleNegativeBareValue: degBare(true),
handle: func(value, _ string) *utilResult {
return uNodes(maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(),
d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)))"),
d("--tw-mask-linear-position", value))
},
})
linearStops := "var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position)"
maskStopUtility("mask-linear-from",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-from-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-from-position", v)}
})
maskStopUtility("mask-linear-to",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-to-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-to-position", v)}
})
// --- Radial masks ---
maskPropertiesRadial := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-mask-radial-from-position", "0%", ""),
property("--tw-mask-radial-to-position", "100%", ""),
property("--tw-mask-radial-from-color", "black", ""),
property("--tw-mask-radial-to-color", "transparent", ""),
property("--tw-mask-radial-shape", "ellipse", ""),
property("--tw-mask-radial-size", "farthest-corner", ""),
property("--tw-mask-radial-position", "center", ""),
})
}
c.staticUtility("mask-circle", []staticDecl{sd("--tw-mask-radial-shape", "circle")})
c.staticUtility("mask-ellipse", []staticDecl{sd("--tw-mask-radial-shape", "ellipse")})
c.staticUtility("mask-radial-closest-side", []staticDecl{sd("--tw-mask-radial-size", "closest-side")})
c.staticUtility("mask-radial-farthest-side", []staticDecl{sd("--tw-mask-radial-size", "farthest-side")})
c.staticUtility("mask-radial-closest-corner", []staticDecl{sd("--tw-mask-radial-size", "closest-corner")})
c.staticUtility("mask-radial-farthest-corner", []staticDecl{sd("--tw-mask-radial-size", "farthest-corner")})
for _, p := range [][2]string{
{"mask-radial-at-top", "top"}, {"mask-radial-at-top-left", "top left"}, {"mask-radial-at-top-right", "top right"},
{"mask-radial-at-bottom", "bottom"}, {"mask-radial-at-bottom-left", "bottom left"}, {"mask-radial-at-bottom-right", "bottom right"},
{"mask-radial-at-left", "left"}, {"mask-radial-at-right", "right"}, {"mask-radial-at-center", "center"},
} {
c.staticUtility(p[0], []staticDecl{sd("--tw-mask-radial-position", p[1])})
}
c.functionalUtility("mask-radial-at", utilityDescription{
defaultValueSet: true, defaultValue: nil,
handle: func(value, _ string) *utilResult { return uNodes(d("--tw-mask-radial-position", value)) },
})
c.functionalUtility("mask-radial", utilityDescription{
defaultValueSet: true, defaultValue: nil,
handle: func(value, _ string) *utilResult {
return uNodes(maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(),
d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops, var(--tw-mask-radial-size)))"),
d("--tw-mask-radial-size", value))
},
})
radialStops := "var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position)"
maskStopUtility("mask-radial-from",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-from-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-from-position", v)}
})
maskStopUtility("mask-radial-to",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-to-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-to-position", v)}
})
// --- Conic masks ---
maskPropertiesConic := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-mask-conic-position", "0deg", ""),
property("--tw-mask-conic-from-position", "0%", ""),
property("--tw-mask-conic-to-position", "100%", ""),
property("--tw-mask-conic-from-color", "black", ""),
property("--tw-mask-conic-to-color", "transparent", ""),
})
}
c.functionalUtility("mask-conic", utilityDescription{
defaultValueSet: true,
defaultValue: nil,
supportsNegative: true,
handleBareValue: degBare(false),
handleNegativeBareValue: degBare(true),
handle: func(value, _ string) *utilResult {
return uNodes(maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(),
d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)))"),
d("--tw-mask-conic-position", value))
},
})
conicStops := "from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position)"
maskStopUtility("mask-conic-from",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-from-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-from-position", v)}
})
maskStopUtility("mask-conic-to",
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-to-color", v)}
},
func(v string) []*AstNode {
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-to-position", v)}
})
registerUtilities11(c)
}
// registerUtilities11: box-decoration, bg-clip/origin, blend modes, fill,
// stroke, object, padding, text-align, indent, vertical-align, font,
// text-transform/style/decoration-line, font-stretch, placeholder, decoration.
func registerUtilities11(c *utilCtx) {
d := decl
theme := c.theme
c.staticUtility("box-decoration-slice", []staticDecl{sd("-webkit-box-decoration-break", "slice"), sd("box-decoration-break", "slice")})
c.staticUtility("box-decoration-clone", []staticDecl{sd("-webkit-box-decoration-break", "clone"), sd("box-decoration-break", "clone")})
c.staticUtility("bg-clip-text", []staticDecl{sd("background-clip", "text")})
c.staticUtility("bg-clip-border", []staticDecl{sd("background-clip", "border-box")})
c.staticUtility("bg-clip-padding", []staticDecl{sd("background-clip", "padding-box")})
c.staticUtility("bg-clip-content", []staticDecl{sd("background-clip", "content-box")})
c.staticUtility("bg-origin-border", []staticDecl{sd("background-origin", "border-box")})
c.staticUtility("bg-origin-padding", []staticDecl{sd("background-origin", "padding-box")})
c.staticUtility("bg-origin-content", []staticDecl{sd("background-origin", "content-box")})
for _, value := range []string{
"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge",
"color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue",
"saturation", "color", "luminosity",
} {
c.staticUtility("bg-blend-"+value, []staticDecl{sd("background-blend-mode", value)})
c.staticUtility("mix-blend-"+value, []staticDecl{sd("mix-blend-mode", value)})
}
c.staticUtility("mix-blend-plus-darker", []staticDecl{sd("mix-blend-mode", "plus-darker")})
c.staticUtility("mix-blend-plus-lighter", []staticDecl{sd("mix-blend-mode", "plus-lighter")})
c.staticUtility("fill-none", []staticDecl{sd("fill", "none")})
c.utilities.functional("fill", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
v, ok := asColor(candidate.Value.Value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("fill", v))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--fill", "--color"}); ok {
return uNodes(d("fill", v))
}
return nil
}, nil)
c.staticUtility("stroke-none", []staticDecl{sd("stroke", "none")})
c.utilities.functional("stroke", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "number", "length", "percentage"})
}
switch typ {
case "number", "length", "percentage":
if candidate.Modifier != nil {
return nil
}
return uNodes(d("stroke-width", value))
default:
v, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("stroke", v))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--stroke", "--color"}); ok {
return uNodes(d("stroke", v))
}
vv := candidate.Value.Value
if v, ok := theme.resolve(&vv, []string{"--stroke-width"}, themeNone); ok {
return uNodes(d("stroke-width", v))
} else if isPositiveInteger(vv) {
return uNodes(d("stroke-width", vv))
}
return nil
}, nil)
c.staticUtility("object-contain", []staticDecl{sd("object-fit", "contain")})
c.staticUtility("object-cover", []staticDecl{sd("object-fit", "cover")})
c.staticUtility("object-fill", []staticDecl{sd("object-fit", "fill")})
c.staticUtility("object-none", []staticDecl{sd("object-fit", "none")})
c.staticUtility("object-scale-down", []staticDecl{sd("object-fit", "scale-down")})
c.functionalUtility("object", utilityDescription{
themeKeys: []string{"--object-position"},
handle: func(value, _ string) *utilResult { return uNodes(d("object-position", value)) },
staticValues: map[string][]*AstNode{
"top": {d("object-position", "top")}, "top-left": {d("object-position", "left top")},
"top-right": {d("object-position", "right top")}, "bottom": {d("object-position", "bottom")},
"bottom-left": {d("object-position", "left bottom")}, "bottom-right": {d("object-position", "right bottom")},
"left": {d("object-position", "left")}, "right": {d("object-position", "right")}, "center": {d("object-position", "center")},
},
})
for _, pair := range [][2]string{
{"p", "padding"}, {"px", "padding-inline"}, {"py", "padding-block"},
{"ps", "padding-inline-start"}, {"pe", "padding-inline-end"},
{"pbs", "padding-block-start"}, {"pbe", "padding-block-end"},
{"pt", "padding-top"}, {"pr", "padding-right"}, {"pb", "padding-bottom"}, {"pl", "padding-left"},
} {
prop := pair[1]
c.spacingUtility(pair[0], []string{"--padding", "--spacing"},
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{})
}
c.staticUtility("text-left", []staticDecl{sd("text-align", "left")})
c.staticUtility("text-center", []staticDecl{sd("text-align", "center")})
c.staticUtility("text-right", []staticDecl{sd("text-align", "right")})
c.staticUtility("text-justify", []staticDecl{sd("text-align", "justify")})
c.staticUtility("text-start", []staticDecl{sd("text-align", "start")})
c.staticUtility("text-end", []staticDecl{sd("text-align", "end")})
c.spacingUtility("indent", []string{"--text-indent", "--spacing"},
func(value string) *utilResult { return uNodes(d("text-indent", value)) }, spacingOpts{supportsNegative: true})
c.staticUtility("align-baseline", []staticDecl{sd("vertical-align", "baseline")})
c.staticUtility("align-top", []staticDecl{sd("vertical-align", "top")})
c.staticUtility("align-middle", []staticDecl{sd("vertical-align", "middle")})
c.staticUtility("align-bottom", []staticDecl{sd("vertical-align", "bottom")})
c.staticUtility("align-text-top", []staticDecl{sd("vertical-align", "text-top")})
c.staticUtility("align-text-bottom", []staticDecl{sd("vertical-align", "text-bottom")})
c.staticUtility("align-sub", []staticDecl{sd("vertical-align", "sub")})
c.staticUtility("align-super", []staticDecl{sd("vertical-align", "super")})
c.functionalUtility("align", utilityDescription{
handle: func(value, _ string) *utilResult { return uNodes(d("vertical-align", value)) },
})
c.utilities.functional("font", func(candidate *Candidate) *utilResult {
if candidate.Value == nil || candidate.Modifier != nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"number", "generic-name", "family-name"})
}
switch typ {
case "generic-name", "family-name":
return uNodes(d("font-family", value))
default:
return uNodes(atRoot([]*AstNode{property("--tw-font-weight", "", "")}),
d("--tw-font-weight", value), d("font-weight", value))
}
}
if families, extra, ok := theme.resolveWith(candidate.Value.Value, []string{"--font"},
[]string{"--font-feature-settings", "--font-variation-settings"}); ok {
nodes := []*AstNode{d("font-family", families)}
if v, has := extra["--font-feature-settings"]; has {
nodes = append(nodes, d("font-feature-settings", v))
}
if v, has := extra["--font-variation-settings"]; has {
nodes = append(nodes, d("font-variation-settings", v))
}
return uList(nodes)
}
vv := candidate.Value.Value
if v, ok := theme.resolve(&vv, []string{"--font-weight"}, themeNone); ok {
return uNodes(atRoot([]*AstNode{property("--tw-font-weight", "", "")}),
d("--tw-font-weight", v), d("font-weight", v))
}
return nil
}, nil)
c.functionalUtility("font-features", utilityDescription{
handle: func(value, _ string) *utilResult { return uNodes(d("font-feature-settings", value)) },
})
c.staticUtility("uppercase", []staticDecl{sd("text-transform", "uppercase")})
c.staticUtility("lowercase", []staticDecl{sd("text-transform", "lowercase")})
c.staticUtility("capitalize", []staticDecl{sd("text-transform", "capitalize")})
c.staticUtility("normal-case", []staticDecl{sd("text-transform", "none")})
c.staticUtility("italic", []staticDecl{sd("font-style", "italic")})
c.staticUtility("not-italic", []staticDecl{sd("font-style", "normal")})
c.staticUtility("underline", []staticDecl{sd("text-decoration-line", "underline")})
c.staticUtility("overline", []staticDecl{sd("text-decoration-line", "overline")})
c.staticUtility("line-through", []staticDecl{sd("text-decoration-line", "line-through")})
c.staticUtility("no-underline", []staticDecl{sd("text-decoration-line", "none")})
for _, pair := range [][2]string{
{"font-stretch-normal", "normal"}, {"font-stretch-ultra-condensed", "ultra-condensed"},
{"font-stretch-extra-condensed", "extra-condensed"}, {"font-stretch-condensed", "condensed"},
{"font-stretch-semi-condensed", "semi-condensed"}, {"font-stretch-semi-expanded", "semi-expanded"},
{"font-stretch-expanded", "expanded"}, {"font-stretch-extra-expanded", "extra-expanded"},
{"font-stretch-ultra-expanded", "ultra-expanded"},
} {
c.staticUtility(pair[0], []staticDecl{sd("font-stretch", pair[1])})
}
c.functionalUtility("font-stretch", utilityDescription{
handleBareValue: func(v *UtilityValue) (string, bool) {
if !strings.HasSuffix(v.Value, "%") {
return "", false
}
numStr := v.Value[:len(v.Value)-1]
if !isPositiveInteger(numStr) {
return "", false
}
n, ok := jsParseNumber(numStr)
if !ok || n < 50 || n > 200 {
return "", false
}
return v.Value, true
},
handle: func(value, _ string) *utilResult { return uNodes(d("font-stretch", value)) },
})
c.colorUtility("placeholder", colorUtilityDescription{
themeKeys: []string{"--placeholder-color", "--color"},
handle: func(value string) *utilResult {
return uNodes(styleRule("&::placeholder", d("--tw-sort", "placeholder-color"), d("color", value)))
},
})
c.staticUtility("decoration-solid", []staticDecl{sd("text-decoration-style", "solid")})
c.staticUtility("decoration-double", []staticDecl{sd("text-decoration-style", "double")})
c.staticUtility("decoration-dotted", []staticDecl{sd("text-decoration-style", "dotted")})
c.staticUtility("decoration-dashed", []staticDecl{sd("text-decoration-style", "dashed")})
c.staticUtility("decoration-wavy", []staticDecl{sd("text-decoration-style", "wavy")})
c.staticUtility("decoration-auto", []staticDecl{sd("text-decoration-thickness", "auto")})
c.staticUtility("decoration-from-font", []staticDecl{sd("text-decoration-thickness", "from-font")})
registerUtilities12(c)
}
// registerUtilities12: text-decoration (functional) + animation.
func registerUtilities12(c *utilCtx) {
d := decl
theme := c.theme
c.utilities.functional("decoration", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length", "percentage"})
}
switch typ {
case "length", "percentage":
if candidate.Modifier != nil {
return nil
}
return uNodes(d("text-decoration-thickness", value))
default:
v, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("text-decoration-color", v))
}
}
vv := candidate.Value.Value
if v, ok := theme.resolve(&vv, []string{"--text-decoration-thickness"}, themeNone); ok {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("text-decoration-thickness", v))
}
if isPositiveInteger(vv) {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("text-decoration-thickness", vv+"px"))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-decoration-color", "--color"}); ok {
return uNodes(d("text-decoration-color", v))
}
return nil
}, nil)
c.functionalUtility("animate", utilityDescription{
themeKeys: []string{"--animate"},
handle: func(value, _ string) *utilResult { return uNodes(d("animation", value)) },
staticValues: map[string][]*AstNode{"none": {d("animation", "none")}},
})
registerUtilities13(c)
}
func alphaReplacedDropShadowProperties(prop, value string, alpha *string, varInjector func(string) string, prefix string) []*AstNode {
requiresFallback := false
parts := segment(value, ",")
replacedParts := make([]string, len(parts))
for i, v := range parts {
replacedParts[i] = "drop-shadow(" + replaceShadowColors(v, func(color string) string {
if alpha == nil {
return varInjector(color)
}
if strings.HasPrefix(color, "current") {
return varInjector(withAlpha(color, *alpha))
}
if strings.HasPrefix(color, "var(") || strings.HasPrefix(*alpha, "var(") {
requiresFallback = true
}
return varInjector(replaceAlpha(color, *alpha))
}) + ")"
}
replacedValue := strings.Join(replacedParts, " ")
if requiresFallback {
fb := make([]string, len(parts))
for i, v := range parts {
fb[i] = "drop-shadow(" + replaceShadowColors(v, varInjector) + ")"
}
return []*AstNode{
decl(prop, prefix+strings.Join(fb, " ")),
rule("@supports (color: lab(from red l a b))", decl(prop, prefix+replacedValue)),
}
}
return []*AstNode{decl(prop, prefix+replacedValue)}
}
// registerUtilities13: filter / backdrop-filter and all individual filters.
func registerUtilities13(c *utilCtx) {
d := decl
theme := c.theme
cssFilterValue := "var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)"
cssBackdropFilterValue := "var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)"
filterProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-blur", "", ""), property("--tw-brightness", "", ""), property("--tw-contrast", "", ""),
property("--tw-grayscale", "", ""), property("--tw-hue-rotate", "", ""), property("--tw-invert", "", ""),
property("--tw-opacity", "", ""), property("--tw-saturate", "", ""), property("--tw-sepia", "", ""),
property("--tw-drop-shadow", "", ""), property("--tw-drop-shadow-color", "", ""),
property("--tw-drop-shadow-alpha", "100%", "<percentage>"), property("--tw-drop-shadow-size", "", ""),
})
}
backdropFilterProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-backdrop-blur", "", ""), property("--tw-backdrop-brightness", "", ""),
property("--tw-backdrop-contrast", "", ""), property("--tw-backdrop-grayscale", "", ""),
property("--tw-backdrop-hue-rotate", "", ""), property("--tw-backdrop-invert", "", ""),
property("--tw-backdrop-opacity", "", ""), property("--tw-backdrop-saturate", "", ""),
property("--tw-backdrop-sepia", "", ""),
})
}
c.utilities.functional("filter", func(candidate *Candidate) *utilResult {
if candidate.Modifier != nil {
return nil
}
if candidate.Value == nil {
return uNodes(filterProperties(), d("filter", cssFilterValue))
}
if candidate.Value.Kind == uvArbitrary {
return uNodes(d("filter", candidate.Value.Value))
}
if candidate.Value.Value == "none" {
return uNodes(d("filter", "none"))
}
return nil
}, nil)
c.utilities.functional("backdrop-filter", func(candidate *Candidate) *utilResult {
if candidate.Modifier != nil {
return nil
}
if candidate.Value == nil {
return uNodes(backdropFilterProperties(), d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
}
if candidate.Value.Kind == uvArbitrary {
return uNodes(d("-webkit-backdrop-filter", candidate.Value.Value), d("backdrop-filter", candidate.Value.Value))
}
if candidate.Value.Value == "none" {
return uNodes(d("-webkit-backdrop-filter", "none"), d("backdrop-filter", "none"))
}
return nil
}, nil)
pctBare := func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "%", true
}
degBareF := func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "deg", true
}
// blur
c.functionalUtility("blur", utilityDescription{
themeKeys: []string{"--blur"},
handle: func(value, _ string) *utilResult {
return uNodes(filterProperties(), d("--tw-blur", "blur("+value+")"), d("filter", cssFilterValue))
},
staticValues: map[string][]*AstNode{"none": {filterProperties(), d("--tw-blur", " "), d("filter", cssFilterValue)}},
})
c.functionalUtility("backdrop-blur", utilityDescription{
themeKeys: []string{"--backdrop-blur", "--blur"},
handle: func(value, _ string) *utilResult {
return uNodes(backdropFilterProperties(), d("--tw-backdrop-blur", "blur("+value+")"),
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
},
staticValues: map[string][]*AstNode{"none": {backdropFilterProperties(), d("--tw-backdrop-blur", " "),
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue)}},
})
// simple filter factory (filter side)
simpleFilter := func(name, themeKey, fn string, defaultPct bool) {
desc := utilityDescription{
themeKeys: []string{themeKey},
handleBareValue: pctBare,
handle: func(value, _ string) *utilResult {
return uNodes(filterProperties(), d("--tw-"+name, fn+"("+value+")"), d("filter", cssFilterValue))
},
}
if defaultPct {
desc.defaultValueSet = true
desc.defaultValue = sptr("100%")
}
c.functionalUtility(name, desc)
}
simpleBackdrop := func(name, twName, themeKeyA, themeKeyB, fn string, defaultPct bool, bare func(*UtilityValue) (string, bool)) {
desc := utilityDescription{
themeKeys: []string{themeKeyA, themeKeyB},
handleBareValue: bare,
handle: func(value, _ string) *utilResult {
return uNodes(backdropFilterProperties(), d("--tw-"+twName, fn+"("+value+")"),
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
},
}
if defaultPct {
desc.defaultValueSet = true
desc.defaultValue = sptr("100%")
}
c.functionalUtility(name, desc)
}
simpleFilter("brightness", "--brightness", "brightness", false)
simpleBackdrop("backdrop-brightness", "backdrop-brightness", "--backdrop-brightness", "--brightness", "brightness", false, pctBare)
simpleFilter("contrast", "--contrast", "contrast", false)
simpleBackdrop("backdrop-contrast", "backdrop-contrast", "--backdrop-contrast", "--contrast", "contrast", false, pctBare)
simpleFilter("grayscale", "--grayscale", "grayscale", true)
simpleBackdrop("backdrop-grayscale", "backdrop-grayscale", "--backdrop-grayscale", "--grayscale", "grayscale", true, pctBare)
// hue-rotate (supportsNegative, deg)
c.functionalUtility("hue-rotate", utilityDescription{
supportsNegative: true, themeKeys: []string{"--hue-rotate"}, handleBareValue: degBareF,
handle: func(value, _ string) *utilResult {
return uNodes(filterProperties(), d("--tw-hue-rotate", "hue-rotate("+value+")"), d("filter", cssFilterValue))
},
})
c.functionalUtility("backdrop-hue-rotate", utilityDescription{
supportsNegative: true, themeKeys: []string{"--backdrop-hue-rotate", "--hue-rotate"}, handleBareValue: degBareF,
handle: func(value, _ string) *utilResult {
return uNodes(backdropFilterProperties(), d("--tw-backdrop-hue-rotate", "hue-rotate("+value+")"),
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
},
})
simpleFilter("invert", "--invert", "invert", true)
simpleBackdrop("backdrop-invert", "backdrop-invert", "--backdrop-invert", "--invert", "invert", true, pctBare)
simpleFilter("saturate", "--saturate", "saturate", false)
simpleBackdrop("backdrop-saturate", "backdrop-saturate", "--backdrop-saturate", "--saturate", "saturate", false, pctBare)
simpleFilter("sepia", "--sepia", "sepia", true)
simpleBackdrop("backdrop-sepia", "backdrop-sepia", "--backdrop-sepia", "--sepia", "sepia", true, pctBare)
c.staticUtility("drop-shadow-none", []staticDecl{sdFn(filterProperties), sd("--tw-drop-shadow", " "), sd("filter", cssFilterValue)})
varInjector := func(color string) string { return "var(--tw-drop-shadow-color, " + color + ")" }
c.utilities.functional("drop-shadow", func(candidate *Candidate) *utilResult {
var alphaPtr *string
if candidate.Modifier != nil {
if candidate.Modifier.Kind == modArbitrary {
v := candidate.Modifier.Value
alphaPtr = &v
} else if isPositiveInteger(candidate.Modifier.Value) {
v := candidate.Modifier.Value + "%"
alphaPtr = &v
}
}
alphaDecl := func() *AstNode {
if alphaPtr != nil {
return d("--tw-drop-shadow-alpha", *alphaPtr)
}
return &AstNode{Kind: nDeclaration, Property: "--tw-drop-shadow-alpha", Undefined: true}
}
if candidate.Value == nil {
value, okGet := theme.Get([]string{"--drop-shadow"})
resolved, okRes := theme.resolve(nil, []string{"--drop-shadow"}, themeNone)
if !okGet || !okRes {
return nil
}
nodes := []*AstNode{filterProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
nodes = append(nodes, d("--tw-drop-shadow", dropShadowJoin(resolved)), d("filter", cssFilterValue))
return uList(nodes)
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color"})
}
if typ == "color" {
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(filterProperties(),
d("--tw-drop-shadow-color", withAlpha(cv, "var(--tw-drop-shadow-alpha)")),
d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
}
if candidate.Modifier != nil && alphaPtr == nil {
return nil
}
nodes := []*AstNode{filterProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
nodes = append(nodes, d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"), d("filter", cssFilterValue))
return uList(nodes)
}
cv := candidate.Value.Value
value, okGet := theme.Get([]string{"--drop-shadow-" + cv})
resolved, okRes := theme.resolve(&cv, []string{"--drop-shadow"}, themeNone)
if okGet && okRes {
if candidate.Modifier != nil && alphaPtr == nil {
return nil
}
nodes := []*AstNode{filterProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
if alphaPtr != nil {
nodes = append(nodes, d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"), d("filter", cssFilterValue))
} else {
nodes = append(nodes, d("--tw-drop-shadow", dropShadowJoin(resolved)), d("filter", cssFilterValue))
}
return uList(nodes)
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--drop-shadow-color", "--color"}); ok {
if v == "inherit" {
return uNodes(filterProperties(), d("--tw-drop-shadow-color", "inherit"), d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
}
return uNodes(filterProperties(), d("--tw-drop-shadow-color", withAlpha(v, "var(--tw-drop-shadow-alpha)")), d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
}
return nil
}, nil)
c.functionalUtility("backdrop-opacity", utilityDescription{
themeKeys: []string{"--backdrop-opacity", "--opacity"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isValidOpacityValue(v.Value) {
return "", false
}
return v.Value + "%", true
},
handle: func(value, _ string) *utilResult {
return uNodes(backdropFilterProperties(), d("--tw-backdrop-opacity", "opacity("+value+")"),
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
},
})
registerUtilities14(c)
}
func dropShadowJoin(resolved string) string {
parts := segment(resolved, ",")
for i, p := range parts {
parts[i] = "drop-shadow(" + p + ")"
}
return strings.Join(parts, " ")
}
// registerUtilities14: transition/delay/duration/ease, will-change, content,
// contain, forced-color-adjust, leading, tracking, antialiasing,
// font-variant-numeric.
func registerUtilities14(c *utilCtx) {
d := decl
theme := c.theme
dtf := "ease"
if v, ok := theme.resolve(nil, []string{"--default-transition-timing-function"}, themeNone); ok {
dtf = v
}
defaultTimingFunction := "var(--tw-ease, " + dtf + ")"
dd := "0s"
if v, ok := theme.resolve(nil, []string{"--default-transition-duration"}, themeNone); ok {
dd = v
}
defaultDuration := "var(--tw-duration, " + dd + ")"
transitionDefault := "color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events"
tdecls := func(prop string) []*AstNode {
return []*AstNode{d("transition-property", prop), d("transition-timing-function", defaultTimingFunction), d("transition-duration", defaultDuration)}
}
c.functionalUtility("transition", utilityDescription{
defaultValueSet: true, defaultValue: sptr(transitionDefault),
themeKeys: []string{"--transition-property"},
handle: func(value, _ string) *utilResult { return uList(tdecls(value)) },
staticValues: map[string][]*AstNode{
"none": {d("transition-property", "none")},
"all": tdecls("all"),
"colors": tdecls("color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to"),
"opacity": tdecls("opacity"),
"shadow": tdecls("box-shadow"),
"transform": tdecls("transform, translate, scale, rotate"),
},
})
c.staticUtility("transition-discrete", []staticDecl{sd("transition-behavior", "allow-discrete")})
c.staticUtility("transition-normal", []staticDecl{sd("transition-behavior", "normal")})
c.functionalUtility("delay", utilityDescription{
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "ms", true
},
themeKeys: []string{"--transition-delay"},
handle: func(value, _ string) *utilResult { return uNodes(d("transition-delay", value)) },
})
transitionDurationProperty := func() *AstNode { return atRoot([]*AstNode{property("--tw-duration", "", "")}) }
c.staticUtility("duration-initial", []staticDecl{sdFn(transitionDurationProperty), sd("--tw-duration", "initial")})
c.utilities.functional("duration", func(candidate *Candidate) *utilResult {
if candidate.Modifier != nil || candidate.Value == nil {
return nil
}
var value string
ok := false
if candidate.Value.Kind == uvArbitrary {
value, ok = candidate.Value.Value, true
} else {
key := candidate.Value.Value
if candidate.Value.Fraction != "" {
key = candidate.Value.Fraction
}
value, ok = theme.resolve(&key, []string{"--transition-duration"}, themeNone)
if !ok && isPositiveInteger(candidate.Value.Value) {
value, ok = candidate.Value.Value+"ms", true
}
}
if !ok {
return nil
}
return uNodes(transitionDurationProperty(), d("--tw-duration", value), d("transition-duration", value))
}, nil)
transitionTimingFunctionProperty := func() *AstNode { return atRoot([]*AstNode{property("--tw-ease", "", "")}) }
c.functionalUtility("ease", utilityDescription{
themeKeys: []string{"--ease"},
handle: func(value, _ string) *utilResult {
return uNodes(transitionTimingFunctionProperty(), d("--tw-ease", value), d("transition-timing-function", value))
},
staticValues: map[string][]*AstNode{
"initial": {transitionTimingFunctionProperty(), d("--tw-ease", "initial")},
"linear": {transitionTimingFunctionProperty(), d("--tw-ease", "linear"), d("transition-timing-function", "linear")},
},
})
c.staticUtility("will-change-auto", []staticDecl{sd("will-change", "auto")})
c.staticUtility("will-change-scroll", []staticDecl{sd("will-change", "scroll-position")})
c.staticUtility("will-change-contents", []staticDecl{sd("will-change", "contents")})
c.staticUtility("will-change-transform", []staticDecl{sd("will-change", "transform")})
c.functionalUtility("will-change", utilityDescription{
handle: func(value, _ string) *utilResult { return uNodes(d("will-change", value)) },
})
c.staticUtility("content-none", []staticDecl{sd("--tw-content", "none"), sd("content", "none")})
c.functionalUtility("content", utilityDescription{
themeKeys: []string{"--content"},
handle: func(value, _ string) *utilResult {
return uNodes(atRoot([]*AstNode{property("--tw-content", `""`, "")}), d("--tw-content", value), d("content", "var(--tw-content)"))
},
})
cssContainValue := "var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)"
cssContainProperties := func() *AstNode {
return atRoot([]*AstNode{property("--tw-contain-size", "", ""), property("--tw-contain-layout", "", ""), property("--tw-contain-paint", "", ""), property("--tw-contain-style", "", "")})
}
c.staticUtility("contain-none", []staticDecl{sd("contain", "none")})
c.staticUtility("contain-content", []staticDecl{sd("contain", "content")})
c.staticUtility("contain-strict", []staticDecl{sd("contain", "strict")})
for _, pair := range [][2]string{
{"contain-size", "size"}, {"contain-inline-size", "inline-size"}, {"contain-layout", "layout"},
{"contain-paint", "paint"}, {"contain-style", "style"},
} {
twVar := "--tw-contain-" + strings.TrimPrefix(pair[0], "contain-")
if pair[0] == "contain-inline-size" {
twVar = "--tw-contain-size"
}
c.staticUtility(pair[0], []staticDecl{sdFn(cssContainProperties), sd(twVar, pair[1]), sd("contain", cssContainValue)})
}
c.functionalUtility("contain", utilityDescription{
handle: func(value, _ string) *utilResult { return uNodes(d("contain", value)) },
})
c.staticUtility("forced-color-adjust-none", []staticDecl{sd("forced-color-adjust", "none")})
c.staticUtility("forced-color-adjust-auto", []staticDecl{sd("forced-color-adjust", "auto")})
c.spacingUtility("leading", []string{"--leading", "--spacing"},
func(value string) *utilResult {
return uNodes(atRoot([]*AstNode{property("--tw-leading", "", "")}), d("--tw-leading", value), d("line-height", value))
}, spacingOpts{staticValues: map[string][]*AstNode{
"none": {atRoot([]*AstNode{property("--tw-leading", "", "")}), d("--tw-leading", "1"), d("line-height", "1")},
}})
c.functionalUtility("tracking", utilityDescription{
supportsNegative: true,
themeKeys: []string{"--tracking"},
handle: func(value, _ string) *utilResult {
return uNodes(atRoot([]*AstNode{property("--tw-tracking", "", "")}), d("--tw-tracking", value), d("letter-spacing", value))
},
})
c.staticUtility("antialiased", []staticDecl{sd("-webkit-font-smoothing", "antialiased"), sd("-moz-osx-font-smoothing", "grayscale")})
c.staticUtility("subpixel-antialiased", []staticDecl{sd("-webkit-font-smoothing", "auto"), sd("-moz-osx-font-smoothing", "auto")})
cssFVN := "var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)"
fvnProps := func() *AstNode {
return atRoot([]*AstNode{property("--tw-ordinal", "", ""), property("--tw-slashed-zero", "", ""), property("--tw-numeric-figure", "", ""), property("--tw-numeric-spacing", "", ""), property("--tw-numeric-fraction", "", "")})
}
c.staticUtility("normal-nums", []staticDecl{sd("font-variant-numeric", "normal")})
for _, e := range []struct{ name, twVar, val string }{
{"ordinal", "--tw-ordinal", "ordinal"}, {"slashed-zero", "--tw-slashed-zero", "slashed-zero"},
{"lining-nums", "--tw-numeric-figure", "lining-nums"}, {"oldstyle-nums", "--tw-numeric-figure", "oldstyle-nums"},
{"proportional-nums", "--tw-numeric-spacing", "proportional-nums"}, {"tabular-nums", "--tw-numeric-spacing", "tabular-nums"},
{"diagonal-fractions", "--tw-numeric-fraction", "diagonal-fractions"}, {"stacked-fractions", "--tw-numeric-fraction", "stacked-fractions"},
} {
e := e
c.staticUtility(e.name, []staticDecl{sdFn(fvnProps), sd(e.twVar, e.val), sd("font-variant-numeric", cssFVN)})
}
registerUtilities15(c)
}
func alphaReplacedShadowProperties(prop, value string, alpha *string, varInjector func(string) string, prefix string) []*AstNode {
requiresFallback := false
replacedValue := replaceShadowColors(value, func(color string) string {
if alpha == nil {
return varInjector(color)
}
if strings.HasPrefix(color, "current") {
return varInjector(withAlpha(color, *alpha))
}
if strings.HasPrefix(color, "var(") || strings.HasPrefix(*alpha, "var(") {
requiresFallback = true
}
return varInjector(replaceAlpha(color, *alpha))
})
applyPrefix := func(x string) string {
if prefix == "" {
return x
}
parts := segment(x, ",")
for i, v := range parts {
parts[i] = strings.TrimSpace(prefix) + " " + strings.TrimSpace(v)
}
return strings.Join(parts, ", ")
}
if requiresFallback {
return []*AstNode{
decl(prop, applyPrefix(replaceShadowColors(value, varInjector))),
rule("@supports (color: lab(from red l a b))", decl(prop, applyPrefix(replacedValue))),
}
}
return []*AstNode{decl(prop, applyPrefix(replacedValue))}
}
// shadowAlpha extracts an opacity modifier as an alpha (*string), matching the
// shadow/text-shadow/drop-shadow modifier handling.
func shadowAlpha(modifier *CandidateModifier) *string {
if modifier == nil {
return nil
}
if modifier.Kind == modArbitrary {
v := modifier.Value
return &v
}
if isPositiveInteger(modifier.Value) {
v := modifier.Value + "%"
return &v
}
return nil
}
// registerUtilities15: outline, opacity, underline-offset, text, text-shadow.
func registerUtilities15(c *utilCtx) {
d := decl
theme := c.theme
outlineProperties := func() *AstNode { return atRoot([]*AstNode{property("--tw-outline-style", "solid", "")}) }
c.utilities.static("outline-hidden", func(_ *Candidate) *utilResult {
return uNodes(d("--tw-outline-style", "none"), d("outline-style", "none"),
atRule("@media", "(forced-colors: active)", d("outline", "2px solid transparent"), d("outline-offset", "2px")))
})
c.staticUtility("outline-none", []staticDecl{sd("--tw-outline-style", "none"), sd("outline-style", "none")})
c.staticUtility("outline-solid", []staticDecl{sd("--tw-outline-style", "solid"), sd("outline-style", "solid")})
c.staticUtility("outline-dashed", []staticDecl{sd("--tw-outline-style", "dashed"), sd("outline-style", "dashed")})
c.staticUtility("outline-dotted", []staticDecl{sd("--tw-outline-style", "dotted"), sd("outline-style", "dotted")})
c.staticUtility("outline-double", []staticDecl{sd("--tw-outline-style", "double"), sd("outline-style", "double")})
c.utilities.functional("outline", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
if candidate.Modifier != nil {
return nil
}
value, ok := theme.Get([]string{"--default-outline-width"})
if !ok {
value = "1px"
}
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", value))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length", "number", "percentage"})
}
switch typ {
case "length", "number", "percentage":
if candidate.Modifier != nil {
return nil
}
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", value))
default:
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("outline-color", cv))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--outline-color", "--color"}); ok {
return uNodes(d("outline-color", v))
}
if candidate.Modifier != nil {
return nil
}
vv := candidate.Value.Value
if v, ok := theme.resolve(&vv, []string{"--outline-width"}, themeNone); ok {
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", v))
} else if isPositiveInteger(vv) {
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", vv+"px"))
}
return nil
}, nil)
c.functionalUtility("outline-offset", utilityDescription{
supportsNegative: true,
themeKeys: []string{"--outline-offset"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "px", true
},
handle: func(value, _ string) *utilResult { return uNodes(d("outline-offset", value)) },
})
c.functionalUtility("opacity", utilityDescription{
themeKeys: []string{"--opacity"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isValidOpacityValue(v.Value) {
return "", false
}
return v.Value + "%", true
},
handle: func(value, _ string) *utilResult { return uNodes(d("opacity", value)) },
})
c.functionalUtility("underline-offset", utilityDescription{
supportsNegative: true,
themeKeys: []string{"--text-underline-offset"},
handleBareValue: func(v *UtilityValue) (string, bool) {
if !isPositiveInteger(v.Value) {
return "", false
}
return v.Value + "px", true
},
handle: func(value, _ string) *utilResult { return uNodes(d("text-underline-offset", value)) },
staticValues: map[string][]*AstNode{"auto": {d("text-underline-offset", "auto")}},
})
// resolveTextModifier resolves a `/<modifier>` on text-* into a line-height.
resolveTextModifier := func(modifier *CandidateModifier) (string, bool) {
var mod string
mok := false
if modifier.Kind == modArbitrary {
mod, mok = modifier.Value, true
} else if v, ok := theme.resolve(&modifier.Value, []string{"--leading"}, themeNone); ok {
mod, mok = v, true
}
if !mok && isValidSpacingMultiplier(modifier.Value) {
if _, ok := theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
return "", false
}
mod, mok = "--spacing("+modifier.Value+")", true
}
if !mok && modifier.Value == "none" {
mod, mok = "1", true
}
return mod, mok
}
c.utilities.functional("text", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length", "percentage", "absolute-size", "relative-size"})
}
switch typ {
case "size", "length", "percentage", "absolute-size", "relative-size":
if candidate.Modifier != nil {
mod, ok := resolveTextModifier(candidate.Modifier)
if !ok {
return nil
}
return uNodes(d("font-size", value), d("line-height", mod))
}
return uNodes(d("font-size", value))
default:
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("color", cv))
}
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-color", "--color"}); ok {
return uNodes(d("color", v))
}
if fontSize, options, ok := theme.resolveWith(candidate.Value.Value, []string{"--text"},
[]string{"--line-height", "--letter-spacing", "--font-weight"}); ok {
if candidate.Modifier != nil {
mod, mok := resolveTextModifier(candidate.Modifier)
if !mok {
return nil
}
return uNodes(d("font-size", fontSize), d("line-height", mod))
}
nodes := []*AstNode{d("font-size", fontSize)}
if lh, has := options["--line-height"]; has {
nodes = append(nodes, d("line-height", "var(--tw-leading, "+lh+")"))
}
if ls, has := options["--letter-spacing"]; has {
nodes = append(nodes, d("letter-spacing", "var(--tw-tracking, "+ls+")"))
}
if fw, has := options["--font-weight"]; has {
nodes = append(nodes, d("font-weight", "var(--tw-font-weight, "+fw+")"))
}
return uList(nodes)
}
return nil
}, nil)
textShadowProperties := func() *AstNode {
return atRoot([]*AstNode{property("--tw-text-shadow-color", "", ""), property("--tw-text-shadow-alpha", "100%", "<percentage>")})
}
c.staticUtility("text-shadow-initial", []staticDecl{sdFn(textShadowProperties), sd("--tw-text-shadow-color", "initial")})
tsInjector := func(color string) string { return "var(--tw-text-shadow-color, " + color + ")" }
c.utilities.functional("text-shadow", func(candidate *Candidate) *utilResult {
alpha := shadowAlpha(candidate.Modifier)
alphaDecl := func() *AstNode {
if alpha != nil {
return d("--tw-text-shadow-alpha", *alpha)
}
return &AstNode{Kind: nDeclaration, Property: "--tw-text-shadow-alpha", Undefined: true}
}
if candidate.Value == nil {
value, ok := theme.Get([]string{"--text-shadow"})
if !ok {
return nil
}
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
return uList(nodes)
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color"})
}
if typ == "color" {
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", withAlpha(cv, "var(--tw-text-shadow-alpha)")))
}
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
return uList(nodes)
}
switch candidate.Value.Value {
case "none":
if candidate.Modifier != nil {
return nil
}
return uNodes(textShadowProperties(), d("text-shadow", "none"))
case "inherit":
if candidate.Modifier != nil {
return nil
}
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", "inherit"))
}
if value, ok := theme.Get([]string{"--text-shadow-" + candidate.Value.Value}); ok {
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
return uList(nodes)
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-shadow-color", "--color"}); ok {
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", withAlpha(v, "var(--tw-text-shadow-alpha)")))
}
return nil
}, nil)
registerUtilities16(c)
}
func undecl(p string) *AstNode { return &AstNode{Kind: nDeclaration, Property: p, Undefined: true} }
// registerUtilities16: box-shadow, inset-shadow, ring, inset-ring, ring-offset.
func registerUtilities16(c *utilCtx) {
d := decl
theme := c.theme
cssBoxShadowValue := "var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)"
nullShadow := "0 0 #0000"
boxShadowProperties := func() *AstNode {
return atRoot([]*AstNode{
property("--tw-shadow", nullShadow, ""), property("--tw-shadow-color", "", ""),
property("--tw-shadow-alpha", "100%", "<percentage>"),
property("--tw-inset-shadow", nullShadow, ""), property("--tw-inset-shadow-color", "", ""),
property("--tw-inset-shadow-alpha", "100%", "<percentage>"),
property("--tw-ring-color", "", ""), property("--tw-ring-shadow", nullShadow, ""),
property("--tw-inset-ring-color", "", ""), property("--tw-inset-ring-shadow", nullShadow, ""),
property("--tw-ring-inset", "", ""), property("--tw-ring-offset-width", "0px", "<length>"),
property("--tw-ring-offset-color", "#fff", ""), property("--tw-ring-offset-shadow", nullShadow, ""),
})
}
c.staticUtility("shadow-initial", []staticDecl{sdFn(boxShadowProperties), sd("--tw-shadow-color", "initial")})
shadowInj := func(color string) string { return "var(--tw-shadow-color, " + color + ")" }
c.utilities.functional("shadow", func(candidate *Candidate) *utilResult {
alpha := shadowAlpha(candidate.Modifier)
alphaDecl := func() *AstNode {
if alpha != nil {
return d("--tw-shadow-alpha", *alpha)
}
return undecl("--tw-shadow-alpha")
}
if candidate.Value == nil {
value, ok := theme.Get([]string{"--shadow"})
if !ok {
return nil
}
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color"})
}
if typ == "color" {
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-shadow-color", withAlpha(cv, "var(--tw-shadow-alpha)")))
}
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
switch candidate.Value.Value {
case "none":
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-shadow", nullShadow), d("box-shadow", cssBoxShadowValue))
case "inherit":
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-shadow-color", "inherit"))
}
if value, ok := theme.Get([]string{"--shadow-" + candidate.Value.Value}); ok {
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--box-shadow-color", "--color"}); ok {
return uNodes(boxShadowProperties(), d("--tw-shadow-color", withAlpha(v, "var(--tw-shadow-alpha)")))
}
return nil
}, nil)
c.staticUtility("inset-shadow-initial", []staticDecl{sdFn(boxShadowProperties), sd("--tw-inset-shadow-color", "initial")})
insetShadowInj := func(color string) string { return "var(--tw-inset-shadow-color, " + color + ")" }
c.utilities.functional("inset-shadow", func(candidate *Candidate) *utilResult {
alpha := shadowAlpha(candidate.Modifier)
alphaDecl := func() *AstNode {
if alpha != nil {
return d("--tw-inset-shadow-alpha", *alpha)
}
return undecl("--tw-inset-shadow-alpha")
}
if candidate.Value == nil {
value, ok := theme.Get([]string{"--inset-shadow"})
if !ok {
return nil
}
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color"})
}
if typ == "color" {
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", withAlpha(cv, "var(--tw-inset-shadow-alpha)")))
}
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "inset")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
switch candidate.Value.Value {
case "none":
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-inset-shadow", "inset "+nullShadow), d("box-shadow", cssBoxShadowValue))
case "inherit":
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", "inherit"))
}
if value, ok := theme.Get([]string{"--inset-shadow-" + candidate.Value.Value}); ok {
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "")...)
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--box-shadow-color", "--color"}); ok {
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", withAlpha(v, "var(--tw-inset-shadow-alpha)")))
}
return nil
}, nil)
c.staticUtility("ring-inset", []staticDecl{sdFn(boxShadowProperties), sd("--tw-ring-inset", "inset")})
defaultRingColor := "currentcolor"
if v, ok := theme.Get([]string{"--default-ring-color"}); ok {
defaultRingColor = v
}
ringShadowValue := func(value string) string {
return "var(--tw-ring-inset,) 0 0 0 calc(" + value + " + var(--tw-ring-offset-width)) var(--tw-ring-color, " + defaultRingColor + ")"
}
c.utilities.functional("ring", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
if candidate.Modifier != nil {
return nil
}
value, ok := theme.Get([]string{"--default-ring-width"})
if !ok {
value = "1px"
}
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length"})
}
if typ == "length" {
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
}
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("--tw-ring-color", cv))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-color", "--color"}); ok {
return uNodes(d("--tw-ring-color", v))
}
if candidate.Modifier != nil {
return nil
}
vv := candidate.Value.Value
value, ok := theme.resolve(&vv, []string{"--ring-width"}, themeNone)
if !ok && isPositiveInteger(vv) {
value, ok = vv+"px", true
}
if ok {
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
}
return nil
}, nil)
insetRingShadowValue := func(value string) string {
return "inset 0 0 0 " + value + " var(--tw-inset-ring-color, currentcolor)"
}
c.utilities.functional("inset-ring", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue("1px")), d("box-shadow", cssBoxShadowValue))
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length"})
}
if typ == "length" {
if candidate.Modifier != nil {
return nil
}
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue(value)), d("box-shadow", cssBoxShadowValue))
}
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("--tw-inset-ring-color", cv))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-color", "--color"}); ok {
return uNodes(d("--tw-inset-ring-color", v))
}
if candidate.Modifier != nil {
return nil
}
vv := candidate.Value.Value
value, ok := theme.resolve(&vv, []string{"--ring-width"}, themeNone)
if !ok && isPositiveInteger(vv) {
value, ok = vv+"px", true
}
if ok {
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue(value)), d("box-shadow", cssBoxShadowValue))
}
return nil
}, nil)
ringOffsetShadowValue := "var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)"
c.utilities.functional("ring-offset", func(candidate *Candidate) *utilResult {
if candidate.Value == nil {
return nil
}
if candidate.Value.Kind == uvArbitrary {
value := candidate.Value.Value
typ := candidate.Value.DataType
if typ == "" {
typ = inferDataType(value, []string{"color", "length"})
}
if typ == "length" {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("--tw-ring-offset-width", value), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
}
cv, ok := asColor(value, candidate.Modifier, theme)
if !ok {
return nil
}
return uNodes(d("--tw-ring-offset-color", cv))
}
vv := candidate.Value.Value
if v, ok := theme.resolve(&vv, []string{"--ring-offset-width"}, themeNone); ok {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("--tw-ring-offset-width", v), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
} else if isPositiveInteger(vv) {
if candidate.Modifier != nil {
return nil
}
return uNodes(d("--tw-ring-offset-width", vv+"px"), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
}
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-offset-color", "--color"}); ok {
return uNodes(d("--tw-ring-offset-color", v))
}
return nil
}, nil)
registerUtilities17(c)
}
// registerUtilities17: @container (container-type).
func registerUtilities17(c *utilCtx) {
d := decl
c.utilities.functional("@container", func(candidate *Candidate) *utilResult {
value := ""
ok := false
if candidate.Value == nil {
value, ok = "inline-size", true
} else if candidate.Value.Kind == uvArbitrary {
value, ok = candidate.Value.Value, true
} else if candidate.Value.Kind == uvNamed && candidate.Value.Value == "normal" {
value, ok = "normal", true
} else if candidate.Value.Kind == uvNamed && candidate.Value.Value == "size" {
value, ok = "size", true
}
if !ok {
return nil
}
if candidate.Modifier != nil {
return uNodes(d("container-type", value), d("container-name", candidate.Modifier.Value))
}
return uNodes(d("container-type", value))
}, nil)
}
// Port of packages/tailwindcss/src/value-parser.ts
//
// Parses a CSS value into a small AST of words, function calls, and
// separators. Used by arbitrary-value decoding and the theme()/calc() helpers.
// Nodes are pointers so passes (e.g. underscore decoding) can mutate them.
type ValueNode interface{ valueNode() }
type ValueWord struct{ Value string }
type ValueFunction struct {
Value string
Nodes []ValueNode
}
type ValueSeparator struct{ Value string }
func (*ValueWord) valueNode() {}
func (*ValueFunction) valueNode() {}
func (*ValueSeparator) valueNode() {}
func valueToCss(ast []ValueNode) string {
var b strings.Builder
writeValueNodes(&b, ast)
return b.String()
}
func writeValueNodes(b *strings.Builder, ast []ValueNode) {
for _, node := range ast {
switch n := node.(type) {
case *ValueWord:
b.WriteString(n.Value)
case *ValueSeparator:
b.WriteString(n.Value)
case *ValueFunction:
b.WriteString(n.Value)
b.WriteByte('(')
writeValueNodes(b, n.Nodes)
b.WriteByte(')')
}
}
}
func isValueSeparatorByte(c byte) bool {
switch c {
case ':', ',', '=', '>', '<', '\n', ' ', '\t':
return true
}
return false
}
func valueParse(input string) []ValueNode {
input = strings.ReplaceAll(input, "\r\n", "\n")
var ast []ValueNode
var stack []*ValueFunction
var parent *ValueFunction
var buf []byte
push := func(n ValueNode) {
if parent != nil {
parent.Nodes = append(parent.Nodes, n)
} else {
ast = append(ast, n)
}
}
flushWord := func() {
if len(buf) > 0 {
push(&ValueWord{Value: string(buf)})
buf = buf[:0]
}
}
for i := 0; i < len(input); i++ {
c := input[i]
switch {
case c == '\\':
// Escaped character: consume this and the next byte.
if i+1 < len(input) {
buf = append(buf, input[i], input[i+1])
i++
} else {
buf = append(buf, c)
}
case c == '/':
// `/` is its own word (e.g. theme(colors.red.500/10)).
flushWord()
push(&ValueWord{Value: "/"})
case isValueSeparatorByte(c):
flushWord()
start := i
end := i + 1
for ; end < len(input); end++ {
if !isValueSeparatorByte(input[end]) {
break
}
}
i = end - 1
push(&ValueSeparator{Value: input[start:end]})
case c == '\'' || c == '"':
start := i
for j := i + 1; j < len(input); j++ {
p := input[j]
if p == '\\' {
j++
continue
}
if p == c {
i = j
break
}
}
buf = append(buf, input[start:i+1]...)
case c == '(':
n := &ValueFunction{Value: string(buf)}
buf = buf[:0]
push(n)
stack = append(stack, n)
parent = n
case c == ')':
var tail *ValueFunction
if len(stack) > 0 {
tail = stack[len(stack)-1]
stack = stack[:len(stack)-1]
}
if len(buf) > 0 {
if tail != nil {
tail.Nodes = append(tail.Nodes, &ValueWord{Value: string(buf)})
}
buf = buf[:0]
}
if len(stack) > 0 {
parent = stack[len(stack)-1]
} else {
parent = nil
}
default:
buf = append(buf, c)
}
}
if len(buf) > 0 {
ast = append(ast, &ValueWord{Value: string(buf)})
}
return ast
}
// Port of packages/tailwindcss/src/variants.ts
//
// The Variants registry plus createVariants (the full variant catalog).
// suggest()/completions are stubbed (IntelliSense only). substituteAtVariant
// returns nothing (the Features bitset is not modeled).
type Compounds int
const (
CompoundsNever Compounds = 0
CompoundsAtRules Compounds = 1 << 0
CompoundsStyleRules Compounds = 1 << 1
)
type variantApplyFn func(r *AstNode, v *Variant) bool
type variantInfo struct {
kind variantKind
order int
applyFn variantApplyFn
compoundsWith Compounds
compounds Compounds
}
type vOpts struct {
compounds Compounds
hasCompounds bool
order int
hasOrder bool
}
type Variants struct {
variants map[string]*variantInfo
order []string
compareFns map[int]func(a, z *Variant) int
groupOrder *int
lastOrder int
}
func NewVariants() *Variants {
return &Variants{variants: map[string]*variantInfo{}, compareFns: map[int]func(a, z *Variant) int{}}
}
func (v *Variants) nextOrder() int {
if v.groupOrder != nil {
return *v.groupOrder
}
return v.lastOrder + 1
}
func (v *Variants) set(name string, kind variantKind, fn variantApplyFn, compoundsWith, compounds Compounds, o vOpts) {
if existing, ok := v.variants[name]; ok {
existing.kind = kind
existing.applyFn = fn
existing.compounds = compounds
return
}
order := o.order
if !o.hasOrder {
v.lastOrder = v.nextOrder()
order = v.lastOrder
}
v.variants[name] = &variantInfo{kind: kind, order: order, applyFn: fn, compoundsWith: compoundsWith, compounds: compounds}
v.order = append(v.order, name)
}
func compoundsOrDefault(o vOpts) Compounds {
if o.hasCompounds {
return o.compounds
}
return CompoundsStyleRules
}
func (v *Variants) static(name string, fn variantApplyFn, o vOpts) {
v.set(name, varStatic, fn, CompoundsNever, compoundsOrDefault(o), o)
}
func (v *Variants) functional(name string, fn variantApplyFn, o vOpts) {
v.set(name, varFunctional, fn, CompoundsNever, compoundsOrDefault(o), o)
}
func (v *Variants) compound(name string, compoundsWith Compounds, fn variantApplyFn, o vOpts) {
v.set(name, varCompound, fn, compoundsWith, compoundsOrDefault(o), o)
}
func (v *Variants) group(fn func(), compareFn func(a, z *Variant) int) {
o := v.nextOrder()
v.groupOrder = &o
if compareFn != nil {
v.compareFns[o] = compareFn
}
fn()
v.groupOrder = nil
}
func (v *Variants) has(name string) bool { _, ok := v.variants[name]; return ok }
func (v *Variants) get(name string) *variantInfo { return v.variants[name] }
func (v *Variants) kind(name string) variantKind {
if info, ok := v.variants[name]; ok {
return info.kind
}
return varStatic
}
func (v *Variants) keys() []string { return v.order }
// parseCustomVariant reads an @custom-variant at-rule into a name and the AST body that
// fromAst expects (a body whose rules contain an @slot where the utility goes).
//
// Two forms, both from Tailwind:
//
// @custom-variant dark (&:where(.dark, .dark *)); // shorthand
//
// @custom-variant dark { // block, explicit slot
// &:where(.dark, .dark *) { @slot; }
// }
//
// In the shorthand, a parenthesised selector starting with '@' is an at-rule
// (`@custom-variant any-hover (@media (any-hover: hover))`), and anything else is a
// selector. Several may be given, comma-separated at the top level.
func parseCustomVariant(node *AstNode) (name string, body []*AstNode, ok bool) {
params := strings.TrimSpace(node.Params)
if params == "" {
return "", nil, false
}
// The name is the first token; whatever follows is the shorthand's parenthesised part.
i := strings.IndexAny(params, " \t(")
if i < 0 {
// No shorthand: it must be the block form, which carries its own @slot.
if len(node.Nodes) == 0 {
return "", nil, false
}
return params, node.Nodes, true
}
name = strings.TrimSpace(params[:i])
rest := strings.TrimSpace(params[i:])
if rest == "" {
if len(node.Nodes) == 0 {
return "", nil, false
}
return name, node.Nodes, true
}
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
return "", nil, false
}
inner := strings.TrimSpace(rest[1 : len(rest)-1])
if inner == "" {
return "", nil, false
}
for _, sel := range splitTopLevel(inner, ',') {
sel = strings.TrimSpace(sel)
if sel == "" {
continue
}
slot := atRule("@slot", "")
if strings.HasPrefix(sel, "@") {
// "@media (any-hover: hover)" -> name "@media", params "(any-hover: hover)"
at, params, _ := strings.Cut(sel, " ")
body = append(body, atRule(at, strings.TrimSpace(params), slot))
continue
}
body = append(body, styleRule(sel, slot))
}
if len(body) == 0 {
return "", nil, false
}
return name, body, true
}
// splitTopLevel splits on sep, ignoring separators nested inside brackets — a selector
// list like `&:where(.dark, .dark *)` is ONE selector, and splitting it on its inner
// comma would produce two broken halves.
func splitTopLevel(s string, sep byte) []string {
var parts []string
depth := 0
start := 0
for i := 0; i < len(s); i++ {
switch s[i] {
case '(', '[':
depth++
case ')', ']':
depth--
case sep:
if depth == 0 {
parts = append(parts, s[start:i])
start = i + 1
}
}
}
return append(parts, s[start:])
}
func (v *Variants) compoundsWith(parent string, child *Variant) bool {
parentInfo, ok := v.variants[parent]
if !ok {
return false
}
var childCompounds Compounds
if child.Kind == varArbitrary {
childCompounds = compoundsForSelectors([]string{child.Selector})
} else {
ci, ok := v.variants[child.Root]
if !ok {
return false
}
childCompounds = ci.compounds
}
if parentInfo.kind != varCompound {
return false
}
if childCompounds == CompoundsNever {
return false
}
if parentInfo.compoundsWith == CompoundsNever {
return false
}
if parentInfo.compoundsWith&childCompounds == 0 {
return false
}
return true
}
func (v *Variants) compare(a, z *Variant) int {
if a == z {
return 0
}
if a == nil {
return -1
}
if z == nil {
return 1
}
if a.Kind == varArbitrary && z.Kind == varArbitrary {
if a.Selector < z.Selector {
return -1
}
return 1
} else if a.Kind == varArbitrary {
return 1
} else if z.Kind == varArbitrary {
return -1
}
aOrder := v.variants[a.Root].order
zOrder := v.variants[z.Root].order
if aOrder != zOrder {
return aOrder - zOrder
}
if a.Kind == varCompound && z.Kind == varCompound {
order := v.compare(a.Variant, z.Variant)
if order != 0 {
return order
}
if a.Modifier != nil && z.Modifier != nil {
if a.Modifier.Value < z.Modifier.Value {
return -1
}
return 1
} else if a.Modifier != nil {
return 1
} else if z.Modifier != nil {
return -1
}
return 0
}
if fn, ok := v.compareFns[aOrder]; ok {
return fn(a, z)
}
if a.Root != z.Root {
if a.Root < z.Root {
return -1
}
return 1
}
aValue := a.Value
zValue := z.Value
if aValue == nil {
return -1
}
if zValue == nil {
return 1
}
if aValue.Kind == vvArbitrary && zValue.Kind != vvArbitrary {
return 1
}
if aValue.Kind != vvArbitrary && zValue.Kind == vvArbitrary {
return -1
}
if aValue.Value < zValue.Value {
return -1
}
return 1
}
// fromAst registers a variant whose body comes from CSS (@custom-variant).
func (v *Variants) fromAst(name string, ast []*AstNode, ds *DesignSystem) {
var selectors []string
usesAtVariant := false
astCopy := ast
walkAst(&astCopy, func(node *AstNode, _ *VisitContext) WalkResult {
if node.Kind == nRule {
selectors = append(selectors, node.Selector)
} else if node.Kind == nAtRule && node.Name == "@variant" {
usesAtVariant = true
} else if node.Kind == nAtRule && node.Name != "@slot" {
selectors = append(selectors, node.Name+" "+node.Params)
}
return WContinue
})
v.static(name, func(r *AstNode, _ *Variant) bool {
body := cloneAstNodes(ast)
if usesAtVariant {
substituteAtVariant(body, ds)
}
substituteAtSlot(body, r.Nodes)
r.Nodes = body
return true
}, vOpts{compounds: compoundsForSelectors(selectors), hasCompounds: true})
}
func compoundsForSelectors(selectors []string) Compounds {
compounds := CompoundsNever
for _, sel := range selectors {
if len(sel) > 0 && sel[0] == '@' {
if !strings.HasPrefix(sel, "@media") && !strings.HasPrefix(sel, "@supports") && !strings.HasPrefix(sel, "@container") {
return CompoundsNever
}
compounds |= CompoundsAtRules
continue
}
if strings.Contains(sel, "::") {
return CompoundsNever
}
compounds |= CompoundsStyleRules
}
return compounds
}
func addStaticVariant(variants *Variants, name string, selectors []string, o vOpts) {
if !o.hasCompounds {
o.compounds = compoundsForSelectors(selectors)
o.hasCompounds = true
}
sels := selectors
variants.static(name, func(r *AstNode, _ *Variant) bool {
orig := r.Nodes
newNodes := make([]*AstNode, len(sels))
for i, sel := range sels {
newNodes[i] = rule(sel, orig...)
}
r.Nodes = newNodes
return true
}, o)
}
func createVariants(theme *Theme) *Variants {
variants := NewVariants()
addStaticVariant(variants, "*", []string{":is(& > *)"}, vOpts{compounds: CompoundsNever, hasCompounds: true})
addStaticVariant(variants, "**", []string{":is(& *)"}, vOpts{compounds: CompoundsNever, hasCompounds: true})
registerCompoundVariants(variants, theme)
registerPseudoVariants(variants)
registerFunctionalVariants(variants)
registerBreakpointVariants(variants, theme)
registerMediaVariants(variants)
return variants
}
// ---- helpers shared by the variant catalog ------------------------------
func negateSelector(selector string) (string, bool) {
if strings.Contains(selector, "::") {
return "", false
}
parts := segment(selector, ",")
for i, sel := range parts {
parts[i] = strings.ReplaceAll(sel, "&", "*")
}
return "&:not(" + strings.Join(parts, ", ") + ")", true
}
var conditionalRules = []string{"@media", "@supports", "@container"}
func negateConditions(ruleName string, conditions []string) []string {
out := make([]string, len(conditions))
for i, condition := range conditions {
if ruleName == "@container" {
ast := valueParse(strings.TrimSpace(condition))
switch {
case len(ast) >= 1 && isValueFn(ast[0]):
out[i] = "not " + condition
case len(ast) >= 3 && isValueWordVal(ast[0], "not") && isValueFn(ast[2]):
ast = ast[2:]
out[i] = valueToCss(ast)
case len(ast) >= 5 && isValueWord(ast[0]) && isValueWordVal(ast[2], "not") && isValueFn(ast[4]):
ast = append(ast[:2], ast[4:]...)
out[i] = valueToCss(ast)
case len(ast) >= 3 && isValueWord(ast[0]) && !isValueWordVal(ast[0], "not") && isValueFn(ast[2]):
rest := append([]ValueNode{&ValueSeparator{Value: " "}, &ValueWord{Value: "not"}}, ast[1:]...)
ast = append([]ValueNode{ast[0]}, rest...)
out[i] = valueToCss(ast)
default:
out[i] = "not " + condition
}
} else {
condition = strings.TrimSpace(condition)
parts := segment(condition, " ")
if parts[0] == "not" {
out[i] = strings.Join(parts[1:], " ")
} else {
out[i] = "not " + condition
}
}
}
return out
}
func isValueFn(n ValueNode) bool { _, ok := n.(*ValueFunction); return ok }
func isValueWord(n ValueNode) bool { _, ok := n.(*ValueWord); return ok }
func isValueWordVal(n ValueNode, val string) bool {
w, ok := n.(*ValueWord)
return ok && w.Value == val
}
func negateAtRule(node *AstNode) *AstNode {
for _, ruleName := range conditionalRules {
if ruleName != node.Name {
continue
}
conditions := segment(node.Params, ",")
if len(conditions) > 1 {
return nil
}
conditions = negateConditions(node.Name, conditions)
return atRule(node.Name, strings.Join(conditions, ", "))
}
return nil
}
var reSupportsFn = regexp.MustCompile(`^[\w-]*\s*\(`)
var reSupportsBool = regexp.MustCompile(`\b(and|or|not)\b`)
func quoteAttributeValue(input string) string {
if strings.Contains(input, "=") {
segs := segment(input, "=")
attribute := segs[0]
value := strings.TrimSpace(strings.Join(segs[1:], "="))
if len(value) > 0 && (value[0] == '\'' || value[0] == '"') {
return input
}
if len(value) > 1 {
tc := value[len(value)-1]
if value[len(value)-2] == ' ' && (tc == 'i' || tc == 'I' || tc == 's' || tc == 'S') {
return attribute + "=\"" + value[:len(value)-2] + "\" " + string(tc)
}
}
return attribute + "=\"" + value + "\""
}
return input
}
func substituteAtSlot(ast []*AstNode, nodes []*AstNode) {
astCopy := ast
walkAst(&astCopy, func(node *AstNode, _ *VisitContext) WalkResult {
if node.Kind == nAtRule && node.Name == "@slot" {
return WReplaceSkip(nodes...)
}
if node.Kind == nAtRule && (node.Name == "@keyframes" || node.Name == "@property") {
*node = *atRoot([]*AstNode{atRule(node.Name, node.Params, node.Nodes...)})
return WSkip
}
return WContinue
})
}
func substituteAtVariant(ast []*AstNode, ds *DesignSystem) {
astCopy := ast
walkAst(&astCopy, func(variantNode *AstNode, _ *VisitContext) WalkResult {
if variantNode.Kind != nAtRule || variantNode.Name != "@variant" {
return WContinue
}
var nodes []*AstNode
compoundVariants := segment(variantNode.Params, ",")
for idx, compoundVariant := range compoundVariants {
var childNodes []*AstNode
if idx == len(compoundVariants)-1 {
childNodes = variantNode.Nodes
} else {
childNodes = cloneAstNodes(variantNode.Nodes)
}
node := styleRule("&", childNodes...)
stacked := segment(compoundVariant, ":")
for i := len(stacked) - 1; i >= 0; i-- {
name := strings.TrimSpace(stacked[i])
if name == "" {
return WContinue
}
variantAst := ds.parseVariant(name)
if variantAst == nil {
return WContinue
}
if !applyVariant(node, variantAst, ds.variants, 0) {
return WContinue
}
}
nodes = append(nodes, node)
}
return WReplace(nodes...)
})
}
func registerCompoundVariants(variants *Variants, theme *Theme) {
prefixDot := func(base string) string {
if theme.Prefix != "" {
return ":where(." + theme.Prefix + "\\:" + base
}
return ":where(." + base
}
variants.compound("not", CompoundsStyleRules|CompoundsAtRules, func(ruleNode *AstNode, variant *Variant) bool {
if variant.Variant.Kind == varArbitrary && variant.Variant.Relative {
return false
}
if variant.Modifier != nil {
return false
}
didApply := false
list := []*AstNode{ruleNode}
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
if node.Kind != nRule && node.Kind != nAtRule {
return WContinue
}
if len(node.Nodes) > 0 {
return WContinue
}
var atRules, styleRules []*AstNode
path := ctx.Path()
path = append(path, node)
for _, p := range path {
if p.Kind == nAtRule {
atRules = append(atRules, p)
} else if p.Kind == nRule {
styleRules = append(styleRules, p)
}
}
if len(atRules) > 1 {
return WStop
}
if len(styleRules) > 1 {
return WStop
}
var rules []*AstNode
for _, sr := range styleRules {
sel, ok := negateSelector(sr.Selector)
if !ok {
didApply = false
return WStop
}
rules = append(rules, styleRule(sel))
}
for _, ar := range atRules {
neg := negateAtRule(ar)
if neg == nil {
didApply = false
return WStop
}
rules = append(rules, neg)
}
*ruleNode = *styleRule("&", rules...)
didApply = true
return WSkip
})
if ruleNode.Kind == nRule && ruleNode.Selector == "&" && len(ruleNode.Nodes) == 1 {
*ruleNode = *ruleNode.Nodes[0]
}
return didApply
}, vOpts{})
groupPeer := func(name, suffix string) {
variants.compound(name, CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
if variant.Variant.Kind == varArbitrary && variant.Variant.Relative {
return false
}
var variantSelector string
if variant.Modifier != nil {
variantSelector = prefixDot(name+"\\/"+variant.Modifier.Value) + ")"
} else {
variantSelector = prefixDot(name) + ")"
}
didApply := false
list := []*AstNode{ruleNode}
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
if node.Kind != nRule {
return WContinue
}
for _, parent := range ctx.Path() {
if parent.Kind != nRule {
continue
}
didApply = false
return WStop
}
selector := strings.ReplaceAll(node.Selector, "&", variantSelector)
if len(segment(selector, ",")) > 1 {
selector = ":is(" + selector + ")"
}
node.Selector = "&:is(" + selector + suffix + ")"
didApply = true
return WContinue
})
return didApply
}, vOpts{})
}
groupPeer("group", " *")
groupPeer("peer", " ~ *")
variants.compound("in", CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
if variant.Modifier != nil {
return false
}
didApply := false
list := []*AstNode{ruleNode}
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
if node.Kind != nRule {
return WContinue
}
for _, parent := range ctx.Path() {
if parent.Kind != nRule {
continue
}
didApply = false
return WStop
}
node.Selector = ":where(" + strings.ReplaceAll(node.Selector, "&", "*") + ") &"
didApply = true
return WContinue
})
return didApply
}, vOpts{})
variants.compound("has", CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
if variant.Modifier != nil {
return false
}
didApply := false
list := []*AstNode{ruleNode}
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
if node.Kind != nRule {
return WContinue
}
for _, parent := range ctx.Path() {
if parent.Kind != nRule {
continue
}
didApply = false
return WStop
}
node.Selector = "&:has(" + strings.ReplaceAll(node.Selector, "&", "*") + ")"
didApply = true
return WContinue
})
return didApply
}, vOpts{})
}
func registerPseudoVariants(variants *Variants) {
sv := func(name string, selectors ...string) { addStaticVariant(variants, name, selectors, vOpts{}) }
sv("first-letter", "&::first-letter")
sv("first-line", "&::first-line")
sv("marker", "& *::marker", "&::marker", "& *::-webkit-details-marker", "&::-webkit-details-marker")
sv("selection", "& *::selection", "&::selection")
sv("file", "&::file-selector-button")
sv("placeholder", "&::placeholder")
sv("backdrop", "&::backdrop")
sv("details-content", "&::details-content")
contentProps := func() *AstNode {
return atRoot([]*AstNode{atRule("@property", "--tw-content",
decl("syntax", `"*"`), decl("initial-value", `""`), decl("inherits", "false"))})
}
pseudoContent := func(name, sel string) {
variants.static(name, func(v *AstNode, _ *Variant) bool {
inner := append([]*AstNode{contentProps(), decl("content", "var(--tw-content)")}, v.Nodes...)
v.Nodes = []*AstNode{styleRule(sel, inner...)}
return true
}, vOpts{compounds: CompoundsNever, hasCompounds: true})
}
pseudoContent("before", "&::before")
pseudoContent("after", "&::after")
sv("first", "&:first-child")
sv("last", "&:last-child")
sv("only", "&:only-child")
sv("odd", "&:nth-child(odd)")
sv("even", "&:nth-child(even)")
sv("first-of-type", "&:first-of-type")
sv("last-of-type", "&:last-of-type")
sv("only-of-type", "&:only-of-type")
sv("visited", "&:visited")
sv("target", "&:target")
sv("open", "&:is([open], :popover-open, :open)")
sv("default", "&:default")
sv("checked", "&:checked")
sv("indeterminate", "&:indeterminate")
sv("placeholder-shown", "&:placeholder-shown")
sv("autofill", "&:autofill")
sv("optional", "&:optional")
sv("required", "&:required")
sv("valid", "&:valid")
sv("invalid", "&:invalid")
sv("user-valid", "&:user-valid")
sv("user-invalid", "&:user-invalid")
sv("in-range", "&:in-range")
sv("out-of-range", "&:out-of-range")
sv("read-only", "&:read-only")
sv("empty", "&:empty")
sv("focus-within", "&:focus-within")
variants.static("hover", func(r *AstNode, _ *Variant) bool {
r.Nodes = []*AstNode{styleRule("&:hover", atRule("@media", "(hover: hover)", r.Nodes...))}
return true
}, vOpts{})
sv("focus", "&:focus")
sv("focus-visible", "&:focus-visible")
sv("active", "&:active")
sv("enabled", "&:enabled")
sv("disabled", "&:disabled")
sv("inert", "&:is([inert], [inert] *)")
}
func registerFunctionalVariants(variants *Variants) {
variants.functional("aria", func(r *AstNode, variant *Variant) bool {
if variant.Value == nil || variant.Modifier != nil {
return false
}
if variant.Value.Kind == vvArbitrary {
r.Nodes = []*AstNode{styleRule("&[aria-"+quoteAttributeValue(variant.Value.Value)+"]", r.Nodes...)}
} else {
r.Nodes = []*AstNode{styleRule("&[aria-"+variant.Value.Value+"=\"true\"]", r.Nodes...)}
}
return true
}, vOpts{})
variants.functional("data", func(r *AstNode, variant *Variant) bool {
if variant.Value == nil || variant.Modifier != nil {
return false
}
r.Nodes = []*AstNode{styleRule("&[data-"+quoteAttributeValue(variant.Value.Value)+"]", r.Nodes...)}
return true
}, vOpts{})
nthVariant := func(name, pseudo string) {
variants.functional(name, func(r *AstNode, variant *Variant) bool {
if variant.Value == nil || variant.Modifier != nil {
return false
}
if variant.Value.Kind == vvNamed && !isPositiveInteger(variant.Value.Value) {
return false
}
r.Nodes = []*AstNode{styleRule("&:"+pseudo+"("+variant.Value.Value+")", r.Nodes...)}
return true
}, vOpts{})
}
nthVariant("nth", "nth-child")
nthVariant("nth-last", "nth-last-child")
nthVariant("nth-of-type", "nth-of-type")
nthVariant("nth-last-of-type", "nth-last-of-type")
variants.functional("supports", func(r *AstNode, variant *Variant) bool {
if variant.Value == nil || variant.Modifier != nil {
return false
}
value := variant.Value.Value
if value == "" {
return false
}
if reSupportsFn.MatchString(value) {
query := reSupportsBool.ReplaceAllString(value, " $1 ")
r.Nodes = []*AstNode{atRule("@supports", query, r.Nodes...)}
return true
}
if !strings.Contains(value, ":") {
value = value + ": var(--tw)"
}
if value[0] != '(' || value[len(value)-1] != ')' {
value = "(" + value + ")"
}
r.Nodes = []*AstNode{atRule("@supports", value, r.Nodes...)}
return true
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}
func registerBreakpointVariants(variants *Variants, theme *Theme) {
compareBP := func(a, z *Variant, direction string, lookup func(*Variant) (string, bool)) int {
if a == z {
return 0
}
av, aok := lookup(a)
if !aok {
if direction == "asc" {
return -1
}
return 1
}
zv, zok := lookup(z)
if !zok {
if direction == "asc" {
return 1
}
return -1
}
return compareBreakpoints(av, zv, direction)
}
breakpoints := theme.namespace("--breakpoint")
resolvedBreakpoints := func(variant *Variant) (string, bool) {
switch variant.Kind {
case varStatic:
return theme.resolveValue(sptr(variant.Root), []string{"--breakpoint"})
case varFunctional:
if variant.Value == nil || variant.Modifier != nil {
return "", false
}
var value string
var ok bool
if variant.Value.Kind == vvArbitrary {
value, ok = variant.Value.Value, true
} else {
value, ok = theme.resolveValue(sptr(variant.Value.Value), []string{"--breakpoint"})
}
if !ok || value == "" || strings.Contains(value, "var(") {
return "", false
}
return value, true
}
return "", false
}
variants.group(func() {
variants.functional("max", func(r *AstNode, variant *Variant) bool {
if variant.Modifier != nil {
return false
}
value, ok := resolvedBreakpoints(variant)
if !ok {
return false
}
r.Nodes = []*AstNode{atRule("@media", "(width < "+value+")", r.Nodes...)}
return true
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}, func(a, z *Variant) int { return compareBP(a, z, "desc", resolvedBreakpoints) })
variants.group(func() {
for _, key := range breakpoints.Keys() {
value, _ := breakpoints.Get(key)
val := value
variants.static(key, func(r *AstNode, _ *Variant) bool {
r.Nodes = []*AstNode{atRule("@media", "(width >= "+val+")", r.Nodes...)}
return true
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}
variants.functional("min", func(r *AstNode, variant *Variant) bool {
if variant.Modifier != nil {
return false
}
value, ok := resolvedBreakpoints(variant)
if !ok {
return false
}
r.Nodes = []*AstNode{atRule("@media", "(width >= "+value+")", r.Nodes...)}
return true
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}, func(a, z *Variant) int { return compareBP(a, z, "asc", resolvedBreakpoints) })
resolvedWidths := func(variant *Variant) (string, bool) {
if variant.Kind == varFunctional {
if variant.Value == nil {
return "", false
}
var value string
var ok bool
if variant.Value.Kind == vvArbitrary {
value, ok = variant.Value.Value, true
} else {
value, ok = theme.resolveValue(sptr(variant.Value.Value), []string{"--container"})
}
if !ok || value == "" || strings.Contains(value, "var(") {
return "", false
}
return value, true
}
return "", false
}
variants.group(func() {
variants.functional("@max", func(r *AstNode, variant *Variant) bool {
value, ok := resolvedWidths(variant)
if !ok {
return false
}
params := "(width < " + value + ")"
if variant.Modifier != nil {
params = variant.Modifier.Value + " " + params
}
r.Nodes = []*AstNode{atRule("@container", params, r.Nodes...)}
return true
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}, func(a, z *Variant) int { return compareBP(a, z, "desc", resolvedWidths) })
variants.group(func() {
atFn := func(r *AstNode, variant *Variant) bool {
value, ok := resolvedWidths(variant)
if !ok {
return false
}
params := "(width >= " + value + ")"
if variant.Modifier != nil {
params = variant.Modifier.Value + " " + params
}
r.Nodes = []*AstNode{atRule("@container", params, r.Nodes...)}
return true
}
variants.functional("@", atFn, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
variants.functional("@min", atFn, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
}, func(a, z *Variant) int { return compareBP(a, z, "asc", resolvedWidths) })
}
func registerMediaVariants(variants *Variants) {
sv := func(name string, selectors ...string) { addStaticVariant(variants, name, selectors, vOpts{}) }
sv("motion-safe", "@media (prefers-reduced-motion: no-preference)")
sv("motion-reduce", "@media (prefers-reduced-motion: reduce)")
sv("contrast-more", "@media (prefers-contrast: more)")
sv("contrast-less", "@media (prefers-contrast: less)")
sv("portrait", "@media (orientation: portrait)")
sv("landscape", "@media (orientation: landscape)")
sv("ltr", `&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)`)
sv("rtl", `&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)`)
sv("dark", "@media (prefers-color-scheme: dark)")
sv("starting", "@starting-style")
sv("print", "@media print")
sv("forced-colors", "@media (forced-colors: active)")
sv("inverted-colors", "@media (inverted-colors: inverted)")
sv("pointer-none", "@media (pointer: none)")
sv("pointer-coarse", "@media (pointer: coarse)")
sv("pointer-fine", "@media (pointer: fine)")
sv("any-pointer-none", "@media (any-pointer: none)")
sv("any-pointer-coarse", "@media (any-pointer: coarse)")
sv("any-pointer-fine", "@media (any-pointer: fine)")
sv("noscript", "@media (scripting: none)")
}
// Port of packages/tailwindcss/src/walk.ts
//
// Depth-first AST traversal with enter/exit hooks that may continue, skip
// children, stop, or replace the current node.
type walkKind int
const (
wkContinue walkKind = iota
wkSkip
wkStop
wkReplace
wkReplaceSkip
wkReplaceStop
)
type WalkResult struct {
kind walkKind
nodes []*AstNode
}
var (
WContinue = WalkResult{kind: wkContinue}
WSkip = WalkResult{kind: wkSkip}
WStop = WalkResult{kind: wkStop}
)
func WReplace(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplace, nodes: nodes} }
func WReplaceSkip(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplaceSkip, nodes: nodes} }
func WReplaceStop(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplaceStop, nodes: nodes} }
type VisitContext struct {
Parent *AstNode
Depth int
Index int
Siblings []*AstNode
ancestor []*AstNode
}
func (c *VisitContext) Path() []*AstNode {
return append([]*AstNode{}, c.ancestor...)
}
func spliceNodes(nodes *[]*AstNode, idx int, repl []*AstNode) {
s := *nodes
out := make([]*AstNode, 0, len(s)-1+len(repl))
out = append(out, s[:idx]...)
out = append(out, repl...)
out = append(out, s[idx+1:]...)
*nodes = out
}
type walkFn func(node *AstNode, ctx *VisitContext) WalkResult
func walkAst(nodes *[]*AstNode, enter walkFn) {
walkAstImpl(nodes, nil, 0, nil, enter, nil)
}
func walkAstEnterExit(nodes *[]*AstNode, enter, exit walkFn) {
walkAstImpl(nodes, nil, 0, nil, enter, exit)
}
func walkAstImpl(nodes *[]*AstNode, parent *AstNode, depth int, ancestors []*AstNode, enter, exit walkFn) bool {
i := 0
for i < len(*nodes) {
node := (*nodes)[i]
ctx := &VisitContext{Parent: parent, Depth: depth, Index: i, Siblings: *nodes, ancestor: ancestors}
res := WContinue
if enter != nil {
res = enter(node, ctx)
}
switch res.kind {
case wkStop:
return false
case wkReplaceStop:
spliceNodes(nodes, i, res.nodes)
return false
case wkReplace:
spliceNodes(nodes, i, res.nodes)
continue
case wkReplaceSkip:
spliceNodes(nodes, i, res.nodes)
i += len(res.nodes)
continue
case wkSkip:
case wkContinue:
if ch := nodeChildren(node); ch != nil && len(*ch) > 0 {
childAnc := append(append([]*AstNode{}, ancestors...), node)
if !walkAstImpl(ch, node, depth+1, childAnc, enter, exit) {
return false
}
}
}
if exit != nil {
res2 := exit(node, ctx)
switch res2.kind {
case wkStop:
return false
case wkReplaceStop:
spliceNodes(nodes, i, res2.nodes)
return false
case wkReplace, wkReplaceSkip:
spliceNodes(nodes, i, res2.nodes)
i += len(res2.nodes)
continue
}
}
i++
}
return true
}