package bundler // 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, ©Nodes, 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, ©Nodes, 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 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 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) default: passthrough = append(passthrough, node) } } } inAst, err := cssParse(input) if err != nil { return "", 0, err } processInput(inAst, baseDir) ds := buildDesignSystem(theme) // 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 } // 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('�') 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 "�" } if codePoint == 0x0000 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff) { return "�" } 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.Index(key[2:], "--") != -1 { 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", ""), property("--tw-border-spacing-y", "0", ""), }) } 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", ""), property("--tw-scrollbar-track", "#0000", ""), }) } 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", ""), property("--tw-gradient-via", "#0000", ""), property("--tw-gradient-to", "#0000", ""), property("--tw-gradient-stops", "", ""), property("--tw-gradient-via-stops", "", ""), property("--tw-gradient-from-position", "0%", ""), property("--tw-gradient-via-position", "50%", ""), property("--tw-gradient-to-position", "100%", ""), }) } 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%", ""), 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 `/` 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%", "")}) } 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%", ""), property("--tw-inset-shadow", nullShadow, ""), property("--tw-inset-shadow-color", "", ""), property("--tw-inset-shadow-alpha", "100%", ""), 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", ""), 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 } 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, theme) 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, theme *Theme) { 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 }