9702 lines
298 KiB
Go
9702 lines
298 KiB
Go
package tw
|
||
|
||
// Native Go Tailwind v4 compiler. A from-scratch, pure-Go implementation of the
|
||
// Tailwind v4 engine — CSS parser (AST), candidate scanner, utility/variant
|
||
// generation, design system + theme resolution, sorting, and preflight — with no
|
||
// Node, no goja, no official tailwindcss distribution. Consolidated here from the
|
||
// former cmd/bundle/tw_*.go engine. Entry points: twCompile (compile a config +
|
||
// candidates to CSS) and scanSources (scan source files for utility candidates).
|
||
|
||
import (
|
||
_ "embed"
|
||
"fmt"
|
||
"math"
|
||
"math/big"
|
||
"os"
|
||
"path/filepath"
|
||
"regexp"
|
||
"sort"
|
||
"strconv"
|
||
"strings"
|
||
)
|
||
|
||
// Port of packages/tailwindcss/src/ast.ts
|
||
//
|
||
// A single mutable node struct with a Kind discriminator (rather than one Go
|
||
// type per kind). This mirrors the upstream structurally-typed objects and is
|
||
// required because variants morph a node's kind in place (the equivalent of
|
||
// `Object.assign(node, styleRule(...))`), which is `*node = *other` here.
|
||
//
|
||
// Deviations (tracked follow-ups):
|
||
// @INCOMPLETE optimizeAst does not yet prune unused @theme variables/keyframes
|
||
// or emit color-mix()/@property browser fallbacks (Polyfills). -mta
|
||
|
||
type nodeKind int
|
||
|
||
const (
|
||
nRule nodeKind = iota
|
||
nAtRule
|
||
nDeclaration
|
||
nComment
|
||
nContext
|
||
nAtRoot
|
||
)
|
||
|
||
type AstNode struct {
|
||
Kind nodeKind
|
||
|
||
// rule
|
||
Selector string
|
||
|
||
// at-rule
|
||
Name string
|
||
Params string
|
||
|
||
// declaration
|
||
Property string
|
||
Value string
|
||
Important bool
|
||
Undefined bool // value is `undefined` upstream; dropped by optimizeAst
|
||
|
||
// context
|
||
Context map[string]string
|
||
|
||
// children: rule / at-rule / context / at-root
|
||
Nodes []*AstNode
|
||
}
|
||
|
||
// ---- factories ----------------------------------------------------------
|
||
|
||
func styleRule(selector string, nodes ...*AstNode) *AstNode {
|
||
return &AstNode{Kind: nRule, Selector: selector, Nodes: nodes}
|
||
}
|
||
|
||
func atRule(name, params string, nodes ...*AstNode) *AstNode {
|
||
return &AstNode{Kind: nAtRule, Name: name, Params: params, Nodes: nodes}
|
||
}
|
||
|
||
func rule(selector string, nodes ...*AstNode) *AstNode {
|
||
if len(selector) > 0 && selector[0] == '@' {
|
||
return parseAtRule(selector, nodes)
|
||
}
|
||
return styleRule(selector, nodes...)
|
||
}
|
||
|
||
func decl(property, value string) *AstNode {
|
||
return &AstNode{Kind: nDeclaration, Property: property, Value: value}
|
||
}
|
||
|
||
func declImportant(property, value string) *AstNode {
|
||
return &AstNode{Kind: nDeclaration, Property: property, Value: value, Important: true}
|
||
}
|
||
|
||
func comment(value string) *AstNode { return &AstNode{Kind: nComment, Value: value} }
|
||
|
||
func contextNode(ctx map[string]string, nodes []*AstNode) *AstNode {
|
||
return &AstNode{Kind: nContext, Context: ctx, Nodes: nodes}
|
||
}
|
||
|
||
func atRoot(nodes []*AstNode) *AstNode { return &AstNode{Kind: nAtRoot, Nodes: nodes} }
|
||
|
||
// nodeChildren returns a pointer to a node's child slice, or nil for leaves.
|
||
func nodeChildren(n *AstNode) *[]*AstNode {
|
||
switch n.Kind {
|
||
case nRule, nAtRule, nContext, nAtRoot:
|
||
return &n.Nodes
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// ---- clone --------------------------------------------------------------
|
||
|
||
func cloneAstNode(node *AstNode) *AstNode {
|
||
cp := *node
|
||
if node.Nodes != nil {
|
||
cp.Nodes = cloneAstNodes(node.Nodes)
|
||
}
|
||
if node.Context != nil {
|
||
m := make(map[string]string, len(node.Context))
|
||
for k, v := range node.Context {
|
||
m[k] = v
|
||
}
|
||
cp.Context = m
|
||
}
|
||
return &cp
|
||
}
|
||
|
||
func cloneAstNodes(nodes []*AstNode) []*AstNode {
|
||
out := make([]*AstNode, len(nodes))
|
||
for i, n := range nodes {
|
||
out[i] = cloneAstNode(n)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// ---- optimize -----------------------------------------------------------
|
||
|
||
func optimizeAst(ast []*AstNode, ds *DesignSystem) []*AstNode {
|
||
var atRoots []*AstNode
|
||
seenAtProperties := make(map[string]bool)
|
||
|
||
var transform func(node *AstNode, parent *[]*AstNode, ctx map[string]bool, depth int)
|
||
transform = func(node *AstNode, parent *[]*AstNode, ctx map[string]bool, depth int) {
|
||
switch node.Kind {
|
||
case nDeclaration:
|
||
if node.Property == "--tw-sort" || node.Undefined {
|
||
return
|
||
}
|
||
if ctx["theme"] && strings.HasPrefix(node.Property, "--") && node.Value == "initial" {
|
||
return
|
||
}
|
||
if ds != nil && strings.Contains(node.Value, "var(") {
|
||
if !(ctx["theme"] && strings.HasPrefix(node.Property, "--")) {
|
||
ds.trackUsedVariables(node.Value)
|
||
}
|
||
}
|
||
*parent = append(*parent, node)
|
||
|
||
case nRule:
|
||
var nodes []*AstNode
|
||
for _, child := range node.Nodes {
|
||
transform(child, &nodes, ctx, depth+1)
|
||
}
|
||
nodes = dedupeDeclarations(nodes)
|
||
if len(nodes) == 0 {
|
||
return
|
||
}
|
||
if node.Selector == "&" {
|
||
*parent = append(*parent, nodes...)
|
||
} else {
|
||
*parent = append(*parent, &AstNode{Kind: nRule, Selector: node.Selector, Nodes: nodes})
|
||
}
|
||
|
||
case nAtRule:
|
||
if node.Name == "@property" && depth == 0 {
|
||
if seenAtProperties[node.Params] {
|
||
return
|
||
}
|
||
seenAtProperties[node.Params] = true
|
||
var copyNodes []*AstNode
|
||
for _, child := range node.Nodes {
|
||
transform(child, ©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 wired (both the shorthand and block
|
||
// forms) — see parseCustomVariant. -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
|
||
var customVariants []*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)
|
||
case node.Kind == nAtRule && node.Name == "@custom-variant":
|
||
customVariants = append(customVariants, node)
|
||
default:
|
||
passthrough = append(passthrough, node)
|
||
}
|
||
}
|
||
}
|
||
|
||
inAst, err := cssParse(input)
|
||
if err != nil {
|
||
return "", 0, err
|
||
}
|
||
processInput(inAst, baseDir)
|
||
|
||
ds := buildDesignSystem(theme)
|
||
|
||
// Register @custom-variant blocks. This is how a project defines `dark:` as a CLASS
|
||
// toggle rather than a media query — the built-in dark variant follows the OS, which
|
||
// a site with a theme switch cannot use:
|
||
//
|
||
// @custom-variant dark (&:where(.dark, .dark *));
|
||
//
|
||
// Both of Tailwind's forms are accepted: the shorthand above, and the block form
|
||
// with an explicit @slot.
|
||
for _, cv := range customVariants {
|
||
if name, body, ok := parseCustomVariant(cv); ok {
|
||
ds.variants.fromAst(name, body, ds)
|
||
}
|
||
}
|
||
|
||
// Register @utility blocks as static utilities.
|
||
for _, u := range customUtilities {
|
||
name := strings.TrimSpace(u.Params)
|
||
var decls []*AstNode
|
||
for _, child := range u.Nodes {
|
||
if child.Kind == nDeclaration {
|
||
decls = append(decls, child)
|
||
}
|
||
}
|
||
captured := decls
|
||
ds.utilities.static(name, func(_ *Candidate) *utilResult { return uList(cloneAstNodes(captured)) })
|
||
}
|
||
|
||
astNodes, _ := compileCandidates(candidates, ds, nil, true)
|
||
|
||
// Assemble the output document.
|
||
var out []*AstNode
|
||
if hasUtilities {
|
||
out = append(out, atRule("@layer", "theme, base, components, utilities"))
|
||
}
|
||
|
||
var rootDecls []*AstNode
|
||
for _, key := range theme.order {
|
||
tv := theme.values[key]
|
||
if tv.options&(themeInline|themeReference) != 0 {
|
||
continue
|
||
}
|
||
val := tv.value
|
||
if reThemeFnInvocation.MatchString(val) {
|
||
if v, ok := substituteFunctionsInValue(val, decl(key, val), ds); ok {
|
||
val = v
|
||
}
|
||
}
|
||
rootDecls = append(rootDecls, decl(key, val))
|
||
}
|
||
if len(rootDecls) > 0 {
|
||
out = append(out, atRule("@layer", "theme", styleRule(":root, :host", rootDecls...)))
|
||
}
|
||
for _, kf := range keyframes {
|
||
out = append(out, kf)
|
||
}
|
||
|
||
if hasPreflight {
|
||
pfAst, e := cssParse(defaultPreflightCSS)
|
||
if e != nil {
|
||
return "", 0, e
|
||
}
|
||
// The preflight is written against Tailwind's compile-time CSS functions —
|
||
// `font-family: --theme(--default-font-family, …)` and five more like it. They
|
||
// have to be resolved here, exactly as the theme's own declarations are above.
|
||
// Left in, `--theme(…)` reaches the browser verbatim, which cannot parse it and
|
||
// so DROPS THE WHOLE DECLARATION: html ends up with no font-family at all and
|
||
// falls back to the browser default, and no @theme override of --font-sans can
|
||
// ever take effect.
|
||
substituteFunctions(pfAst, ds)
|
||
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('<27>')
|
||
continue
|
||
}
|
||
|
||
if (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||
|
||
codeUnit == 0x007f ||
|
||
(index == 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
|
||
(index == 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && first == 0x002d) {
|
||
// Escape as a code point.
|
||
b.WriteByte('\\')
|
||
b.WriteString(strconv.FormatInt(int64(codeUnit), 16))
|
||
b.WriteByte(' ')
|
||
continue
|
||
}
|
||
|
||
if codeUnit >= 0x0080 ||
|
||
codeUnit == 0x002d ||
|
||
codeUnit == 0x005f ||
|
||
(codeUnit >= 0x0030 && codeUnit <= 0x0039) ||
|
||
(codeUnit >= 0x0041 && codeUnit <= 0x005a) ||
|
||
(codeUnit >= 0x0061 && codeUnit <= 0x007a) {
|
||
b.WriteRune(codeUnit)
|
||
continue
|
||
}
|
||
|
||
// Otherwise, the escaped character.
|
||
b.WriteByte('\\')
|
||
b.WriteRune(codeUnit)
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
var reUnescape = regexp.MustCompile(`\\([0-9A-Fa-f]{1,6}[\t\n\f\r ]?|[\s\S])`)
|
||
|
||
func unescape(escaped string) string {
|
||
return reUnescape.ReplaceAllStringFunc(escaped, func(match string) string {
|
||
r := []rune(match)
|
||
if len(r) <= 2 {
|
||
return string(r[1])
|
||
}
|
||
codePoint, err := strconv.ParseInt(strings.TrimSpace(string(r[1:])), 16, 64)
|
||
if err != nil {
|
||
return "<22>"
|
||
}
|
||
if codePoint == 0x0000 || codePoint > 0x10ffff || (codePoint >= 0xd800 && codePoint <= 0xdfff) {
|
||
return "<22>"
|
||
}
|
||
return string(rune(codePoint))
|
||
})
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/infer-data-type.ts
|
||
//
|
||
// Data types recognised by inferDataType. Used by functional utilities to
|
||
// dispatch arbitrary values (e.g. text-[10px] is a length, text-[#fff] a color).
|
||
|
||
const (
|
||
dtColor = "color"
|
||
dtLength = "length"
|
||
dtPercentage = "percentage"
|
||
dtRatio = "ratio"
|
||
dtNumber = "number"
|
||
dtInteger = "integer"
|
||
dtURL = "url"
|
||
dtPosition = "position"
|
||
dtBgSize = "bg-size"
|
||
dtLineWidth = "line-width"
|
||
dtImage = "image"
|
||
dtFamilyName = "family-name"
|
||
dtGenericName = "generic-name"
|
||
dtAbsoluteSize = "absolute-size"
|
||
dtRelativeSize = "relative-size"
|
||
dtAngle = "angle"
|
||
dtVector = "vector"
|
||
)
|
||
|
||
var dataTypeChecks = map[string]func(string) bool{
|
||
dtColor: isColor,
|
||
dtLength: isLength,
|
||
dtPercentage: isPercentage,
|
||
dtRatio: isFraction,
|
||
dtNumber: isNumber,
|
||
dtInteger: isPositiveInteger,
|
||
dtURL: isURL,
|
||
dtPosition: isBackgroundPosition,
|
||
dtBgSize: isBackgroundSize,
|
||
dtLineWidth: isLineWidth,
|
||
dtImage: isImage,
|
||
dtFamilyName: isFamilyName,
|
||
dtGenericName: isGenericName,
|
||
dtAbsoluteSize: isAbsoluteSize,
|
||
dtRelativeSize: isRelativeSize,
|
||
dtAngle: isAngle,
|
||
dtVector: isVector,
|
||
}
|
||
|
||
// inferDataType returns the first matching data type from types, or "" (null).
|
||
func inferDataType(value string, types []string) string {
|
||
if strings.HasPrefix(value, "var(") {
|
||
return ""
|
||
}
|
||
for _, t := range types {
|
||
if check, ok := dataTypeChecks[t]; ok && check(value) {
|
||
return t
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// ---- individual checks --------------------------------------------------
|
||
|
||
var reIsURL = regexp.MustCompile(`^url\(.*\)$`)
|
||
|
||
func isURL(value string) bool { return reIsURL.MatchString(value) }
|
||
|
||
func isLineWidth(value string) bool {
|
||
for _, v := range segment(value, " ") {
|
||
if !(isLength(v) || isNumber(v) || v == "thin" || v == "medium" || v == "thick") {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
var (
|
||
reIsImageFn = regexp.MustCompile(`^(?:element|image|cross-fade|image-set)\(`)
|
||
reIsGradientFn = regexp.MustCompile(`^(repeating-)?(conic|linear|radial)-gradient\(`)
|
||
)
|
||
|
||
func isImage(value string) bool {
|
||
count := 0
|
||
for _, part := range segment(value, ",") {
|
||
if strings.HasPrefix(part, "var(") {
|
||
continue
|
||
}
|
||
if isURL(part) || reIsGradientFn.MatchString(part) || reIsImageFn.MatchString(part) {
|
||
count++
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return count > 0
|
||
}
|
||
|
||
func isGenericName(value string) bool {
|
||
switch value {
|
||
case "serif", "sans-serif", "monospace", "cursive", "fantasy", "system-ui",
|
||
"ui-serif", "ui-sans-serif", "ui-monospace", "ui-rounded", "math", "emoji", "fangsong":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isFamilyName(value string) bool {
|
||
count := 0
|
||
for _, part := range segment(value, ",") {
|
||
if len(part) > 0 && part[0] >= '0' && part[0] <= '9' {
|
||
return false
|
||
}
|
||
if strings.HasPrefix(part, "var(") {
|
||
continue
|
||
}
|
||
count++
|
||
}
|
||
return count > 0
|
||
}
|
||
|
||
func isAbsoluteSize(value string) bool {
|
||
switch value {
|
||
case "xx-small", "x-small", "small", "medium", "large", "x-large", "xx-large", "xxx-large":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isRelativeSize(value string) bool {
|
||
return value == "larger" || value == "smaller"
|
||
}
|
||
|
||
const hasNumber = `[+-]?\d*\.?\d+(?:[eE][+-]?\d+)?`
|
||
|
||
var (
|
||
reIsNumber = regexp.MustCompile(`^` + hasNumber + `$`)
|
||
reIsPercentage = regexp.MustCompile(`^` + hasNumber + `%$`)
|
||
reIsFraction = regexp.MustCompile(`^` + hasNumber + `\s*/\s*` + hasNumber + `$`)
|
||
)
|
||
|
||
func isNumber(value string) bool { return reIsNumber.MatchString(value) || hasMathFn(value) }
|
||
func isPercentage(value string) bool { return reIsPercentage.MatchString(value) || hasMathFn(value) }
|
||
func isFraction(value string) bool { return reIsFraction.MatchString(value) || hasMathFn(value) }
|
||
|
||
var lengthUnits = []string{
|
||
"cm", "mm", "Q", "in", "pc", "pt", "px", "em", "ex", "ch", "rem", "lh", "rlh",
|
||
"vw", "vh", "vmin", "vmax", "vb", "vi", "svw", "svh", "lvw", "lvh", "dvw", "dvh",
|
||
"cqw", "cqh", "cqi", "cqb", "cqmin", "cqmax",
|
||
}
|
||
|
||
var (
|
||
reIsLength = regexp.MustCompile(`^` + hasNumber + `(` + strings.Join(lengthUnits, "|") + `)$`)
|
||
reIsLengthFn = regexp.MustCompile(`(?i)^(--spacing)\(`)
|
||
)
|
||
|
||
func isLength(value string) bool {
|
||
return reIsLength.MatchString(value) || reIsLengthFn.MatchString(value) || hasMathFn(value)
|
||
}
|
||
|
||
func isBackgroundPosition(value string) bool {
|
||
count := 0
|
||
for _, part := range segment(value, " ") {
|
||
switch part {
|
||
case "center", "top", "right", "bottom", "left":
|
||
count++
|
||
continue
|
||
}
|
||
if strings.HasPrefix(part, "var(") {
|
||
continue
|
||
}
|
||
if isLength(part) || isPercentage(part) {
|
||
count++
|
||
continue
|
||
}
|
||
return false
|
||
}
|
||
return count > 0
|
||
}
|
||
|
||
func isBackgroundSize(value string) bool {
|
||
count := 0
|
||
for _, size := range segment(value, ",") {
|
||
if size == "cover" || size == "contain" {
|
||
count++
|
||
continue
|
||
}
|
||
values := segment(size, " ")
|
||
if len(values) != 1 && len(values) != 2 {
|
||
return false
|
||
}
|
||
ok := true
|
||
for _, v := range values {
|
||
if !(v == "auto" || isLength(v) || isPercentage(v)) {
|
||
ok = false
|
||
break
|
||
}
|
||
}
|
||
if ok {
|
||
count++
|
||
}
|
||
}
|
||
return count > 0
|
||
}
|
||
|
||
var angleUnits = []string{"deg", "rad", "grad", "turn"}
|
||
|
||
var reIsAngle = regexp.MustCompile(`^` + hasNumber + `(` + strings.Join(angleUnits, "|") + `)$`)
|
||
|
||
func isAngle(value string) bool { return reIsAngle.MatchString(value) }
|
||
|
||
var reIsVector = regexp.MustCompile(`^` + hasNumber + ` +` + hasNumber + ` +` + hasNumber + `$`)
|
||
|
||
func isVector(value string) bool { return reIsVector.MatchString(value) }
|
||
|
||
// ---- numeric predicates -------------------------------------------------
|
||
|
||
func jsParseNumber(s string) (float64, bool) {
|
||
f, err := strconv.ParseFloat(s, 64)
|
||
if err != nil || math.IsInf(f, 0) || math.IsNaN(f) {
|
||
return 0, false
|
||
}
|
||
return f, true
|
||
}
|
||
|
||
// jsNumberToString mirrors JS `String(Number(x))` for the small decimal values
|
||
// these predicates see (multiples of 0.25, small integers).
|
||
func jsNumberToString(f float64) string {
|
||
return strconv.FormatFloat(f, 'g', -1, 64)
|
||
}
|
||
|
||
func isPositiveInteger(value string) bool {
|
||
f, ok := jsParseNumber(value)
|
||
if !ok {
|
||
return false
|
||
}
|
||
return f == math.Trunc(f) && f >= 0 && jsNumberToString(f) == value
|
||
}
|
||
|
||
func isStrictPositiveInteger(value string) bool {
|
||
f, ok := jsParseNumber(value)
|
||
if !ok {
|
||
return false
|
||
}
|
||
return f == math.Trunc(f) && f > 0 && jsNumberToString(f) == value
|
||
}
|
||
|
||
func isValidSpacingMultiplier(value string) bool { return isMultipleOf(value, 0.25) }
|
||
func isValidOpacityValue(value string) bool { return isMultipleOf(value, 0.25) }
|
||
|
||
func isMultipleOf(value string, divisor float64) bool {
|
||
f, ok := jsParseNumber(value)
|
||
if !ok || f < 0 {
|
||
return false
|
||
}
|
||
q := f / divisor
|
||
if math.Abs(q-math.Round(q)) > 1e-9 {
|
||
return false
|
||
}
|
||
return jsNumberToString(f) == value
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/is-color.ts
|
||
|
||
var twNamedColors = func() map[string]bool {
|
||
names := []string{
|
||
// CSS Level 1
|
||
"black", "silver", "gray", "white", "maroon", "red", "purple", "fuchsia",
|
||
"green", "lime", "olive", "yellow", "navy", "blue", "teal", "aqua",
|
||
// CSS Level 2/3
|
||
"aliceblue", "antiquewhite", "aqua", "aquamarine", "azure", "beige", "bisque",
|
||
"black", "blanchedalmond", "blue", "blueviolet", "brown", "burlywood", "cadetblue",
|
||
"chartreuse", "chocolate", "coral", "cornflowerblue", "cornsilk", "crimson", "cyan",
|
||
"darkblue", "darkcyan", "darkgoldenrod", "darkgray", "darkgreen", "darkgrey", "darkkhaki",
|
||
"darkmagenta", "darkolivegreen", "darkorange", "darkorchid", "darkred", "darksalmon",
|
||
"darkseagreen", "darkslateblue", "darkslategray", "darkslategrey", "darkturquoise",
|
||
"darkviolet", "deeppink", "deepskyblue", "dimgray", "dimgrey", "dodgerblue", "firebrick",
|
||
"floralwhite", "forestgreen", "fuchsia", "gainsboro", "ghostwhite", "gold", "goldenrod",
|
||
"gray", "green", "greenyellow", "grey", "honeydew", "hotpink", "indianred", "indigo",
|
||
"ivory", "khaki", "lavender", "lavenderblush", "lawngreen", "lemonchiffon", "lightblue",
|
||
"lightcoral", "lightcyan", "lightgoldenrodyellow", "lightgray", "lightgreen", "lightgrey",
|
||
"lightpink", "lightsalmon", "lightseagreen", "lightskyblue", "lightslategray",
|
||
"lightslategrey", "lightsteelblue", "lightyellow", "lime", "limegreen", "linen", "magenta",
|
||
"maroon", "mediumaquamarine", "mediumblue", "mediumorchid", "mediumpurple", "mediumseagreen",
|
||
"mediumslateblue", "mediumspringgreen", "mediumturquoise", "mediumvioletred", "midnightblue",
|
||
"mintcream", "mistyrose", "moccasin", "navajowhite", "navy", "oldlace", "olive", "olivedrab",
|
||
"orange", "orangered", "orchid", "palegoldenrod", "palegreen", "paleturquoise",
|
||
"palevioletred", "papayawhip", "peachpuff", "peru", "pink", "plum", "powderblue", "purple",
|
||
"rebeccapurple", "red", "rosybrown", "royalblue", "saddlebrown", "salmon", "sandybrown",
|
||
"seagreen", "seashell", "sienna", "silver", "skyblue", "slateblue", "slategray", "slategrey",
|
||
"snow", "springgreen", "steelblue", "tan", "teal", "thistle", "tomato", "turquoise", "violet",
|
||
"wheat", "white", "whitesmoke", "yellow", "yellowgreen",
|
||
// Keywords
|
||
"transparent", "currentcolor",
|
||
// System colors
|
||
"canvas", "canvastext", "linktext", "visitedtext", "activetext", "buttonface", "buttontext",
|
||
"buttonborder", "field", "fieldtext", "highlight", "highlighttext", "selecteditem",
|
||
"selecteditemtext", "mark", "marktext", "graytext", "accentcolor", "accentcolortext",
|
||
}
|
||
m := make(map[string]bool, len(names))
|
||
for _, n := range names {
|
||
m[n] = true
|
||
}
|
||
return m
|
||
}()
|
||
|
||
var reIsColorFn = regexp.MustCompile(`(?i)^(rgba?|hsla?|hwb|color|(ok)?(lab|lch)|light-dark|color-mix|--alpha)\(`)
|
||
|
||
func isColor(value string) bool {
|
||
if len(value) > 0 && value[0] == '#' {
|
||
return true
|
||
}
|
||
return reIsColorFn.MatchString(value) || twNamedColors[strings.ToLower(value)]
|
||
}
|
||
|
||
func isNamedColor(value string) bool {
|
||
return twNamedColors[strings.ToLower(value)]
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/is-valid-arbitrary.ts
|
||
//
|
||
// An arbitrary value is valid when parens/brackets are balanced and there is no
|
||
// top-level `;`. Note `{` intentionally does not push the stack, so a candidate
|
||
// like `[&{color:red}]` is rejected.
|
||
func isValidArbitrary(input string) bool {
|
||
var stack []byte
|
||
for i := 0; i < len(input); i++ {
|
||
c := input[i]
|
||
switch c {
|
||
case '\\':
|
||
i++
|
||
case '\'', '"':
|
||
for i++; i < len(input); i++ {
|
||
nc := input[i]
|
||
if nc == '\\' {
|
||
i++
|
||
continue
|
||
}
|
||
if nc == c {
|
||
break
|
||
}
|
||
}
|
||
case '(':
|
||
stack = append(stack, ')')
|
||
case '[':
|
||
stack = append(stack, ']')
|
||
case ')', ']', '}':
|
||
if len(stack) == 0 {
|
||
return false
|
||
}
|
||
if c == stack[len(stack)-1] {
|
||
stack = stack[:len(stack)-1]
|
||
}
|
||
case ';':
|
||
if len(stack) == 0 {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/math-operators.ts
|
||
|
||
var mathFunctions = []string{
|
||
"calc", "min", "max", "clamp", "mod", "rem", "sin", "cos", "tan",
|
||
"asin", "acos", "atan", "atan2", "pow", "sqrt", "hypot", "log", "exp", "round",
|
||
}
|
||
|
||
func hasMathFn(input string) bool {
|
||
if !strings.Contains(input, "(") {
|
||
return false
|
||
}
|
||
for _, fn := range mathFunctions {
|
||
if strings.Contains(input, fn+"(") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func isMathFunctionName(fn string) bool {
|
||
for _, m := range mathFunctions {
|
||
if m == fn {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
const (
|
||
mLowerA = 0x61
|
||
mLowerZ = 0x7a
|
||
mUpperA = 0x41
|
||
mUpperZ = 0x5a
|
||
mLowerE = 0x65
|
||
mUpperE = 0x45
|
||
mZero = 0x30
|
||
mNine = 0x39
|
||
mAdd = '+'
|
||
mSub = '-'
|
||
mMul = '*'
|
||
mDiv = '/'
|
||
mLParen = '('
|
||
mRParen = ')'
|
||
mComma = ','
|
||
mSpace = ' '
|
||
mPct = '%'
|
||
)
|
||
|
||
func addWhitespaceAroundMathOperators(input string) string {
|
||
containsAny := false
|
||
for _, fn := range mathFunctions {
|
||
if strings.Contains(input, fn) {
|
||
containsAny = true
|
||
break
|
||
}
|
||
}
|
||
if !containsAny {
|
||
return input
|
||
}
|
||
|
||
var result []byte
|
||
var formattable []bool // stack; index 0 == top
|
||
|
||
valuePos := -1
|
||
lastValuePos := -1
|
||
|
||
for i := 0; i < len(input); i++ {
|
||
char := input[i]
|
||
|
||
// Track number-then-unit so we know it's a value, not a function call.
|
||
if char >= mZero && char <= mNine {
|
||
valuePos = i
|
||
} else if valuePos != -1 &&
|
||
(char == mPct || (char >= mLowerA && char <= mLowerZ) || (char >= mUpperA && char <= mUpperZ)) {
|
||
valuePos = i
|
||
} else {
|
||
lastValuePos = valuePos
|
||
valuePos = -1
|
||
}
|
||
|
||
switch {
|
||
case char == mLParen:
|
||
result = append(result, char)
|
||
// Scan backwards for the function name (lowercase alnum).
|
||
start := i
|
||
for j := i - 1; j >= 0; j-- {
|
||
inner := input[j]
|
||
if inner >= mZero && inner <= mNine {
|
||
start = j
|
||
} else if inner >= mLowerA && inner <= mLowerZ {
|
||
start = j
|
||
} else {
|
||
break
|
||
}
|
||
}
|
||
fn := input[start:i]
|
||
if isMathFunctionName(fn) {
|
||
formattable = append([]bool{true}, formattable...)
|
||
} else if len(formattable) > 0 && formattable[0] && fn == "" {
|
||
formattable = append([]bool{true}, formattable...)
|
||
} else {
|
||
formattable = append([]bool{false}, formattable...)
|
||
}
|
||
|
||
case char == mRParen:
|
||
result = append(result, char)
|
||
if len(formattable) > 0 {
|
||
formattable = formattable[1:]
|
||
}
|
||
|
||
case char == mComma && len(formattable) > 0 && formattable[0]:
|
||
result = append(result, ',', ' ')
|
||
|
||
case char == mSpace && len(formattable) > 0 && formattable[0] && len(result) > 0 && result[len(result)-1] == mSpace:
|
||
// Skip consecutive whitespace.
|
||
|
||
case (char == mAdd || char == mMul || char == mDiv || char == mSub) && len(formattable) > 0 && formattable[0]:
|
||
trimmed := trimRightSpace(result)
|
||
var prev, prevPrev byte
|
||
if len(trimmed) >= 1 {
|
||
prev = trimmed[len(trimmed)-1]
|
||
}
|
||
if len(trimmed) >= 2 {
|
||
prevPrev = trimmed[len(trimmed)-2]
|
||
}
|
||
var next byte
|
||
if i+1 < len(input) {
|
||
next = input[i+1]
|
||
}
|
||
|
||
switch {
|
||
case (prev == mLowerE || prev == mUpperE) && prevPrev >= mZero && prevPrev <= mNine:
|
||
// Scientific notation, e.g. -3.4e-2.
|
||
result = append(result, char)
|
||
case prev == mAdd || prev == mMul || prev == mDiv || prev == mSub:
|
||
result = append(result, char)
|
||
case prev == mLParen || prev == mComma:
|
||
result = append(result, char)
|
||
case i-1 >= 0 && input[i-1] == mSpace:
|
||
result = append(result, char, ' ')
|
||
case (prev >= mZero && prev <= mNine) ||
|
||
(next >= mZero && next <= mNine) ||
|
||
prev == mRParen ||
|
||
next == mLParen ||
|
||
next == mAdd || next == mMul || next == mDiv || next == mSub ||
|
||
(lastValuePos != -1 && lastValuePos == i-1):
|
||
result = append(result, ' ', char, ' ')
|
||
default:
|
||
result = append(result, char)
|
||
}
|
||
|
||
default:
|
||
result = append(result, char)
|
||
}
|
||
}
|
||
|
||
return string(result)
|
||
}
|
||
|
||
func trimRightSpace(b []byte) []byte {
|
||
end := len(b)
|
||
for end > 0 {
|
||
c := b[end-1]
|
||
if c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f' || c == '\v' {
|
||
end--
|
||
continue
|
||
}
|
||
break
|
||
}
|
||
return b[:end]
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/property-order.ts
|
||
// The canonical order in which CSS properties are sorted within a rule.
|
||
|
||
var twPropertyOrder = []string{
|
||
"container-type",
|
||
"pointer-events",
|
||
"visibility",
|
||
"position",
|
||
"inset",
|
||
"inset-inline",
|
||
"inset-block",
|
||
"inset-inline-start",
|
||
"inset-inline-end",
|
||
"inset-block-start",
|
||
"inset-block-end",
|
||
"top",
|
||
"right",
|
||
"bottom",
|
||
"left",
|
||
"isolation",
|
||
"z-index",
|
||
"order",
|
||
"grid-column",
|
||
"grid-column-start",
|
||
"grid-column-end",
|
||
"grid-row",
|
||
"grid-row-start",
|
||
"grid-row-end",
|
||
"float",
|
||
"clear",
|
||
"--tw-container-component",
|
||
"margin",
|
||
"margin-inline",
|
||
"margin-block",
|
||
"margin-inline-start",
|
||
"margin-inline-end",
|
||
"margin-block-start",
|
||
"margin-block-end",
|
||
"margin-top",
|
||
"margin-right",
|
||
"margin-bottom",
|
||
"margin-left",
|
||
"box-sizing",
|
||
"display",
|
||
"field-sizing",
|
||
"aspect-ratio",
|
||
"height",
|
||
"max-height",
|
||
"min-height",
|
||
"width",
|
||
"max-width",
|
||
"min-width",
|
||
"flex",
|
||
"flex-shrink",
|
||
"flex-grow",
|
||
"flex-basis",
|
||
"table-layout",
|
||
"caption-side",
|
||
"border-collapse",
|
||
"border-spacing",
|
||
"--tw-border-spacing-x",
|
||
"--tw-border-spacing-y",
|
||
"transform-origin",
|
||
"translate",
|
||
"--tw-translate-x",
|
||
"--tw-translate-y",
|
||
"--tw-translate-z",
|
||
"scale",
|
||
"--tw-scale-x",
|
||
"--tw-scale-y",
|
||
"--tw-scale-z",
|
||
"rotate",
|
||
"--tw-rotate-x",
|
||
"--tw-rotate-y",
|
||
"--tw-rotate-z",
|
||
"--tw-skew-x",
|
||
"--tw-skew-y",
|
||
"transform",
|
||
"zoom",
|
||
"animation",
|
||
"cursor",
|
||
"touch-action",
|
||
"--tw-pan-x",
|
||
"--tw-pan-y",
|
||
"--tw-pinch-zoom",
|
||
"resize",
|
||
"scroll-snap-type",
|
||
"--tw-scroll-snap-strictness",
|
||
"scroll-snap-align",
|
||
"scroll-snap-stop",
|
||
"scroll-margin",
|
||
"scroll-margin-inline",
|
||
"scroll-margin-block",
|
||
"scroll-margin-inline-start",
|
||
"scroll-margin-inline-end",
|
||
"scroll-margin-block-start",
|
||
"scroll-margin-block-end",
|
||
"scroll-margin-top",
|
||
"scroll-margin-right",
|
||
"scroll-margin-bottom",
|
||
"scroll-margin-left",
|
||
"scroll-padding",
|
||
"scroll-padding-inline",
|
||
"scroll-padding-block",
|
||
"scroll-padding-inline-start",
|
||
"scroll-padding-inline-end",
|
||
"scroll-padding-block-start",
|
||
"scroll-padding-block-end",
|
||
"scroll-padding-top",
|
||
"scroll-padding-right",
|
||
"scroll-padding-bottom",
|
||
"scroll-padding-left",
|
||
"scrollbar-width",
|
||
"scrollbar-color",
|
||
"scrollbar-gutter",
|
||
"list-style-position",
|
||
"list-style-type",
|
||
"list-style-image",
|
||
"appearance",
|
||
"columns",
|
||
"break-before",
|
||
"break-inside",
|
||
"break-after",
|
||
"grid-auto-columns",
|
||
"grid-auto-flow",
|
||
"grid-auto-rows",
|
||
"grid-template-columns",
|
||
"grid-template-rows",
|
||
"flex-direction",
|
||
"flex-wrap",
|
||
"place-content",
|
||
"place-items",
|
||
"align-content",
|
||
"align-items",
|
||
"justify-content",
|
||
"justify-items",
|
||
"gap",
|
||
"column-gap",
|
||
"row-gap",
|
||
"--tw-space-x-reverse",
|
||
"--tw-space-y-reverse",
|
||
"divide-x-width",
|
||
"divide-y-width",
|
||
"--tw-divide-y-reverse",
|
||
"divide-style",
|
||
"divide-color",
|
||
"place-self",
|
||
"align-self",
|
||
"justify-self",
|
||
"overflow",
|
||
"overflow-x",
|
||
"overflow-y",
|
||
"overscroll-behavior",
|
||
"overscroll-behavior-x",
|
||
"overscroll-behavior-y",
|
||
"scroll-behavior",
|
||
"border-radius",
|
||
"border-start-radius",
|
||
"border-end-radius",
|
||
"border-top-radius",
|
||
"border-right-radius",
|
||
"border-bottom-radius",
|
||
"border-left-radius",
|
||
"border-start-start-radius",
|
||
"border-start-end-radius",
|
||
"border-end-end-radius",
|
||
"border-end-start-radius",
|
||
"border-top-left-radius",
|
||
"border-top-right-radius",
|
||
"border-bottom-right-radius",
|
||
"border-bottom-left-radius",
|
||
"border-width",
|
||
"border-inline-width",
|
||
"border-block-width",
|
||
"border-inline-start-width",
|
||
"border-inline-end-width",
|
||
"border-block-start-width",
|
||
"border-block-end-width",
|
||
"border-top-width",
|
||
"border-right-width",
|
||
"border-bottom-width",
|
||
"border-left-width",
|
||
"border-style",
|
||
"border-inline-style",
|
||
"border-block-style",
|
||
"border-inline-start-style",
|
||
"border-inline-end-style",
|
||
"border-block-start-style",
|
||
"border-block-end-style",
|
||
"border-top-style",
|
||
"border-right-style",
|
||
"border-bottom-style",
|
||
"border-left-style",
|
||
"border-color",
|
||
"border-inline-color",
|
||
"border-block-color",
|
||
"border-inline-start-color",
|
||
"border-inline-end-color",
|
||
"border-block-start-color",
|
||
"border-block-end-color",
|
||
"border-top-color",
|
||
"border-right-color",
|
||
"border-bottom-color",
|
||
"border-left-color",
|
||
"background-color",
|
||
"background-image",
|
||
"--tw-gradient-position",
|
||
"--tw-gradient-stops",
|
||
"--tw-gradient-via-stops",
|
||
"--tw-gradient-from",
|
||
"--tw-gradient-from-position",
|
||
"--tw-gradient-via",
|
||
"--tw-gradient-via-position",
|
||
"--tw-gradient-to",
|
||
"--tw-gradient-to-position",
|
||
"mask-image",
|
||
"--tw-mask-top",
|
||
"--tw-mask-top-from-color",
|
||
"--tw-mask-top-from-position",
|
||
"--tw-mask-top-to-color",
|
||
"--tw-mask-top-to-position",
|
||
"--tw-mask-right",
|
||
"--tw-mask-right-from-color",
|
||
"--tw-mask-right-from-position",
|
||
"--tw-mask-right-to-color",
|
||
"--tw-mask-right-to-position",
|
||
"--tw-mask-bottom",
|
||
"--tw-mask-bottom-from-color",
|
||
"--tw-mask-bottom-from-position",
|
||
"--tw-mask-bottom-to-color",
|
||
"--tw-mask-bottom-to-position",
|
||
"--tw-mask-left",
|
||
"--tw-mask-left-from-color",
|
||
"--tw-mask-left-from-position",
|
||
"--tw-mask-left-to-color",
|
||
"--tw-mask-left-to-position",
|
||
"--tw-mask-linear",
|
||
"--tw-mask-linear-position",
|
||
"--tw-mask-linear-from-color",
|
||
"--tw-mask-linear-from-position",
|
||
"--tw-mask-linear-to-color",
|
||
"--tw-mask-linear-to-position",
|
||
"--tw-mask-radial",
|
||
"--tw-mask-radial-shape",
|
||
"--tw-mask-radial-size",
|
||
"--tw-mask-radial-position",
|
||
"--tw-mask-radial-from-color",
|
||
"--tw-mask-radial-from-position",
|
||
"--tw-mask-radial-to-color",
|
||
"--tw-mask-radial-to-position",
|
||
"--tw-mask-conic",
|
||
"--tw-mask-conic-position",
|
||
"--tw-mask-conic-from-color",
|
||
"--tw-mask-conic-from-position",
|
||
"--tw-mask-conic-to-color",
|
||
"--tw-mask-conic-to-position",
|
||
"box-decoration-break",
|
||
"background-size",
|
||
"background-attachment",
|
||
"background-clip",
|
||
"background-position",
|
||
"background-repeat",
|
||
"background-origin",
|
||
"mask-composite",
|
||
"mask-mode",
|
||
"mask-type",
|
||
"mask-size",
|
||
"mask-clip",
|
||
"mask-position",
|
||
"mask-repeat",
|
||
"mask-origin",
|
||
"fill",
|
||
"stroke",
|
||
"stroke-width",
|
||
"object-fit",
|
||
"object-position",
|
||
"padding",
|
||
"padding-inline",
|
||
"padding-block",
|
||
"padding-inline-start",
|
||
"padding-inline-end",
|
||
"padding-block-start",
|
||
"padding-block-end",
|
||
"padding-top",
|
||
"padding-right",
|
||
"padding-bottom",
|
||
"padding-left",
|
||
"text-align",
|
||
"text-indent",
|
||
"vertical-align",
|
||
"font-family",
|
||
"font-feature-settings",
|
||
"font-size",
|
||
"line-height",
|
||
"font-weight",
|
||
"letter-spacing",
|
||
"text-wrap",
|
||
"overflow-wrap",
|
||
"word-break",
|
||
"text-overflow",
|
||
"hyphens",
|
||
"white-space",
|
||
"tab-size",
|
||
"color",
|
||
"text-transform",
|
||
"font-style",
|
||
"font-stretch",
|
||
"font-variant-numeric",
|
||
"text-decoration-line",
|
||
"text-decoration-color",
|
||
"text-decoration-style",
|
||
"text-decoration-thickness",
|
||
"text-underline-offset",
|
||
"-webkit-font-smoothing",
|
||
"placeholder-color",
|
||
"caret-color",
|
||
"accent-color",
|
||
"color-scheme",
|
||
"opacity",
|
||
"background-blend-mode",
|
||
"mix-blend-mode",
|
||
"box-shadow",
|
||
"--tw-shadow",
|
||
"--tw-shadow-color",
|
||
"--tw-ring-shadow",
|
||
"--tw-ring-color",
|
||
"--tw-inset-shadow",
|
||
"--tw-inset-shadow-color",
|
||
"--tw-inset-ring-shadow",
|
||
"--tw-inset-ring-color",
|
||
"--tw-ring-offset-width",
|
||
"--tw-ring-offset-color",
|
||
"outline",
|
||
"outline-width",
|
||
"outline-offset",
|
||
"outline-color",
|
||
"--tw-blur",
|
||
"--tw-brightness",
|
||
"--tw-contrast",
|
||
"--tw-drop-shadow",
|
||
"--tw-grayscale",
|
||
"--tw-hue-rotate",
|
||
"--tw-invert",
|
||
"--tw-saturate",
|
||
"--tw-sepia",
|
||
"filter",
|
||
"--tw-backdrop-blur",
|
||
"--tw-backdrop-brightness",
|
||
"--tw-backdrop-contrast",
|
||
"--tw-backdrop-grayscale",
|
||
"--tw-backdrop-hue-rotate",
|
||
"--tw-backdrop-invert",
|
||
"--tw-backdrop-opacity",
|
||
"--tw-backdrop-saturate",
|
||
"--tw-backdrop-sepia",
|
||
"backdrop-filter",
|
||
"transition-property",
|
||
"transition-behavior",
|
||
"transition-delay",
|
||
"transition-duration",
|
||
"transition-timing-function",
|
||
"will-change",
|
||
"contain",
|
||
"content",
|
||
"forced-color-adjust",
|
||
}
|
||
|
||
// twProseCSS returns the complete set of CSSRule nodes for the `prose` utility,
|
||
// matching the output of @tailwindcss/typography's default (base) configuration.
|
||
func proseCSS() []*AstNode {
|
||
p := func(sel string, decls ...*AstNode) *AstNode {
|
||
return styleRule(sel, decls...)
|
||
}
|
||
d := decl
|
||
|
||
return []*AstNode{
|
||
// Root
|
||
p("&",
|
||
d("color", "var(--tw-prose-body)"),
|
||
d("max-width", "65ch"),
|
||
d("font-size", "1rem"),
|
||
d("line-height", "1.75"),
|
||
// Gray theme variables (default)
|
||
d("--tw-prose-body", "#374151"),
|
||
d("--tw-prose-headings", "#111827"),
|
||
d("--tw-prose-lead", "#4b5563"),
|
||
d("--tw-prose-links", "#111827"),
|
||
d("--tw-prose-bold", "#111827"),
|
||
d("--tw-prose-counters", "#6b7280"),
|
||
d("--tw-prose-bullets", "#d1d5db"),
|
||
d("--tw-prose-hr", "#e5e7eb"),
|
||
d("--tw-prose-quotes", "#111827"),
|
||
d("--tw-prose-quote-borders", "#e5e7eb"),
|
||
d("--tw-prose-captions", "#6b7280"),
|
||
d("--tw-prose-kbd", "#111827"),
|
||
d("--tw-prose-kbd-shadows", "17 24 39"),
|
||
d("--tw-prose-code", "#111827"),
|
||
d("--tw-prose-pre-code", "#e5e7eb"),
|
||
d("--tw-prose-pre-bg", "#1f2937"),
|
||
d("--tw-prose-th-borders", "#d1d5db"),
|
||
d("--tw-prose-td-borders", "#e5e7eb"),
|
||
// Invert variables
|
||
d("--tw-prose-invert-body", "#d1d5db"),
|
||
d("--tw-prose-invert-headings", "#fff"),
|
||
d("--tw-prose-invert-lead", "#9ca3af"),
|
||
d("--tw-prose-invert-links", "#fff"),
|
||
d("--tw-prose-invert-bold", "#fff"),
|
||
d("--tw-prose-invert-counters", "#9ca3af"),
|
||
d("--tw-prose-invert-bullets", "#4b5563"),
|
||
d("--tw-prose-invert-hr", "#374151"),
|
||
d("--tw-prose-invert-quotes", "#f3f4f6"),
|
||
d("--tw-prose-invert-quote-borders", "#374151"),
|
||
d("--tw-prose-invert-captions", "#9ca3af"),
|
||
d("--tw-prose-invert-kbd", "#fff"),
|
||
d("--tw-prose-invert-kbd-shadows", "255 255 255"),
|
||
d("--tw-prose-invert-code", "#fff"),
|
||
d("--tw-prose-invert-pre-code", "#d1d5db"),
|
||
d("--tw-prose-invert-pre-bg", "rgb(0 0 0 / 50%)"),
|
||
d("--tw-prose-invert-th-borders", "#4b5563"),
|
||
d("--tw-prose-invert-td-borders", "#374151"),
|
||
),
|
||
|
||
// Lead text
|
||
p("& [class~=\"lead\"]",
|
||
d("color", "var(--tw-prose-lead)"),
|
||
d("font-size", "1.25em"),
|
||
d("line-height", "1.6"),
|
||
d("margin-top", "1.2em"),
|
||
d("margin-bottom", "1.2em"),
|
||
),
|
||
|
||
// Links
|
||
p("& a",
|
||
d("color", "var(--tw-prose-links)"),
|
||
d("text-decoration", "underline"),
|
||
d("font-weight", "500"),
|
||
),
|
||
|
||
// Strong
|
||
p("& strong",
|
||
d("color", "var(--tw-prose-bold)"),
|
||
d("font-weight", "600"),
|
||
),
|
||
p("& a strong, & blockquote strong, & thead th strong",
|
||
d("color", "inherit"),
|
||
),
|
||
|
||
// Lists
|
||
p("& ol",
|
||
d("list-style-type", "decimal"),
|
||
d("margin-top", "1.25em"),
|
||
d("margin-bottom", "1.25em"),
|
||
d("padding-inline-start", "1.625em"),
|
||
),
|
||
p("& ul",
|
||
d("list-style-type", "disc"),
|
||
d("margin-top", "1.25em"),
|
||
d("margin-bottom", "1.25em"),
|
||
d("padding-inline-start", "1.625em"),
|
||
),
|
||
p("& li",
|
||
d("margin-top", "0.5em"),
|
||
d("margin-bottom", "0.5em"),
|
||
),
|
||
p("& ol > li",
|
||
d("padding-inline-start", "0.375em"),
|
||
),
|
||
p("& ul > li",
|
||
d("padding-inline-start", "0.375em"),
|
||
),
|
||
p("& ol > li::marker",
|
||
d("font-weight", "400"),
|
||
d("color", "var(--tw-prose-counters)"),
|
||
),
|
||
p("& ul > li::marker",
|
||
d("color", "var(--tw-prose-bullets)"),
|
||
),
|
||
p("& > ul > li p",
|
||
d("margin-top", "0.75em"),
|
||
d("margin-bottom", "0.75em"),
|
||
),
|
||
p("& > ul > li > p:first-child",
|
||
d("margin-top", "1.25em"),
|
||
),
|
||
p("& > ul > li > p:last-child",
|
||
d("margin-bottom", "1.25em"),
|
||
),
|
||
p("& > ol > li > p:first-child",
|
||
d("margin-top", "1.25em"),
|
||
),
|
||
p("& > ol > li > p:last-child",
|
||
d("margin-bottom", "1.25em"),
|
||
),
|
||
p("& ul ul, & ul ol, & ol ul, & ol ol",
|
||
d("margin-top", "0.75em"),
|
||
d("margin-bottom", "0.75em"),
|
||
),
|
||
|
||
// Definition lists
|
||
p("& dl",
|
||
d("margin-top", "1.25em"),
|
||
d("margin-bottom", "1.25em"),
|
||
),
|
||
p("& dt",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "600"),
|
||
d("margin-top", "1.25em"),
|
||
),
|
||
p("& dd",
|
||
d("margin-top", "0.5em"),
|
||
d("padding-inline-start", "1.625em"),
|
||
),
|
||
|
||
// Paragraphs
|
||
p("& p",
|
||
d("margin-top", "1.25em"),
|
||
d("margin-bottom", "1.25em"),
|
||
),
|
||
|
||
// Headings
|
||
p("& h1",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "800"),
|
||
d("font-size", "2.25em"),
|
||
d("margin-top", "0"),
|
||
d("margin-bottom", "0.8888889em"),
|
||
d("line-height", "1.1111111"),
|
||
),
|
||
p("& h1 strong",
|
||
d("font-weight", "900"),
|
||
d("color", "inherit"),
|
||
),
|
||
p("& h2",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "700"),
|
||
d("font-size", "1.5em"),
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "1em"),
|
||
d("line-height", "1.3333333"),
|
||
),
|
||
p("& h2 strong",
|
||
d("font-weight", "800"),
|
||
d("color", "inherit"),
|
||
),
|
||
p("& h3",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "600"),
|
||
d("font-size", "1.25em"),
|
||
d("margin-top", "1.6em"),
|
||
d("margin-bottom", "0.6em"),
|
||
d("line-height", "1.6"),
|
||
),
|
||
p("& h3 strong",
|
||
d("font-weight", "700"),
|
||
d("color", "inherit"),
|
||
),
|
||
p("& h4",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "600"),
|
||
d("margin-top", "1.5em"),
|
||
d("margin-bottom", "0.5em"),
|
||
d("line-height", "1.5"),
|
||
),
|
||
p("& h4 strong",
|
||
d("font-weight", "700"),
|
||
d("color", "inherit"),
|
||
),
|
||
|
||
// Horizontal rule
|
||
p("& hr",
|
||
d("border-color", "var(--tw-prose-hr)"),
|
||
d("border-top-width", "1px"),
|
||
d("margin-top", "3em"),
|
||
d("margin-bottom", "3em"),
|
||
),
|
||
p("& hr + *",
|
||
d("margin-top", "0"),
|
||
),
|
||
p("& h2 + *",
|
||
d("margin-top", "0"),
|
||
),
|
||
p("& h3 + *",
|
||
d("margin-top", "0"),
|
||
),
|
||
p("& h4 + *",
|
||
d("margin-top", "0"),
|
||
),
|
||
|
||
// Blockquote
|
||
p("& blockquote",
|
||
d("font-weight", "500"),
|
||
d("font-style", "italic"),
|
||
d("color", "var(--tw-prose-quotes)"),
|
||
d("border-inline-start-width", "0.25rem"),
|
||
d("border-inline-start-color", "var(--tw-prose-quote-borders)"),
|
||
d("quotes", "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\""),
|
||
d("margin-top", "1.6em"),
|
||
d("margin-bottom", "1.6em"),
|
||
d("padding-inline-start", "1em"),
|
||
),
|
||
p("& blockquote p:first-of-type::before",
|
||
d("content", "open-quote"),
|
||
),
|
||
p("& blockquote p:last-of-type::after",
|
||
d("content", "close-quote"),
|
||
),
|
||
|
||
// Images and media
|
||
p("& img",
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "2em"),
|
||
),
|
||
p("& picture",
|
||
d("display", "block"),
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "2em"),
|
||
),
|
||
p("& picture > img",
|
||
d("margin-top", "0"),
|
||
d("margin-bottom", "0"),
|
||
),
|
||
p("& video",
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "2em"),
|
||
),
|
||
|
||
// Figures
|
||
p("& figure",
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "2em"),
|
||
),
|
||
p("& figure > *",
|
||
d("margin-top", "0"),
|
||
d("margin-bottom", "0"),
|
||
),
|
||
p("& figcaption",
|
||
d("color", "var(--tw-prose-captions)"),
|
||
d("font-size", "0.875em"),
|
||
d("line-height", "1.4285714"),
|
||
d("margin-top", "0.8571429em"),
|
||
),
|
||
|
||
// Keyboard
|
||
p("& kbd",
|
||
d("font-weight", "500"),
|
||
d("font-family", "inherit"),
|
||
d("color", "var(--tw-prose-kbd)"),
|
||
d("box-shadow", "0 0 0 1px rgb(var(--tw-prose-kbd-shadows) / 10%), 0 3px 0 rgb(var(--tw-prose-kbd-shadows) / 10%)"),
|
||
d("font-size", "0.875em"),
|
||
d("border-radius", "0.3125rem"),
|
||
d("padding-top", "0.1875em"),
|
||
d("padding-inline-end", "0.375em"),
|
||
d("padding-bottom", "0.1875em"),
|
||
d("padding-inline-start", "0.375em"),
|
||
),
|
||
|
||
// Code
|
||
p("& code",
|
||
d("color", "var(--tw-prose-code)"),
|
||
d("font-weight", "600"),
|
||
d("font-size", "0.875em"),
|
||
),
|
||
p("& code::before",
|
||
d("content", "\"`\""),
|
||
),
|
||
p("& code::after",
|
||
d("content", "\"`\""),
|
||
),
|
||
p("& a code, & h1 code, & h2 code, & h3 code, & h4 code, & blockquote code, & thead th code",
|
||
d("color", "inherit"),
|
||
),
|
||
|
||
// Pre
|
||
p("& pre",
|
||
d("color", "var(--tw-prose-pre-code)"),
|
||
d("background-color", "var(--tw-prose-pre-bg)"),
|
||
d("overflow-x", "auto"),
|
||
d("font-weight", "400"),
|
||
d("font-size", "0.875em"),
|
||
d("line-height", "1.7142857"),
|
||
d("margin-top", "1.7142857em"),
|
||
d("margin-bottom", "1.7142857em"),
|
||
d("border-radius", "0.375rem"),
|
||
d("padding-top", "0.8571429em"),
|
||
d("padding-inline-end", "1.1428571em"),
|
||
d("padding-bottom", "0.8571429em"),
|
||
d("padding-inline-start", "1.1428571em"),
|
||
),
|
||
p("& pre code",
|
||
d("background-color", "transparent"),
|
||
d("border-width", "0"),
|
||
d("border-radius", "0"),
|
||
d("padding", "0"),
|
||
d("font-weight", "inherit"),
|
||
d("color", "inherit"),
|
||
d("font-size", "inherit"),
|
||
d("font-family", "inherit"),
|
||
d("line-height", "inherit"),
|
||
),
|
||
p("& pre code::before",
|
||
d("content", "none"),
|
||
),
|
||
p("& pre code::after",
|
||
d("content", "none"),
|
||
),
|
||
|
||
// Tables
|
||
p("& table",
|
||
d("width", "100%"),
|
||
d("table-layout", "auto"),
|
||
d("margin-top", "2em"),
|
||
d("margin-bottom", "2em"),
|
||
d("font-size", "0.875em"),
|
||
d("line-height", "1.7142857"),
|
||
),
|
||
p("& thead",
|
||
d("border-bottom-width", "1px"),
|
||
d("border-bottom-color", "var(--tw-prose-th-borders)"),
|
||
),
|
||
p("& thead th",
|
||
d("color", "var(--tw-prose-headings)"),
|
||
d("font-weight", "600"),
|
||
d("vertical-align", "bottom"),
|
||
d("padding-inline-end", "0.5714286em"),
|
||
d("padding-bottom", "0.5714286em"),
|
||
d("padding-inline-start", "0.5714286em"),
|
||
),
|
||
p("& thead th:first-child",
|
||
d("padding-inline-start", "0"),
|
||
),
|
||
p("& thead th:last-child",
|
||
d("padding-inline-end", "0"),
|
||
),
|
||
p("& tbody tr",
|
||
d("border-bottom-width", "1px"),
|
||
d("border-bottom-color", "var(--tw-prose-td-borders)"),
|
||
),
|
||
p("& tbody tr:last-child",
|
||
d("border-bottom-width", "0"),
|
||
),
|
||
p("& tbody td, & tfoot td",
|
||
d("vertical-align", "baseline"),
|
||
d("padding-top", "0.5714286em"),
|
||
d("padding-inline-end", "0.5714286em"),
|
||
d("padding-bottom", "0.5714286em"),
|
||
d("padding-inline-start", "0.5714286em"),
|
||
),
|
||
p("& tbody td:first-child, & tfoot td:first-child",
|
||
d("padding-inline-start", "0"),
|
||
),
|
||
p("& tbody td:last-child, & tfoot td:last-child",
|
||
d("padding-inline-end", "0"),
|
||
),
|
||
p("& tfoot",
|
||
d("border-top-width", "1px"),
|
||
d("border-top-color", "var(--tw-prose-th-borders)"),
|
||
),
|
||
p("& th, & td",
|
||
d("text-align", "start"),
|
||
),
|
||
|
||
// h2/h3 code sizes
|
||
p("& h2 code",
|
||
d("font-size", "0.875em"),
|
||
),
|
||
p("& h3 code",
|
||
d("font-size", "0.9em"),
|
||
),
|
||
|
||
// First/last child margin reset
|
||
p("& > :first-child",
|
||
d("margin-top", "0"),
|
||
),
|
||
p("& > :last-child",
|
||
d("margin-bottom", "0"),
|
||
),
|
||
}
|
||
}
|
||
|
||
// twProseInvertCSS returns CSSRule nodes for the `prose-invert` modifier.
|
||
func proseInvertCSS() []*AstNode {
|
||
vars := []string{
|
||
"body", "headings", "lead", "links", "bold", "counters", "bullets",
|
||
"hr", "quotes", "quote-borders", "captions", "kbd", "kbd-shadows",
|
||
"code", "pre-code", "pre-bg", "th-borders", "td-borders",
|
||
}
|
||
var decls []*AstNode
|
||
for _, v := range vars {
|
||
decls = append(decls, decl("--tw-prose-"+v, "var(--tw-prose-invert-"+v+")"))
|
||
}
|
||
return []*AstNode{
|
||
styleRule("&", decls...),
|
||
}
|
||
}
|
||
|
||
// registerProse registers the non-core `prose`/`prose-invert` typography
|
||
// utilities (ported from @tailwindcss/typography's base output). -mta
|
||
func registerProse(c *utilCtx) {
|
||
c.utilities.static("prose", func(_ *Candidate) *utilResult { return uList(proseCSS()) })
|
||
c.utilities.static("prose-invert", func(_ *Candidate) *utilResult { return uList(proseInvertCSS()) })
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/replace-shadow-colors.ts
|
||
//
|
||
// The upstream walks the value AST; since every branch returns Skip/ReplaceStop
|
||
// (functions are never recursed into), only top-level value nodes matter, so
|
||
// this iterates them directly.
|
||
|
||
var shadowKeywords = map[string]bool{"inset": true, "inherit": true, "initial": true, "revert": true, "unset": true}
|
||
var shadowLengthFns = map[string]bool{"calc": true, "clamp": true, "max": true, "min": true, "--spacing": true}
|
||
var shadowColorFns = map[string]bool{
|
||
"color": true, "color-mix": true, "contrast-color": true, "device-cmyk": true,
|
||
"hsl": true, "hsla": true, "hwb": true, "lab": true, "lch": true, "light-dark": true,
|
||
"oklab": true, "oklch": true, "rgb": true, "rgba": true, "--alpha": true,
|
||
}
|
||
var reShadowLength = regexp.MustCompile(`^-?(\d+|\.\d+)(.*?)$`)
|
||
|
||
func replaceShadowColors(input string, replacement func(color string) string) string {
|
||
replaceAst := func(node ValueNode) []ValueNode {
|
||
color := valueToCss([]ValueNode{node})
|
||
return valueParse(replacement(color))
|
||
}
|
||
|
||
parts := segment(input, ",")
|
||
out := make([]string, len(parts))
|
||
for pi, shadow := range parts {
|
||
shadow = strings.TrimSpace(shadow)
|
||
ast := valueParse(shadow)
|
||
|
||
unknownIdx := -1
|
||
unknowns := 0
|
||
lengths := 0
|
||
replaced := false
|
||
|
||
for i := 0; i < len(ast); i++ {
|
||
switch n := ast[i].(type) {
|
||
case *ValueWord:
|
||
lw := strings.ToLower(n.Value)
|
||
if shadowKeywords[lw] {
|
||
continue
|
||
}
|
||
if reShadowLength.MatchString(lw) {
|
||
lengths++
|
||
continue
|
||
}
|
||
if (len(n.Value) > 0 && n.Value[0] == '#') || isNamedColor(n.Value) {
|
||
repl := replaceAst(ast[i])
|
||
ast = spliceValueNodes(ast, i, repl)
|
||
replaced = true
|
||
}
|
||
if replaced {
|
||
break
|
||
}
|
||
unknownIdx = i
|
||
unknowns++
|
||
case *ValueFunction:
|
||
lf := strings.ToLower(n.Value)
|
||
if shadowColorFns[lf] {
|
||
repl := replaceAst(ast[i])
|
||
ast = spliceValueNodes(ast, i, repl)
|
||
replaced = true
|
||
break
|
||
}
|
||
if shadowLengthFns[lf] {
|
||
lengths++
|
||
continue
|
||
}
|
||
unknownIdx = i
|
||
unknowns++
|
||
case *ValueSeparator:
|
||
continue
|
||
}
|
||
if replaced {
|
||
break
|
||
}
|
||
}
|
||
|
||
if replaced {
|
||
out[pi] = valueToCss(ast)
|
||
continue
|
||
}
|
||
if lengths < 2 {
|
||
out[pi] = shadow
|
||
continue
|
||
}
|
||
if unknowns == 0 {
|
||
out[pi] = shadow + " " + replacement("currentcolor")
|
||
continue
|
||
}
|
||
if unknowns == 1 {
|
||
repl := replaceAst(ast[unknownIdx])
|
||
ast = spliceValueNodes(ast, unknownIdx, repl)
|
||
replaced = true
|
||
}
|
||
if replaced {
|
||
out[pi] = valueToCss(ast)
|
||
} else {
|
||
out[pi] = shadow
|
||
}
|
||
}
|
||
|
||
return strings.Join(out, ", ")
|
||
}
|
||
|
||
func spliceValueNodes(nodes []ValueNode, idx int, repl []ValueNode) []ValueNode {
|
||
out := make([]ValueNode, 0, len(nodes)-1+len(repl))
|
||
out = append(out, nodes[:idx]...)
|
||
out = append(out, repl...)
|
||
out = append(out, nodes[idx+1:]...)
|
||
return out
|
||
}
|
||
|
||
func twScanFile(path string) ([]string, error) {
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
src := string(content)
|
||
// For HTML/template files, also extract tokens from raw content
|
||
// to catch class names inside Go template directives ({{ }})
|
||
// which break the JS-oriented quote-based extraction.
|
||
if strings.HasSuffix(path, ".html") || strings.HasSuffix(path, ".gohtml") || strings.HasSuffix(path, ".tmpl") {
|
||
return extractCandidatesHTML(src), nil
|
||
}
|
||
return extractCandidates(src), nil
|
||
}
|
||
|
||
func extractCandidatesHTML(src string) []string {
|
||
seen := make(map[string]bool)
|
||
var results []string
|
||
add := func(candidates []string) {
|
||
for _, c := range candidates {
|
||
if !seen[c] {
|
||
seen[c] = true
|
||
results = append(results, c)
|
||
}
|
||
}
|
||
}
|
||
// Standard extraction from quoted strings
|
||
add(extractCandidates(src))
|
||
// Also extract tokens from the entire raw content — this catches
|
||
// class names inside Go template blocks like {{if ...}}class{{end}}
|
||
// where embedded quotes break the string-based extraction.
|
||
add(extractTokens(src))
|
||
return results
|
||
}
|
||
|
||
func twScanFiles(dir string) ([]string, error) {
|
||
var candidates []string
|
||
seen := make(map[string]bool)
|
||
|
||
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
||
if err != nil || d.IsDir() {
|
||
return err
|
||
}
|
||
if !strings.HasSuffix(path, ".js") {
|
||
return nil
|
||
}
|
||
content, err := os.ReadFile(path)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
for _, c := range extractCandidates(string(content)) {
|
||
if !seen[c] {
|
||
seen[c] = true
|
||
candidates = append(candidates, c)
|
||
}
|
||
}
|
||
return nil
|
||
})
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
|
||
sort.Strings(candidates)
|
||
return candidates, nil
|
||
}
|
||
|
||
func extractCandidates(src string) []string {
|
||
var results []string
|
||
n := len(src)
|
||
i := 0
|
||
// prevValue is true when the previous significant token can end an
|
||
// expression. It tells a `/` apart: division when it follows a value,
|
||
// a regex literal otherwise. Without this, a quote inside a regex (e.g.
|
||
// /it's/) reads as a string and desyncs the scanner, just like an
|
||
// apostrophe in a comment would.
|
||
prevValue := false
|
||
|
||
for i < n {
|
||
ch := src[i]
|
||
|
||
switch {
|
||
case ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r':
|
||
// Whitespace is insignificant; prevValue carries over.
|
||
i++
|
||
|
||
case ch == '/' && i+1 < n && (src[i+1] == '/' || src[i+1] == '*'):
|
||
// Comment — skip so quotes/apostrophes inside (e.g. "don't",
|
||
// "button's") don't desync the string scanner. A single stray
|
||
// apostrophe in a comment otherwise flips quote parity and swallows
|
||
// every class literal until the next quote. Insignificant, so
|
||
// prevValue carries over. Strings are matched after this, so a `//`
|
||
// inside a quoted URL is never reached here.
|
||
i, _ = skipComment(src, i, n)
|
||
|
||
case ch == '"' || ch == '\'':
|
||
// String literal — where class names live.
|
||
i++
|
||
start := i
|
||
for i < n && src[i] != ch {
|
||
if src[i] == '\\' && i+1 < n {
|
||
i += 2
|
||
continue
|
||
}
|
||
i++
|
||
}
|
||
results = append(results, extractTokens(src[start:i])...)
|
||
if i < n {
|
||
i++
|
||
}
|
||
prevValue = true
|
||
|
||
case ch == '`':
|
||
i++
|
||
results = append(results, extractFromTemplate(src, &i, n)...)
|
||
prevValue = true
|
||
|
||
case ch == '/':
|
||
// Not a comment (handled above): a regex literal unless it follows
|
||
// a value, in which case it's the division operator.
|
||
if prevValue {
|
||
i++
|
||
prevValue = false
|
||
} else {
|
||
i = skipRegexLiteral(src, i, n)
|
||
prevValue = true
|
||
}
|
||
|
||
case isIdentByte(ch):
|
||
start := i
|
||
for i < n && isIdentByte(src[i]) {
|
||
i++
|
||
}
|
||
prevValue = !keywordExpectsRegex(src[start:i])
|
||
|
||
case ch == ')' || ch == ']':
|
||
prevValue = true
|
||
i++
|
||
|
||
default:
|
||
// Any other operator/punctuation ({ } ( = : ; , . ...): a `/` that
|
||
// follows is a regex, not division.
|
||
prevValue = false
|
||
i++
|
||
}
|
||
}
|
||
|
||
return results
|
||
}
|
||
|
||
func extractFromTemplate(src string, pos *int, n int) []string {
|
||
var results []string
|
||
i := *pos
|
||
|
||
for i < n {
|
||
if src[i] == '\\' && i+1 < n {
|
||
i += 2
|
||
continue
|
||
}
|
||
if src[i] == '`' {
|
||
i++
|
||
*pos = i
|
||
return results
|
||
}
|
||
if src[i] == '$' && i+1 < n && src[i+1] == '{' {
|
||
i += 2
|
||
depth := 1
|
||
// Same regex/division disambiguation as extractCandidates, scoped to
|
||
// this interpolation. Tracking prevValue (and skipping comments and
|
||
// regex literals) keeps quotes inside either from miscounting the
|
||
// braces that delimit the ${...} expression.
|
||
prevValue := false
|
||
for i < n && depth > 0 {
|
||
c := src[i]
|
||
switch {
|
||
case c == ' ' || c == '\t' || c == '\n' || c == '\r':
|
||
i++
|
||
case c == '/' && i+1 < n && (src[i+1] == '/' || src[i+1] == '*'):
|
||
i, _ = skipComment(src, i, n)
|
||
case c == '{':
|
||
depth++
|
||
i++
|
||
prevValue = false
|
||
case c == '}':
|
||
depth--
|
||
i++
|
||
prevValue = false
|
||
case c == '"' || c == '\'':
|
||
i++
|
||
start := i
|
||
for i < n && src[i] != c {
|
||
if src[i] == '\\' && i+1 < n {
|
||
i += 2
|
||
continue
|
||
}
|
||
i++
|
||
}
|
||
results = append(results, extractTokens(src[start:i])...)
|
||
if i < n {
|
||
i++
|
||
}
|
||
prevValue = true
|
||
case c == '`':
|
||
i++
|
||
results = append(results, extractFromTemplate(src, &i, n)...)
|
||
prevValue = true
|
||
case c == '/':
|
||
if prevValue {
|
||
i++
|
||
prevValue = false
|
||
} else {
|
||
i = skipRegexLiteral(src, i, n)
|
||
prevValue = true
|
||
}
|
||
case isIdentByte(c):
|
||
start := i
|
||
for i < n && isIdentByte(src[i]) {
|
||
i++
|
||
}
|
||
prevValue = !keywordExpectsRegex(src[start:i])
|
||
case c == ')' || c == ']':
|
||
prevValue = true
|
||
i++
|
||
default:
|
||
prevValue = false
|
||
i++
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
|
||
// Accumulate template text content.
|
||
start := i
|
||
for i < n && src[i] != '`' && src[i] != '\\' && !(src[i] == '$' && i+1 < n && src[i+1] == '{') {
|
||
i++
|
||
}
|
||
if i > start {
|
||
results = append(results, extractTokens(src[start:i])...)
|
||
}
|
||
}
|
||
|
||
*pos = i
|
||
return results
|
||
}
|
||
|
||
func extractTokens(s string) []string {
|
||
var tokens []string
|
||
n := len(s)
|
||
i := 0
|
||
|
||
for i < n {
|
||
// Skip non-candidate characters.
|
||
for i < n && !isCandidateChar(s[i]) {
|
||
i++
|
||
}
|
||
if i >= n {
|
||
break
|
||
}
|
||
|
||
start := i
|
||
for i < n && isCandidateChar(s[i]) {
|
||
// Handle bracket groups [...]
|
||
if s[i] == '[' {
|
||
i++
|
||
for i < n && s[i] != ']' {
|
||
i++
|
||
}
|
||
if i < n {
|
||
i++
|
||
}
|
||
continue
|
||
}
|
||
i++
|
||
}
|
||
|
||
token := s[start:i]
|
||
if looksLikeTWCandidate(token) {
|
||
tokens = append(tokens, token)
|
||
}
|
||
}
|
||
|
||
return tokens
|
||
}
|
||
|
||
func isCandidateChar(ch byte) bool {
|
||
return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') ||
|
||
ch == '-' || ch == '_' || ch == '/' || ch == ':' || ch == '!' || ch == '[' || ch == ']' ||
|
||
ch == '#' || ch == '.' || ch == '%' || ch == '(' || ch == ')' || ch == ','
|
||
}
|
||
|
||
func looksLikeTWCandidate(s string) bool {
|
||
if len(s) == 0 || len(s) > 200 {
|
||
return false
|
||
}
|
||
// Must start with a letter, ! or -. Also allow `[` to support bare
|
||
// arbitrary-property syntax like `[transition:opacity_300ms]` and arbitrary
|
||
// variants like `[&_td]:p-4`.
|
||
ch := s[0]
|
||
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '!' || ch == '-' || ch == '[') {
|
||
return false
|
||
}
|
||
// Filter out things that are clearly not utilities.
|
||
if strings.ContainsAny(s, "{}()=<>&|+*~^") {
|
||
// Allow parens only inside brackets
|
||
if !strings.Contains(s, "[") {
|
||
return false
|
||
}
|
||
}
|
||
return true
|
||
}
|
||
|
||
func isASCIILetter(b byte) bool {
|
||
return (b >= 'a' && b <= 'z') || (b >= 'A' && b <= 'Z')
|
||
}
|
||
|
||
// isIdentByte reports whether b can appear in a JS identifier or number, used
|
||
// to consume barewords whole so a keyword can be told from a plain value.
|
||
func isIdentByte(b byte) bool {
|
||
return isASCIILetter(b) || (b >= '0' && b <= '9') || b == '_' || b == '$'
|
||
}
|
||
|
||
// keywordExpectsRegex reports whether a `/` directly following this bareword is
|
||
// a regex literal (the word is a keyword that expects an expression next)
|
||
// rather than the division operator.
|
||
func keywordExpectsRegex(word string) bool {
|
||
switch word {
|
||
case "return", "typeof", "instanceof", "in", "of", "new", "delete",
|
||
"void", "do", "else", "case", "yield", "await", "throw":
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
// skipComment skips a // line comment or /* */ block comment that starts at i
|
||
// (src[i] must be '/'). ok is false when i is not the start of a comment.
|
||
func skipComment(src string, i, n int) (int, bool) {
|
||
if i+1 >= n || src[i] != '/' {
|
||
return i, false
|
||
}
|
||
switch src[i+1] {
|
||
case '/':
|
||
i += 2
|
||
for i < n && src[i] != '\n' {
|
||
i++
|
||
}
|
||
return i, true
|
||
case '*':
|
||
i += 2
|
||
for i+1 < n && !(src[i] == '*' && src[i+1] == '/') {
|
||
i++
|
||
}
|
||
i += 2 // consume the closing */
|
||
if i > n {
|
||
i = n
|
||
}
|
||
return i, true
|
||
}
|
||
return i, false
|
||
}
|
||
|
||
// skipRegexLiteral skips a /.../flags regex literal whose opening slash is at i.
|
||
// Character classes [...] are honored so a `/` inside them doesn't end the
|
||
// literal; a newline aborts (a real regex can't span one) to bound any runaway
|
||
// from a mis-detected division.
|
||
func skipRegexLiteral(src string, i, n int) int {
|
||
i++ // opening /
|
||
inClass := false
|
||
for i < n {
|
||
c := src[i]
|
||
switch {
|
||
case c == '\\' && i+1 < n:
|
||
i += 2
|
||
case c == '\n':
|
||
return i
|
||
case c == '[':
|
||
inClass = true
|
||
i++
|
||
case c == ']':
|
||
inClass = false
|
||
i++
|
||
case c == '/' && !inClass:
|
||
i++ // closing /
|
||
for i < n && isASCIILetter(src[i]) {
|
||
i++ // flags
|
||
}
|
||
return i
|
||
default:
|
||
i++
|
||
}
|
||
}
|
||
return i
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utils/segment.ts
|
||
//
|
||
// Splits a string on a top-level occurrence of a single-character separator,
|
||
// ignoring separators that appear inside (), [], {} or quoted strings. Regex
|
||
// can't do balanced matching, so this is a tiny state machine — identical in
|
||
// behaviour to the upstream implementation. Operates on bytes; all structural
|
||
// characters are ASCII and UTF-8 continuation bytes are always >= 0x80, so
|
||
// multi-byte runes never collide with them.
|
||
func segment(input, separator string) []string {
|
||
if separator == "" {
|
||
return []string{input}
|
||
}
|
||
sep := separator[0]
|
||
|
||
var stack []byte // expected closing brackets
|
||
var parts []string
|
||
lastPos := 0
|
||
|
||
for i := 0; i < len(input); i++ {
|
||
c := input[i]
|
||
|
||
if len(stack) == 0 && c == sep {
|
||
parts = append(parts, input[lastPos:i])
|
||
lastPos = i + 1
|
||
continue
|
||
}
|
||
|
||
switch c {
|
||
case '\\':
|
||
// Next character is escaped; skip it.
|
||
i++
|
||
case '\'', '"':
|
||
// Consume the whole string literal.
|
||
for i++; i < len(input); i++ {
|
||
nc := input[i]
|
||
if nc == '\\' {
|
||
i++
|
||
continue
|
||
}
|
||
if nc == c {
|
||
break
|
||
}
|
||
}
|
||
case '(':
|
||
stack = append(stack, ')')
|
||
case '[':
|
||
stack = append(stack, ']')
|
||
case '{':
|
||
stack = append(stack, '}')
|
||
case ')', ']', '}':
|
||
if len(stack) > 0 && c == stack[len(stack)-1] {
|
||
stack = stack[:len(stack)-1]
|
||
}
|
||
}
|
||
}
|
||
|
||
parts = append(parts, input[lastPos:])
|
||
return parts
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/sort.ts
|
||
|
||
type classOrderEntry struct {
|
||
class string
|
||
order *big.Int // nil for non-Tailwind classes
|
||
}
|
||
|
||
func getClassOrder(ds *DesignSystem, classes []string) []classOrderEntry {
|
||
astNodes, nodeSorting := compileCandidates(classes, ds, nil, true)
|
||
|
||
sorted := map[string]*big.Int{}
|
||
for _, c := range classes {
|
||
sorted[c] = nil
|
||
}
|
||
|
||
idx := big.NewInt(0)
|
||
for _, node := range astNodes {
|
||
meta, ok := nodeSorting[node]
|
||
if !ok || meta.candidate == "" {
|
||
continue
|
||
}
|
||
if sorted[meta.candidate] == nil {
|
||
sorted[meta.candidate] = new(big.Int).Set(idx)
|
||
idx.Add(idx, big.NewInt(1))
|
||
}
|
||
}
|
||
|
||
out := make([]classOrderEntry, len(classes))
|
||
for i, c := range classes {
|
||
out[i] = classOrderEntry{class: c, order: sorted[c]}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/theme.ts
|
||
//
|
||
// The theme stores `--namespace-key` design tokens (from the default theme.css
|
||
// and the project's @theme block) and resolves utility values against them.
|
||
// Insertion order is preserved (the upstream code relies on JS Map order for
|
||
// deterministic emission and namespace iteration).
|
||
|
||
type ThemeOptions int
|
||
|
||
const (
|
||
themeNone ThemeOptions = 0
|
||
themeInline ThemeOptions = 1 << 0
|
||
themeReference ThemeOptions = 1 << 1
|
||
themeDefault ThemeOptions = 1 << 2
|
||
themeStatic ThemeOptions = 1 << 3
|
||
themeUsed ThemeOptions = 1 << 4
|
||
)
|
||
|
||
var ignoredThemeKeyMap = map[string][]string{
|
||
"--font": {"--font-weight", "--font-size"},
|
||
"--inset": {"--inset-shadow", "--inset-ring"},
|
||
"--text": {"--text-color", "--text-decoration-color", "--text-decoration-thickness", "--text-indent", "--text-shadow", "--text-underline-offset"},
|
||
"--grid-column": {"--grid-column-start", "--grid-column-end"},
|
||
"--grid-row": {"--grid-row-start", "--grid-row-end"},
|
||
}
|
||
|
||
func isIgnoredThemeKey(themeKey, namespace string) bool {
|
||
for _, ig := range ignoredThemeKeyMap[namespace] {
|
||
if themeKey == ig || strings.HasPrefix(themeKey, ig+"-") {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
type themeValue struct {
|
||
value string
|
||
options ThemeOptions
|
||
}
|
||
|
||
type Theme struct {
|
||
Prefix string
|
||
values map[string]*themeValue
|
||
order []string
|
||
keyframes []*AstNode
|
||
}
|
||
|
||
func NewTheme() *Theme {
|
||
return &Theme{values: make(map[string]*themeValue)}
|
||
}
|
||
|
||
func sptr(s string) *string { return &s }
|
||
|
||
func (t *Theme) setVal(key string, v *themeValue) {
|
||
if _, ok := t.values[key]; !ok {
|
||
t.order = append(t.order, key)
|
||
}
|
||
t.values[key] = v
|
||
}
|
||
|
||
func (t *Theme) delVal(key string) {
|
||
if _, ok := t.values[key]; !ok {
|
||
return
|
||
}
|
||
delete(t.values, key)
|
||
for i, k := range t.order {
|
||
if k == key {
|
||
t.order = append(t.order[:i], t.order[i+1:]...)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
func (t *Theme) Size() int { return len(t.values) }
|
||
|
||
func (t *Theme) add(key, value string, options ThemeOptions) {
|
||
if strings.HasSuffix(key, "-*") {
|
||
if value != "initial" {
|
||
// Invalid usage upstream throws; we ignore to stay non-fatal.
|
||
return
|
||
}
|
||
if key == "--*" {
|
||
t.values = make(map[string]*themeValue)
|
||
t.order = nil
|
||
} else {
|
||
t.clearNamespace(key[:len(key)-2], themeNone)
|
||
}
|
||
}
|
||
|
||
if options&themeDefault != 0 {
|
||
if existing, ok := t.values[key]; ok && existing.options&themeDefault == 0 {
|
||
return
|
||
}
|
||
}
|
||
|
||
if value == "initial" {
|
||
t.delVal(key)
|
||
} else {
|
||
t.setVal(key, &themeValue{value: value, options: options})
|
||
}
|
||
}
|
||
|
||
func (t *Theme) keysInNamespaces(themeKeys []string) []string {
|
||
var keys []string
|
||
for _, namespace := range themeKeys {
|
||
prefix := namespace + "-"
|
||
for _, key := range t.order {
|
||
if !strings.HasPrefix(key, prefix) {
|
||
continue
|
||
}
|
||
if strings.Contains(key[2:], "--") {
|
||
continue
|
||
}
|
||
if isIgnoredThemeKey(key, namespace) {
|
||
continue
|
||
}
|
||
keys = append(keys, key[len(prefix):])
|
||
}
|
||
}
|
||
return keys
|
||
}
|
||
|
||
func (t *Theme) Get(themeKeys []string) (string, bool) {
|
||
for _, key := range themeKeys {
|
||
if v, ok := t.values[key]; ok {
|
||
return v.value, true
|
||
}
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
func (t *Theme) Has(key string) bool {
|
||
_, ok := t.values[key]
|
||
return ok
|
||
}
|
||
|
||
func (t *Theme) hasDefault(key string) bool {
|
||
return t.getOptions(key)&themeDefault == themeDefault
|
||
}
|
||
|
||
func (t *Theme) getOptions(key string) ThemeOptions {
|
||
key = unescape(t.unprefixKey(key))
|
||
if v, ok := t.values[key]; ok {
|
||
return v.options
|
||
}
|
||
return themeNone
|
||
}
|
||
|
||
func (t *Theme) prefixKey(key string) string {
|
||
if t.Prefix == "" {
|
||
return key
|
||
}
|
||
return "--" + t.Prefix + "-" + key[2:]
|
||
}
|
||
|
||
func (t *Theme) unprefixKey(key string) string {
|
||
if t.Prefix == "" {
|
||
return key
|
||
}
|
||
return "--" + key[3+len(t.Prefix):]
|
||
}
|
||
|
||
func (t *Theme) clearNamespace(namespace string, clearOptions ThemeOptions) {
|
||
ignored := ignoredThemeKeyMap[namespace]
|
||
var toDelete []string
|
||
outer:
|
||
for _, key := range t.order {
|
||
if strings.HasPrefix(key, namespace) {
|
||
if clearOptions != themeNone {
|
||
options := t.getOptions(key)
|
||
if options&clearOptions != clearOptions {
|
||
continue
|
||
}
|
||
}
|
||
for _, ig := range ignored {
|
||
if strings.HasPrefix(key, ig) {
|
||
continue outer
|
||
}
|
||
}
|
||
toDelete = append(toDelete, key)
|
||
}
|
||
}
|
||
for _, key := range toDelete {
|
||
t.delVal(key)
|
||
}
|
||
}
|
||
|
||
func (t *Theme) resolveKey(candidateValue *string, themeKeys []string) string {
|
||
for _, namespace := range themeKeys {
|
||
var themeKey string
|
||
if candidateValue != nil {
|
||
themeKey = namespace + "-" + *candidateValue
|
||
} else {
|
||
themeKey = namespace
|
||
}
|
||
|
||
if _, ok := t.values[themeKey]; !ok {
|
||
if candidateValue != nil && strings.Contains(*candidateValue, ".") {
|
||
themeKey = namespace + "-" + strings.ReplaceAll(*candidateValue, ".", "_")
|
||
if _, ok := t.values[themeKey]; !ok {
|
||
continue
|
||
}
|
||
} else {
|
||
continue
|
||
}
|
||
}
|
||
|
||
if isIgnoredThemeKey(themeKey, namespace) {
|
||
continue
|
||
}
|
||
return themeKey
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func (t *Theme) varFn(themeKey string) string {
|
||
v, ok := t.values[themeKey]
|
||
if !ok {
|
||
return ""
|
||
}
|
||
fallback := ""
|
||
if v.options&themeReference != 0 {
|
||
fallback = v.value
|
||
}
|
||
s := "var(" + escape(t.prefixKey(themeKey))
|
||
if fallback != "" {
|
||
s += ", " + fallback
|
||
}
|
||
s += ")"
|
||
return s
|
||
}
|
||
|
||
func (t *Theme) markUsedVariable(themeKey string) bool {
|
||
key := unescape(t.unprefixKey(themeKey))
|
||
v, ok := t.values[key]
|
||
if !ok {
|
||
return false
|
||
}
|
||
wasUsed := v.options&themeUsed != 0
|
||
v.options |= themeUsed
|
||
return !wasUsed
|
||
}
|
||
|
||
func (t *Theme) resolve(candidateValue *string, themeKeys []string, options ThemeOptions) (string, bool) {
|
||
key := t.resolveKey(candidateValue, themeKeys)
|
||
if key == "" {
|
||
return "", false
|
||
}
|
||
v := t.values[key]
|
||
if (options|v.options)&themeInline != 0 {
|
||
return v.value, true
|
||
}
|
||
return t.varFn(key), true
|
||
}
|
||
|
||
func (t *Theme) resolveValue(candidateValue *string, themeKeys []string) (string, bool) {
|
||
key := t.resolveKey(candidateValue, themeKeys)
|
||
if key == "" {
|
||
return "", false
|
||
}
|
||
return t.values[key].value, true
|
||
}
|
||
|
||
func (t *Theme) resolveWith(candidateValue string, themeKeys []string, nestedKeys []string) (string, map[string]string, bool) {
|
||
key := t.resolveKey(&candidateValue, themeKeys)
|
||
if key == "" {
|
||
return "", nil, false
|
||
}
|
||
|
||
extra := map[string]string{}
|
||
for _, name := range nestedKeys {
|
||
nestedKey := key + name
|
||
nv, ok := t.values[nestedKey]
|
||
if !ok {
|
||
continue
|
||
}
|
||
if nv.options&themeInline != 0 {
|
||
extra[name] = nv.value
|
||
} else {
|
||
extra[name] = t.varFn(nestedKey)
|
||
}
|
||
}
|
||
|
||
v := t.values[key]
|
||
if v.options&themeInline != 0 {
|
||
return v.value, extra, true
|
||
}
|
||
return t.varFn(key), extra, true
|
||
}
|
||
|
||
// ThemeNamespace is the flattened view of a single theme namespace.
|
||
type ThemeNamespace struct {
|
||
m map[string]string
|
||
order []string
|
||
hasNull bool
|
||
nullVal string
|
||
}
|
||
|
||
func (n *ThemeNamespace) Get(key string) (string, bool) {
|
||
v, ok := n.m[key]
|
||
return v, ok
|
||
}
|
||
|
||
func (n *ThemeNamespace) GetNull() (string, bool) {
|
||
return n.nullVal, n.hasNull
|
||
}
|
||
|
||
func (n *ThemeNamespace) set(key, value string) {
|
||
if _, ok := n.m[key]; !ok {
|
||
n.order = append(n.order, key)
|
||
}
|
||
n.m[key] = value
|
||
}
|
||
|
||
func (n *ThemeNamespace) Keys() []string { return n.order }
|
||
|
||
// Values returns the namespace values in insertion order (including the null
|
||
// entry's value, if present, matching JS Map iteration order).
|
||
func (n *ThemeNamespace) Values() []string {
|
||
var out []string
|
||
if n.hasNull {
|
||
out = append(out, n.nullVal)
|
||
}
|
||
for _, k := range n.order {
|
||
out = append(out, n.m[k])
|
||
}
|
||
return out
|
||
}
|
||
|
||
func (t *Theme) namespace(namespace string) *ThemeNamespace {
|
||
ns := &ThemeNamespace{m: map[string]string{}}
|
||
prefix := namespace + "-"
|
||
for _, key := range t.order {
|
||
v := t.values[key]
|
||
switch {
|
||
case key == namespace:
|
||
ns.hasNull = true
|
||
ns.nullVal = v.value
|
||
case strings.HasPrefix(key, prefix+"-"):
|
||
// Preserve `--` prefix for sub-variables (e.g. --text-sm--line-height).
|
||
ns.set(key[len(namespace):], v.value)
|
||
case strings.HasPrefix(key, prefix):
|
||
ns.set(key[len(prefix):], v.value)
|
||
}
|
||
}
|
||
return ns
|
||
}
|
||
|
||
func (t *Theme) addKeyframes(value *AstNode) {
|
||
t.keyframes = append(t.keyframes, value)
|
||
}
|
||
|
||
func (t *Theme) getKeyframes() []*AstNode {
|
||
return t.keyframes
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/utilities.ts
|
||
//
|
||
// The Utilities registry plus the helper builders (staticUtility,
|
||
// functionalUtility, colorUtility, spacingUtility) and the full utility
|
||
// catalog registered by createUtilities.
|
||
//
|
||
// Deviation: the IntelliSense suggestion layer (suggest/getCompletions) is
|
||
// stubbed to no-ops — it does not affect generated CSS, only IDE autocomplete.
|
||
// -mta
|
||
|
||
type utilKind int
|
||
|
||
const (
|
||
utilStatic utilKind = iota
|
||
utilFunctional
|
||
)
|
||
|
||
type UtilityOptions struct {
|
||
Types []string
|
||
}
|
||
|
||
// utilResult models the TS tri-state return of a compile fn:
|
||
//
|
||
// nil -> undefined (skip this utility, try the next)
|
||
// {null:true} -> null (invalid; bail to fallback utilities if typed)
|
||
// {nodes:[...]} -> the produced AST nodes
|
||
type utilResult struct {
|
||
nodes []*AstNode
|
||
null bool
|
||
}
|
||
|
||
func uNodes(nodes ...*AstNode) *utilResult { return &utilResult{nodes: nodes} }
|
||
func uList(nodes []*AstNode) *utilResult { return &utilResult{nodes: nodes} }
|
||
func uNull() *utilResult { return &utilResult{null: true} }
|
||
|
||
type Utility struct {
|
||
kind utilKind
|
||
compileFn func(*Candidate) *utilResult
|
||
options *UtilityOptions
|
||
}
|
||
|
||
type Utilities struct {
|
||
m map[string][]*Utility
|
||
order []string
|
||
}
|
||
|
||
func NewUtilities() *Utilities {
|
||
return &Utilities{m: make(map[string][]*Utility)}
|
||
}
|
||
|
||
func (u *Utilities) addUtility(name string, util *Utility) {
|
||
if _, ok := u.m[name]; !ok {
|
||
u.order = append(u.order, name)
|
||
}
|
||
u.m[name] = append(u.m[name], util)
|
||
}
|
||
|
||
func (u *Utilities) static(name string, fn func(*Candidate) *utilResult) {
|
||
u.addUtility(name, &Utility{kind: utilStatic, compileFn: fn})
|
||
}
|
||
|
||
func (u *Utilities) functional(name string, fn func(*Candidate) *utilResult, options *UtilityOptions) {
|
||
u.addUtility(name, &Utility{kind: utilFunctional, compileFn: fn, options: options})
|
||
}
|
||
|
||
func (u *Utilities) has(name string, kind utilKind) bool {
|
||
fns, ok := u.m[name]
|
||
if !ok {
|
||
return false
|
||
}
|
||
for _, f := range fns {
|
||
if f.kind == kind {
|
||
return true
|
||
}
|
||
}
|
||
return false
|
||
}
|
||
|
||
func (u *Utilities) get(name string) []*Utility { return u.m[name] }
|
||
|
||
func (u *Utilities) keys(kind utilKind) []string {
|
||
var keys []string
|
||
for _, key := range u.order {
|
||
for _, f := range u.m[key] {
|
||
if f.kind == kind {
|
||
keys = append(keys, key)
|
||
break
|
||
}
|
||
}
|
||
}
|
||
return keys
|
||
}
|
||
|
||
// ---- color/alpha helpers ------------------------------------------------
|
||
|
||
func withAlpha(value, alpha string) string {
|
||
if f, ok := jsParseNumber(alpha); ok {
|
||
alpha = jsNumberToString(f*100) + "%"
|
||
}
|
||
if alpha == "100%" {
|
||
return value
|
||
}
|
||
return "color-mix(in oklab, " + value + " " + alpha + ", transparent)"
|
||
}
|
||
|
||
func replaceAlpha(value, alpha string) string {
|
||
if f, ok := jsParseNumber(alpha); ok {
|
||
alpha = jsNumberToString(f*100) + "%"
|
||
}
|
||
return "oklab(from " + value + " l a b / " + alpha + ")"
|
||
}
|
||
|
||
// asColor resolves a color value plus an optional opacity modifier. Returns
|
||
// (value, true) or ("", false) when the modifier is an invalid opacity.
|
||
func asColor(value string, modifier *CandidateModifier, theme *Theme) (string, bool) {
|
||
if modifier == nil {
|
||
return value, true
|
||
}
|
||
if modifier.Kind == modArbitrary {
|
||
return withAlpha(value, modifier.Value), true
|
||
}
|
||
if alpha, ok := theme.resolve(&modifier.Value, []string{"--opacity"}, themeNone); ok && alpha != "" {
|
||
return withAlpha(value, alpha), true
|
||
}
|
||
if !isValidOpacityValue(modifier.Value) {
|
||
return "", false
|
||
}
|
||
return withAlpha(value, modifier.Value+"%"), true
|
||
}
|
||
|
||
func resolveThemeColor(candidate *Candidate, theme *Theme, themeKeys []string) (string, bool) {
|
||
var value string
|
||
var ok bool
|
||
switch candidate.Value.Value {
|
||
case "inherit":
|
||
value, ok = "inherit", true
|
||
case "transparent":
|
||
value, ok = "transparent", true
|
||
case "current":
|
||
value, ok = "currentcolor", true
|
||
default:
|
||
value, ok = theme.resolve(&candidate.Value.Value, themeKeys, themeNone)
|
||
}
|
||
if !ok {
|
||
return "", false
|
||
}
|
||
return asColor(value, candidate.Modifier, theme)
|
||
}
|
||
|
||
func property(ident, initialValue, syntax string) *AstNode {
|
||
syntaxStr := `"*"`
|
||
if syntax != "" {
|
||
syntaxStr = `"` + syntax + `"`
|
||
}
|
||
nodes := []*AstNode{decl("syntax", syntaxStr), decl("inherits", "false")}
|
||
if initialValue != "" {
|
||
nodes = append(nodes, decl("initial-value", initialValue))
|
||
}
|
||
return atRule("@property", ident, nodes...)
|
||
}
|
||
|
||
// ---- utility registration helpers --------------------------------------
|
||
|
||
// utilCtx carries the theme + registry through the catalog registration helpers
|
||
// (the upstream closures over `theme`/`utilities` inside createUtilities).
|
||
type utilCtx struct {
|
||
theme *Theme
|
||
utilities *Utilities
|
||
}
|
||
|
||
// staticDecl is one entry of a static utility: either a property/value pair or
|
||
// a node-producing function.
|
||
type staticDecl struct {
|
||
prop string
|
||
val string
|
||
fn func() *AstNode
|
||
}
|
||
|
||
func sd(prop, val string) staticDecl { return staticDecl{prop: prop, val: val} }
|
||
func sdFn(fn func() *AstNode) staticDecl { return staticDecl{fn: fn} }
|
||
|
||
func (c *utilCtx) staticUtility(className string, decls []staticDecl) {
|
||
c.utilities.static(className, func(_ *Candidate) *utilResult {
|
||
nodes := make([]*AstNode, len(decls))
|
||
for i, d := range decls {
|
||
if d.fn != nil {
|
||
nodes[i] = d.fn()
|
||
} else {
|
||
nodes[i] = decl(d.prop, d.val)
|
||
}
|
||
}
|
||
return uList(nodes)
|
||
})
|
||
}
|
||
|
||
// suggest is a no-op: IntelliSense suggestions don't affect generated CSS.
|
||
func (c *utilCtx) suggest(classRoot string, defns func() []any) {}
|
||
|
||
type utilityDescription struct {
|
||
supportsNegative bool
|
||
supportsFractions bool
|
||
themeKeys []string
|
||
|
||
// defaultValueSet distinguishes "undefined" (false) from an explicit
|
||
// default (true). When set, defaultValue==nil means null.
|
||
defaultValueSet bool
|
||
defaultValue *string
|
||
|
||
staticValues map[string][]*AstNode
|
||
|
||
handleBareValue func(*UtilityValue) (string, bool)
|
||
handleNegativeBareValue func(*UtilityValue) (string, bool)
|
||
handle func(value, dataType string) *utilResult
|
||
}
|
||
|
||
func (c *utilCtx) functionalUtility(classRoot string, desc utilityDescription) {
|
||
make := func(negative bool) func(*Candidate) *utilResult {
|
||
return func(candidate *Candidate) *utilResult {
|
||
var value *string
|
||
dataType := ""
|
||
|
||
if candidate.Value == nil {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if desc.defaultValueSet {
|
||
value = desc.defaultValue
|
||
} else {
|
||
if v, ok := c.theme.resolve(nil, desc.themeKeys, themeNone); ok {
|
||
value = &v
|
||
}
|
||
}
|
||
} else if candidate.Value.Kind == uvArbitrary {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
v := candidate.Value.Value
|
||
value = &v
|
||
dataType = candidate.Value.DataType
|
||
} else {
|
||
key := candidate.Value.Value
|
||
if candidate.Value.Fraction != "" {
|
||
key = candidate.Value.Fraction
|
||
}
|
||
if v, ok := c.theme.resolve(&key, desc.themeKeys, themeNone); ok {
|
||
value = &v
|
||
}
|
||
|
||
if value == nil && desc.supportsFractions && candidate.Value.Fraction != "" {
|
||
fparts := segment(candidate.Value.Fraction, "/")
|
||
if len(fparts) != 2 || !isPositiveInteger(fparts[0]) || !isPositiveInteger(fparts[1]) {
|
||
return nil
|
||
}
|
||
s := "calc(" + fparts[0] + " / " + fparts[1] + " * 100%)"
|
||
value = &s
|
||
}
|
||
|
||
if value == nil && negative && desc.handleNegativeBareValue != nil {
|
||
v, ok := desc.handleNegativeBareValue(candidate.Value)
|
||
var vv *string
|
||
if ok {
|
||
vv = &v
|
||
}
|
||
includesSlash := vv != nil && strings.Contains(*vv, "/")
|
||
if !includesSlash && candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if vv != nil {
|
||
return desc.handle(*vv, "")
|
||
}
|
||
}
|
||
|
||
if value == nil && desc.handleBareValue != nil {
|
||
if v, ok := desc.handleBareValue(candidate.Value); ok {
|
||
value = &v
|
||
}
|
||
includesSlash := value != nil && strings.Contains(*value, "/")
|
||
if !includesSlash && candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
}
|
||
|
||
if value == nil && !negative && desc.staticValues != nil && candidate.Modifier == nil {
|
||
if fb, ok := desc.staticValues[candidate.Value.Value]; ok {
|
||
return uList(cloneAstNodes(fb))
|
||
}
|
||
}
|
||
}
|
||
|
||
if value == nil {
|
||
return nil
|
||
}
|
||
|
||
handleVal := *value
|
||
if negative {
|
||
handleVal = addWhitespaceAroundMathOperators("calc(" + *value + " * -1)")
|
||
}
|
||
return desc.handle(handleVal, dataType)
|
||
}
|
||
}
|
||
|
||
if desc.supportsNegative {
|
||
c.utilities.functional("-"+classRoot, make(true), nil)
|
||
}
|
||
c.utilities.functional(classRoot, make(false), nil)
|
||
}
|
||
|
||
type colorUtilityDescription struct {
|
||
themeKeys []string
|
||
handle func(value string) *utilResult
|
||
}
|
||
|
||
func (c *utilCtx) colorUtility(classRoot string, desc colorUtilityDescription) {
|
||
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
var value string
|
||
var ok bool
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value, ok = asColor(candidate.Value.Value, candidate.Modifier, c.theme)
|
||
} else {
|
||
value, ok = resolveThemeColor(candidate, c.theme, desc.themeKeys)
|
||
}
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return desc.handle(value)
|
||
}, nil)
|
||
}
|
||
|
||
func (c *utilCtx) spacingUtility(name string, themeKeys []string, handle func(value string) *utilResult, opts spacingOpts) {
|
||
if opts.supportsNegative {
|
||
c.utilities.static("-"+name+"-px", func(_ *Candidate) *utilResult { return handle("-1px") })
|
||
}
|
||
c.utilities.static(name+"-px", func(_ *Candidate) *utilResult { return handle("1px") })
|
||
|
||
nullDefault := (*string)(nil)
|
||
c.functionalUtility(name, utilityDescription{
|
||
themeKeys: themeKeys,
|
||
supportsFractions: opts.supportsFractions,
|
||
supportsNegative: opts.supportsNegative,
|
||
defaultValueSet: true,
|
||
defaultValue: nullDefault,
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
|
||
return "", false
|
||
}
|
||
if !isValidSpacingMultiplier(v.Value) {
|
||
return "", false
|
||
}
|
||
return "--spacing(" + v.Value + ")", true
|
||
},
|
||
handleNegativeBareValue: func(v *UtilityValue) (string, bool) {
|
||
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
|
||
return "", false
|
||
}
|
||
if !isValidSpacingMultiplier(v.Value) {
|
||
return "", false
|
||
}
|
||
return "--spacing(-" + v.Value + ")", true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return handle(value) },
|
||
staticValues: opts.staticValues,
|
||
})
|
||
}
|
||
|
||
type spacingOpts struct {
|
||
supportsNegative bool
|
||
supportsFractions bool
|
||
staticValues map[string][]*AstNode
|
||
}
|
||
|
||
// createUtilities builds the full utility registry for a theme.
|
||
func createUtilities(theme *Theme) *Utilities {
|
||
c := &utilCtx{theme: theme, utilities: NewUtilities()}
|
||
registerUtilities(c)
|
||
registerProse(c)
|
||
return c.utilities
|
||
}
|
||
|
||
// Port of the createUtilities() catalog from
|
||
// packages/tailwindcss/src/utilities.ts. Each registration mirrors the upstream
|
||
// staticUtility/functionalUtility/colorUtility/spacingUtility calls. suggest()
|
||
// calls are omitted (IntelliSense only).
|
||
|
||
// bareInteger is the common handleBareValue that accepts a positive integer.
|
||
func bareInteger(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value, true
|
||
}
|
||
|
||
func registerUtilities(c *utilCtx) {
|
||
d := decl
|
||
|
||
c.staticUtility("sr-only", []staticDecl{
|
||
sd("position", "absolute"), sd("width", "1px"), sd("height", "1px"),
|
||
sd("padding", "0"), sd("margin", "-1px"), sd("overflow", "hidden"),
|
||
sd("clip-path", "inset(50%)"), sd("white-space", "nowrap"), sd("border-width", "0"),
|
||
})
|
||
c.staticUtility("not-sr-only", []staticDecl{
|
||
sd("position", "static"), sd("width", "auto"), sd("height", "auto"),
|
||
sd("padding", "0"), sd("margin", "0"), sd("overflow", "visible"),
|
||
sd("clip-path", "none"), sd("white-space", "normal"),
|
||
})
|
||
|
||
c.staticUtility("pointer-events-none", []staticDecl{sd("pointer-events", "none")})
|
||
c.staticUtility("pointer-events-auto", []staticDecl{sd("pointer-events", "auto")})
|
||
|
||
c.staticUtility("visible", []staticDecl{sd("visibility", "visible")})
|
||
c.staticUtility("invisible", []staticDecl{sd("visibility", "hidden")})
|
||
c.staticUtility("collapse", []staticDecl{sd("visibility", "collapse")})
|
||
|
||
c.staticUtility("static", []staticDecl{sd("position", "static")})
|
||
c.staticUtility("fixed", []staticDecl{sd("position", "fixed")})
|
||
c.staticUtility("absolute", []staticDecl{sd("position", "absolute")})
|
||
c.staticUtility("relative", []staticDecl{sd("position", "relative")})
|
||
c.staticUtility("sticky", []staticDecl{sd("position", "sticky")})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"inset", "inset"}, {"inset-x", "inset-inline"}, {"inset-y", "inset-block"},
|
||
{"inset-s", "inset-inline-start"}, {"inset-e", "inset-inline-end"},
|
||
{"inset-bs", "inset-block-start"}, {"inset-be", "inset-block-end"},
|
||
{"top", "top"}, {"right", "right"}, {"bottom", "bottom"}, {"left", "left"},
|
||
} {
|
||
name, prop := pair[0], pair[1]
|
||
c.staticUtility(name+"-auto", []staticDecl{sd(prop, "auto")})
|
||
c.staticUtility(name+"-full", []staticDecl{sd(prop, "100%")})
|
||
c.staticUtility("-"+name+"-full", []staticDecl{sd(prop, "-100%")})
|
||
c.spacingUtility(name, []string{"--inset", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) },
|
||
spacingOpts{supportsNegative: true, supportsFractions: true})
|
||
}
|
||
|
||
c.staticUtility("isolate", []staticDecl{sd("isolation", "isolate")})
|
||
c.staticUtility("isolation-auto", []staticDecl{sd("isolation", "auto")})
|
||
|
||
c.functionalUtility("z", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--z-index"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("z-index", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("z-index", "auto")}},
|
||
})
|
||
|
||
c.functionalUtility("order", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--order"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("order", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"first": {d("order", "-9999")},
|
||
"last": {d("order", "9999")},
|
||
},
|
||
})
|
||
|
||
c.functionalUtility("col", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-column"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-column", "auto")}},
|
||
})
|
||
c.functionalUtility("col-span", utilityDescription{
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(d("grid-column", "span "+value+" / span "+value))
|
||
},
|
||
staticValues: map[string][]*AstNode{"full": {d("grid-column", "1 / -1")}},
|
||
})
|
||
c.functionalUtility("col-start", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-column-start"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column-start", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-column-start", "auto")}},
|
||
})
|
||
c.functionalUtility("col-end", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-column-end"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-column-end", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-column-end", "auto")}},
|
||
})
|
||
|
||
c.functionalUtility("row", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-row"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-row", "auto")}},
|
||
})
|
||
c.functionalUtility("row-span", utilityDescription{
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(d("grid-row", "span "+value+" / span "+value))
|
||
},
|
||
staticValues: map[string][]*AstNode{"full": {d("grid-row", "1 / -1")}},
|
||
})
|
||
c.functionalUtility("row-start", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-row-start"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row-start", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-row-start", "auto")}},
|
||
})
|
||
c.functionalUtility("row-end", utilityDescription{
|
||
supportsNegative: true,
|
||
handleBareValue: bareInteger,
|
||
themeKeys: []string{"--grid-row-end"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-row-end", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("grid-row-end", "auto")}},
|
||
})
|
||
|
||
c.staticUtility("float-start", []staticDecl{sd("float", "inline-start")})
|
||
c.staticUtility("float-end", []staticDecl{sd("float", "inline-end")})
|
||
c.staticUtility("float-right", []staticDecl{sd("float", "right")})
|
||
c.staticUtility("float-left", []staticDecl{sd("float", "left")})
|
||
c.staticUtility("float-none", []staticDecl{sd("float", "none")})
|
||
|
||
c.staticUtility("clear-start", []staticDecl{sd("clear", "inline-start")})
|
||
c.staticUtility("clear-end", []staticDecl{sd("clear", "inline-end")})
|
||
c.staticUtility("clear-right", []staticDecl{sd("clear", "right")})
|
||
c.staticUtility("clear-left", []staticDecl{sd("clear", "left")})
|
||
c.staticUtility("clear-both", []staticDecl{sd("clear", "both")})
|
||
c.staticUtility("clear-none", []staticDecl{sd("clear", "none")})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"m", "margin"}, {"mx", "margin-inline"}, {"my", "margin-block"},
|
||
{"ms", "margin-inline-start"}, {"me", "margin-inline-end"},
|
||
{"mbs", "margin-block-start"}, {"mbe", "margin-block-end"},
|
||
{"mt", "margin-top"}, {"mr", "margin-right"}, {"mb", "margin-bottom"}, {"ml", "margin-left"},
|
||
} {
|
||
namespace, prop := pair[0], pair[1]
|
||
c.staticUtility(namespace+"-auto", []staticDecl{sd(prop, "auto")})
|
||
c.spacingUtility(namespace, []string{"--margin", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) },
|
||
spacingOpts{supportsNegative: true})
|
||
}
|
||
|
||
c.staticUtility("box-border", []staticDecl{sd("box-sizing", "border-box")})
|
||
c.staticUtility("box-content", []staticDecl{sd("box-sizing", "content-box")})
|
||
|
||
registerUtilities2(c)
|
||
}
|
||
|
||
func registerUtilities2(c *utilCtx) {
|
||
d := decl
|
||
|
||
c.functionalUtility("line-clamp", utilityDescription{
|
||
themeKeys: []string{"--line-clamp"},
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(
|
||
d("overflow", "hidden"),
|
||
d("display", "-webkit-box"),
|
||
d("-webkit-box-orient", "vertical"),
|
||
d("-webkit-line-clamp", value),
|
||
)
|
||
},
|
||
staticValues: map[string][]*AstNode{
|
||
"none": {
|
||
d("overflow", "visible"),
|
||
d("display", "block"),
|
||
d("-webkit-box-orient", "horizontal"),
|
||
d("-webkit-line-clamp", "unset"),
|
||
},
|
||
},
|
||
})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"block", "block"}, {"inline-block", "inline-block"}, {"inline", "inline"},
|
||
{"hidden", "none"}, {"inline-flex", "inline-flex"}, {"table", "table"},
|
||
{"inline-table", "inline-table"}, {"table-caption", "table-caption"},
|
||
{"table-cell", "table-cell"}, {"table-column", "table-column"},
|
||
{"table-column-group", "table-column-group"}, {"table-footer-group", "table-footer-group"},
|
||
{"table-header-group", "table-header-group"}, {"table-row-group", "table-row-group"},
|
||
{"table-row", "table-row"}, {"flow-root", "flow-root"}, {"flex", "flex"},
|
||
{"grid", "grid"}, {"inline-grid", "inline-grid"}, {"contents", "contents"},
|
||
{"list-item", "list-item"},
|
||
} {
|
||
c.staticUtility(pair[0], []staticDecl{sd("display", pair[1])})
|
||
}
|
||
|
||
c.staticUtility("field-sizing-content", []staticDecl{sd("field-sizing", "content")})
|
||
c.staticUtility("field-sizing-fixed", []staticDecl{sd("field-sizing", "fixed")})
|
||
|
||
c.functionalUtility("aspect", utilityDescription{
|
||
themeKeys: []string{"--aspect"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if v.Fraction == "" {
|
||
return "", false
|
||
}
|
||
fparts := segment(v.Fraction, "/")
|
||
if len(fparts) != 2 || !isValidSpacingMultiplier(fparts[0]) || !isValidSpacingMultiplier(fparts[1]) {
|
||
return "", false
|
||
}
|
||
return v.Fraction, true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("aspect-ratio", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"auto": {d("aspect-ratio", "auto")},
|
||
"square": {d("aspect-ratio", "1 / 1")},
|
||
},
|
||
})
|
||
|
||
// size / w / h / min / max statics
|
||
for _, pair := range [][2]string{
|
||
{"full", "100%"}, {"svw", "100svw"}, {"lvw", "100lvw"}, {"dvw", "100dvw"},
|
||
{"svh", "100svh"}, {"lvh", "100lvh"}, {"dvh", "100dvh"},
|
||
{"min", "min-content"}, {"max", "max-content"}, {"fit", "fit-content"},
|
||
} {
|
||
key, value := pair[0], pair[1]
|
||
c.staticUtility("size-"+key, []staticDecl{sd("--tw-sort", "size"), sd("width", value), sd("height", value)})
|
||
c.staticUtility("w-"+key, []staticDecl{sd("width", value)})
|
||
c.staticUtility("h-"+key, []staticDecl{sd("height", value)})
|
||
c.staticUtility("min-w-"+key, []staticDecl{sd("min-width", value)})
|
||
c.staticUtility("min-h-"+key, []staticDecl{sd("min-height", value)})
|
||
c.staticUtility("max-w-"+key, []staticDecl{sd("max-width", value)})
|
||
c.staticUtility("max-h-"+key, []staticDecl{sd("max-height", value)})
|
||
}
|
||
|
||
c.staticUtility("size-auto", []staticDecl{sd("--tw-sort", "size"), sd("width", "auto"), sd("height", "auto")})
|
||
c.staticUtility("w-auto", []staticDecl{sd("width", "auto")})
|
||
c.staticUtility("h-auto", []staticDecl{sd("height", "auto")})
|
||
c.staticUtility("min-w-auto", []staticDecl{sd("min-width", "auto")})
|
||
c.staticUtility("min-h-auto", []staticDecl{sd("min-height", "auto")})
|
||
|
||
c.staticUtility("h-lh", []staticDecl{sd("height", "1lh")})
|
||
c.staticUtility("min-h-lh", []staticDecl{sd("min-height", "1lh")})
|
||
c.staticUtility("max-h-lh", []staticDecl{sd("max-height", "1lh")})
|
||
|
||
c.staticUtility("w-screen", []staticDecl{sd("width", "100vw")})
|
||
c.staticUtility("min-w-screen", []staticDecl{sd("min-width", "100vw")})
|
||
c.staticUtility("max-w-screen", []staticDecl{sd("max-width", "100vw")})
|
||
c.staticUtility("h-screen", []staticDecl{sd("height", "100vh")})
|
||
c.staticUtility("min-h-screen", []staticDecl{sd("min-height", "100vh")})
|
||
c.staticUtility("max-h-screen", []staticDecl{sd("max-height", "100vh")})
|
||
|
||
c.staticUtility("max-w-none", []staticDecl{sd("max-width", "none")})
|
||
c.staticUtility("max-h-none", []staticDecl{sd("max-height", "none")})
|
||
|
||
c.spacingUtility("size", []string{"--size", "--spacing"},
|
||
func(value string) *utilResult {
|
||
return uNodes(d("--tw-sort", "size"), d("width", value), d("height", value))
|
||
}, spacingOpts{supportsFractions: true})
|
||
|
||
for _, e := range []struct {
|
||
name string
|
||
namespaces []string
|
||
property string
|
||
}{
|
||
{"w", []string{"--width", "--spacing", "--container"}, "width"},
|
||
{"min-w", []string{"--min-width", "--spacing", "--container"}, "min-width"},
|
||
{"max-w", []string{"--max-width", "--spacing", "--container"}, "max-width"},
|
||
{"h", []string{"--height", "--spacing"}, "height"},
|
||
{"min-h", []string{"--min-height", "--height", "--spacing"}, "min-height"},
|
||
{"max-h", []string{"--max-height", "--height", "--spacing"}, "max-height"},
|
||
} {
|
||
prop := e.property
|
||
c.spacingUtility(e.name, e.namespaces,
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) },
|
||
spacingOpts{supportsFractions: true})
|
||
}
|
||
|
||
// inline-size / block-size
|
||
for _, pair := range [][2]string{
|
||
{"full", "100%"}, {"min", "min-content"}, {"max", "max-content"}, {"fit", "fit-content"},
|
||
} {
|
||
key, value := pair[0], pair[1]
|
||
c.staticUtility("inline-"+key, []staticDecl{sd("inline-size", value)})
|
||
c.staticUtility("block-"+key, []staticDecl{sd("block-size", value)})
|
||
c.staticUtility("min-inline-"+key, []staticDecl{sd("min-inline-size", value)})
|
||
c.staticUtility("min-block-"+key, []staticDecl{sd("min-block-size", value)})
|
||
c.staticUtility("max-inline-"+key, []staticDecl{sd("max-inline-size", value)})
|
||
c.staticUtility("max-block-"+key, []staticDecl{sd("max-block-size", value)})
|
||
}
|
||
for _, pair := range [][2]string{{"svw", "100svw"}, {"lvw", "100lvw"}, {"dvw", "100dvw"}} {
|
||
key, value := pair[0], pair[1]
|
||
c.staticUtility("inline-"+key, []staticDecl{sd("inline-size", value)})
|
||
c.staticUtility("min-inline-"+key, []staticDecl{sd("min-inline-size", value)})
|
||
c.staticUtility("max-inline-"+key, []staticDecl{sd("max-inline-size", value)})
|
||
}
|
||
for _, pair := range [][2]string{{"svh", "100svh"}, {"lvh", "100lvh"}, {"dvh", "100dvh"}} {
|
||
key, value := pair[0], pair[1]
|
||
c.staticUtility("block-"+key, []staticDecl{sd("block-size", value)})
|
||
c.staticUtility("min-block-"+key, []staticDecl{sd("min-block-size", value)})
|
||
c.staticUtility("max-block-"+key, []staticDecl{sd("max-block-size", value)})
|
||
}
|
||
|
||
c.staticUtility("inline-auto", []staticDecl{sd("inline-size", "auto")})
|
||
c.staticUtility("block-auto", []staticDecl{sd("block-size", "auto")})
|
||
c.staticUtility("min-inline-auto", []staticDecl{sd("min-inline-size", "auto")})
|
||
c.staticUtility("min-block-auto", []staticDecl{sd("min-block-size", "auto")})
|
||
|
||
c.staticUtility("block-lh", []staticDecl{sd("block-size", "1lh")})
|
||
c.staticUtility("min-block-lh", []staticDecl{sd("min-block-size", "1lh")})
|
||
c.staticUtility("max-block-lh", []staticDecl{sd("max-block-size", "1lh")})
|
||
|
||
c.staticUtility("inline-screen", []staticDecl{sd("inline-size", "100vw")})
|
||
c.staticUtility("min-inline-screen", []staticDecl{sd("min-inline-size", "100vw")})
|
||
c.staticUtility("max-inline-screen", []staticDecl{sd("max-inline-size", "100vw")})
|
||
c.staticUtility("block-screen", []staticDecl{sd("block-size", "100vh")})
|
||
c.staticUtility("min-block-screen", []staticDecl{sd("min-block-size", "100vh")})
|
||
c.staticUtility("max-block-screen", []staticDecl{sd("max-block-size", "100vh")})
|
||
|
||
c.staticUtility("max-inline-none", []staticDecl{sd("max-inline-size", "none")})
|
||
c.staticUtility("max-block-none", []staticDecl{sd("max-block-size", "none")})
|
||
|
||
for _, e := range []struct {
|
||
name string
|
||
namespaces []string
|
||
property string
|
||
}{
|
||
{"inline", []string{"--spacing", "--container"}, "inline-size"},
|
||
{"min-inline", []string{"--spacing", "--container"}, "min-inline-size"},
|
||
{"max-inline", []string{"--spacing", "--container"}, "max-inline-size"},
|
||
{"block", []string{"--spacing"}, "block-size"},
|
||
{"min-block", []string{"--spacing"}, "min-block-size"},
|
||
{"max-block", []string{"--spacing"}, "max-block-size"},
|
||
} {
|
||
prop := e.property
|
||
c.spacingUtility(e.name, e.namespaces,
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) },
|
||
spacingOpts{supportsFractions: true})
|
||
}
|
||
|
||
c.utilities.static("container", func(_ *Candidate) *utilResult {
|
||
breakpoints := c.theme.namespace("--breakpoint").Values()
|
||
sort.SliceStable(breakpoints, func(i, j int) bool {
|
||
return compareBreakpoints(breakpoints[i], breakpoints[j], "asc") < 0
|
||
})
|
||
decls := []*AstNode{d("--tw-sort", "--tw-container-component"), d("width", "100%")}
|
||
for _, bp := range breakpoints {
|
||
decls = append(decls, atRule("@media", "(width >= "+bp+")", d("max-width", bp)))
|
||
}
|
||
return uList(decls)
|
||
})
|
||
|
||
c.staticUtility("flex-auto", []staticDecl{sd("flex", "auto")})
|
||
c.staticUtility("flex-initial", []staticDecl{sd("flex", "0 auto")})
|
||
c.staticUtility("flex-none", []staticDecl{sd("flex", "none")})
|
||
|
||
c.utilities.functional("flex", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("flex", candidate.Value.Value))
|
||
}
|
||
if candidate.Value.Fraction != "" {
|
||
fparts := segment(candidate.Value.Fraction, "/")
|
||
if len(fparts) != 2 || !isPositiveInteger(fparts[0]) || !isPositiveInteger(fparts[1]) {
|
||
return nil
|
||
}
|
||
return uNodes(d("flex", "calc("+candidate.Value.Fraction+" * 100%)"))
|
||
}
|
||
if isPositiveInteger(candidate.Value.Value) {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("flex", candidate.Value.Value))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.functionalUtility("shrink", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: sptr("1"),
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("flex-shrink", value)) },
|
||
})
|
||
c.functionalUtility("grow", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: sptr("1"),
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("flex-grow", value)) },
|
||
})
|
||
|
||
c.staticUtility("basis-auto", []staticDecl{sd("flex-basis", "auto")})
|
||
c.staticUtility("basis-full", []staticDecl{sd("flex-basis", "100%")})
|
||
c.spacingUtility("basis", []string{"--flex-basis", "--spacing", "--container"},
|
||
func(value string) *utilResult { return uNodes(d("flex-basis", value)) },
|
||
spacingOpts{supportsFractions: true})
|
||
|
||
c.staticUtility("table-auto", []staticDecl{sd("table-layout", "auto")})
|
||
c.staticUtility("table-fixed", []staticDecl{sd("table-layout", "fixed")})
|
||
c.staticUtility("caption-top", []staticDecl{sd("caption-side", "top")})
|
||
c.staticUtility("caption-bottom", []staticDecl{sd("caption-side", "bottom")})
|
||
c.staticUtility("border-collapse", []staticDecl{sd("border-collapse", "collapse")})
|
||
c.staticUtility("border-separate", []staticDecl{sd("border-collapse", "separate")})
|
||
|
||
borderSpacingProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-border-spacing-x", "0", "<length>"),
|
||
property("--tw-border-spacing-y", "0", "<length>"),
|
||
})
|
||
}
|
||
c.spacingUtility("border-spacing", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
|
||
return uNodes(borderSpacingProperties(),
|
||
d("--tw-border-spacing-x", value), d("--tw-border-spacing-y", value),
|
||
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
|
||
}, spacingOpts{})
|
||
c.spacingUtility("border-spacing-x", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
|
||
return uNodes(borderSpacingProperties(),
|
||
d("--tw-border-spacing-x", value),
|
||
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
|
||
}, spacingOpts{})
|
||
c.spacingUtility("border-spacing-y", []string{"--border-spacing", "--spacing"}, func(value string) *utilResult {
|
||
return uNodes(borderSpacingProperties(),
|
||
d("--tw-border-spacing-y", value),
|
||
d("border-spacing", "var(--tw-border-spacing-x) var(--tw-border-spacing-y)"))
|
||
}, spacingOpts{})
|
||
|
||
registerUtilities3(c)
|
||
}
|
||
|
||
func registerUtilities3(c *utilCtx) {
|
||
d := decl
|
||
|
||
originStatics := func(prop string) map[string][]*AstNode {
|
||
return map[string][]*AstNode{
|
||
"center": {d(prop, "center")},
|
||
"top": {d(prop, "top")},
|
||
"top-right": {d(prop, "100% 0")},
|
||
"right": {d(prop, "100%")},
|
||
"bottom-right": {d(prop, "100% 100%")},
|
||
"bottom": {d(prop, "bottom")},
|
||
"bottom-left": {d(prop, "0 100%")},
|
||
"left": {d(prop, "0")},
|
||
"top-left": {d(prop, "0 0")},
|
||
}
|
||
}
|
||
|
||
c.functionalUtility("origin", utilityDescription{
|
||
themeKeys: []string{"--transform-origin"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("transform-origin", value)) },
|
||
staticValues: originStatics("transform-origin"),
|
||
})
|
||
c.functionalUtility("perspective-origin", utilityDescription{
|
||
themeKeys: []string{"--perspective-origin"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("perspective-origin", value)) },
|
||
staticValues: originStatics("perspective-origin"),
|
||
})
|
||
|
||
c.functionalUtility("perspective", utilityDescription{
|
||
themeKeys: []string{"--perspective"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("perspective", value)) },
|
||
staticValues: map[string][]*AstNode{"none": {d("perspective", "none")}},
|
||
})
|
||
|
||
translateProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-translate-x", "0", ""),
|
||
property("--tw-translate-y", "0", ""),
|
||
property("--tw-translate-z", "0", ""),
|
||
})
|
||
}
|
||
|
||
c.staticUtility("translate-none", []staticDecl{sd("translate", "none")})
|
||
c.staticUtility("-translate-full", []staticDecl{
|
||
sdFn(translateProperties),
|
||
sd("--tw-translate-x", "-100%"), sd("--tw-translate-y", "-100%"),
|
||
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
|
||
})
|
||
c.staticUtility("translate-full", []staticDecl{
|
||
sdFn(translateProperties),
|
||
sd("--tw-translate-x", "100%"), sd("--tw-translate-y", "100%"),
|
||
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
|
||
})
|
||
|
||
c.spacingUtility("translate", []string{"--translate", "--spacing"},
|
||
func(value string) *utilResult {
|
||
return uNodes(translateProperties(),
|
||
d("--tw-translate-x", value), d("--tw-translate-y", value),
|
||
d("translate", "var(--tw-translate-x) var(--tw-translate-y)"))
|
||
}, spacingOpts{supportsNegative: true, supportsFractions: true})
|
||
|
||
for _, axis := range []string{"x", "y"} {
|
||
ax := axis
|
||
c.staticUtility("-translate-"+ax+"-full", []staticDecl{
|
||
sdFn(translateProperties),
|
||
sd("--tw-translate-"+ax, "-100%"),
|
||
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
|
||
})
|
||
c.staticUtility("translate-"+ax+"-full", []staticDecl{
|
||
sdFn(translateProperties),
|
||
sd("--tw-translate-"+ax, "100%"),
|
||
sd("translate", "var(--tw-translate-x) var(--tw-translate-y)"),
|
||
})
|
||
c.spacingUtility("translate-"+ax, []string{"--translate", "--spacing"},
|
||
func(value string) *utilResult {
|
||
return uNodes(translateProperties(),
|
||
d("--tw-translate-"+ax, value),
|
||
d("translate", "var(--tw-translate-x) var(--tw-translate-y)"))
|
||
}, spacingOpts{supportsNegative: true, supportsFractions: true})
|
||
}
|
||
|
||
c.spacingUtility("translate-z", []string{"--translate", "--spacing"},
|
||
func(value string) *utilResult {
|
||
return uNodes(translateProperties(),
|
||
d("--tw-translate-z", value),
|
||
d("translate", "var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)"))
|
||
}, spacingOpts{supportsNegative: true})
|
||
|
||
c.staticUtility("translate-3d", []staticDecl{
|
||
sdFn(translateProperties),
|
||
sd("translate", "var(--tw-translate-x) var(--tw-translate-y) var(--tw-translate-z)"),
|
||
})
|
||
|
||
scaleProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-scale-x", "1", ""),
|
||
property("--tw-scale-y", "1", ""),
|
||
property("--tw-scale-z", "1", ""),
|
||
})
|
||
}
|
||
|
||
c.staticUtility("scale-none", []staticDecl{sd("scale", "none")})
|
||
|
||
handleScale := func(negative bool) func(*Candidate) *utilResult {
|
||
return func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil || candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
if negative {
|
||
value = "calc(" + value + " * -1)"
|
||
}
|
||
return uNodes(d("scale", value))
|
||
}
|
||
value, ok := c.theme.resolve(&candidate.Value.Value, []string{"--scale"}, themeNone)
|
||
if !ok && isPositiveInteger(candidate.Value.Value) {
|
||
value = candidate.Value.Value + "%"
|
||
ok = true
|
||
}
|
||
if !ok {
|
||
return nil
|
||
}
|
||
if negative {
|
||
value = "calc(" + value + " * -1)"
|
||
}
|
||
return uNodes(scaleProperties(),
|
||
d("--tw-scale-x", value), d("--tw-scale-y", value), d("--tw-scale-z", value),
|
||
d("scale", "var(--tw-scale-x) var(--tw-scale-y)"))
|
||
}
|
||
}
|
||
c.utilities.functional("-scale", handleScale(true), nil)
|
||
c.utilities.functional("scale", handleScale(false), nil)
|
||
|
||
for _, axis := range []string{"x", "y", "z"} {
|
||
ax := axis
|
||
zSuffix := ""
|
||
if ax == "z" {
|
||
zSuffix = " var(--tw-scale-z)"
|
||
}
|
||
c.functionalUtility("scale-"+ax, utilityDescription{
|
||
supportsNegative: true,
|
||
themeKeys: []string{"--scale"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "%", true
|
||
},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(scaleProperties(),
|
||
d("--tw-scale-"+ax, value),
|
||
d("scale", "var(--tw-scale-x) var(--tw-scale-y)"+zSuffix))
|
||
},
|
||
})
|
||
}
|
||
|
||
c.staticUtility("scale-3d", []staticDecl{
|
||
sdFn(scaleProperties),
|
||
sd("scale", "var(--tw-scale-x) var(--tw-scale-y) var(--tw-scale-z)"),
|
||
})
|
||
|
||
registerUtilities4(c)
|
||
}
|
||
|
||
func registerUtilities4(c *utilCtx) {
|
||
d := decl
|
||
|
||
c.staticUtility("rotate-none", []staticDecl{sd("rotate", "none")})
|
||
|
||
handleRotate := func(negative bool) func(*Candidate) *utilResult {
|
||
return func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil || candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
var value string
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value = candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"angle", "vector"})
|
||
}
|
||
if typ == "vector" {
|
||
return uNodes(d("rotate", value+" var(--tw-rotate)"))
|
||
} else if typ != "angle" {
|
||
if negative {
|
||
return uNodes(d("rotate", "calc("+value+" * -1)"))
|
||
}
|
||
return uNodes(d("rotate", value))
|
||
}
|
||
} else {
|
||
v, ok := c.theme.resolve(&candidate.Value.Value, []string{"--rotate"}, themeNone)
|
||
if !ok && isPositiveInteger(candidate.Value.Value) {
|
||
v = candidate.Value.Value + "deg"
|
||
ok = true
|
||
}
|
||
if !ok {
|
||
return nil
|
||
}
|
||
value = v
|
||
}
|
||
if negative {
|
||
return uNodes(d("rotate", "calc("+value+" * -1)"))
|
||
}
|
||
return uNodes(d("rotate", value))
|
||
}
|
||
}
|
||
c.utilities.functional("-rotate", handleRotate(true), nil)
|
||
c.utilities.functional("rotate", handleRotate(false), nil)
|
||
|
||
transformValue := strings.Join([]string{
|
||
"var(--tw-rotate-x,)", "var(--tw-rotate-y,)", "var(--tw-rotate-z,)",
|
||
"var(--tw-skew-x,)", "var(--tw-skew-y,)",
|
||
}, " ")
|
||
transformProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-rotate-x", "", ""), property("--tw-rotate-y", "", ""),
|
||
property("--tw-rotate-z", "", ""), property("--tw-skew-x", "", ""),
|
||
property("--tw-skew-y", "", ""),
|
||
})
|
||
}
|
||
|
||
for _, axis := range []string{"x", "y", "z"} {
|
||
ax := axis
|
||
up := strings.ToUpper(ax)
|
||
c.functionalUtility("rotate-"+ax, utilityDescription{
|
||
supportsNegative: true,
|
||
themeKeys: []string{"--rotate"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "deg", true
|
||
},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(transformProperties(),
|
||
d("--tw-rotate-"+ax, "rotate"+up+"("+value+")"),
|
||
d("transform", transformValue))
|
||
},
|
||
})
|
||
}
|
||
|
||
skewBare := func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "deg", true
|
||
}
|
||
c.functionalUtility("skew", utilityDescription{
|
||
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(transformProperties(),
|
||
d("--tw-skew-x", "skewX("+value+")"), d("--tw-skew-y", "skewY("+value+")"),
|
||
d("transform", transformValue))
|
||
},
|
||
})
|
||
c.functionalUtility("skew-x", utilityDescription{
|
||
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(transformProperties(), d("--tw-skew-x", "skewX("+value+")"), d("transform", transformValue))
|
||
},
|
||
})
|
||
c.functionalUtility("skew-y", utilityDescription{
|
||
supportsNegative: true, themeKeys: []string{"--skew"}, handleBareValue: skewBare,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(transformProperties(), d("--tw-skew-y", "skewY("+value+")"), d("transform", transformValue))
|
||
},
|
||
})
|
||
|
||
c.utilities.functional("transform", func(candidate *Candidate) *utilResult {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value := ""
|
||
set := false
|
||
if candidate.Value == nil {
|
||
value = transformValue
|
||
set = true
|
||
} else if candidate.Value.Kind == uvArbitrary {
|
||
value = candidate.Value.Value
|
||
set = true
|
||
}
|
||
if !set {
|
||
return nil
|
||
}
|
||
return uNodes(transformProperties(), d("transform", value))
|
||
}, nil)
|
||
|
||
c.staticUtility("transform-cpu", []staticDecl{sd("transform", transformValue)})
|
||
c.staticUtility("transform-gpu", []staticDecl{sd("transform", "translateZ(0) "+transformValue)})
|
||
c.staticUtility("transform-none", []staticDecl{sd("transform", "none")})
|
||
|
||
c.functionalUtility("zoom", utilityDescription{
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "%", true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("zoom", value)) },
|
||
})
|
||
|
||
c.staticUtility("transform-flat", []staticDecl{sd("transform-style", "flat")})
|
||
c.staticUtility("transform-3d", []staticDecl{sd("transform-style", "preserve-3d")})
|
||
c.staticUtility("transform-content", []staticDecl{sd("transform-box", "content-box")})
|
||
c.staticUtility("transform-border", []staticDecl{sd("transform-box", "border-box")})
|
||
c.staticUtility("transform-fill", []staticDecl{sd("transform-box", "fill-box")})
|
||
c.staticUtility("transform-stroke", []staticDecl{sd("transform-box", "stroke-box")})
|
||
c.staticUtility("transform-view", []staticDecl{sd("transform-box", "view-box")})
|
||
c.staticUtility("backface-visible", []staticDecl{sd("backface-visibility", "visible")})
|
||
c.staticUtility("backface-hidden", []staticDecl{sd("backface-visibility", "hidden")})
|
||
|
||
for _, value := range []string{
|
||
"auto", "default", "pointer", "wait", "text", "move", "help", "not-allowed", "none",
|
||
"context-menu", "progress", "cell", "crosshair", "vertical-text", "alias", "copy",
|
||
"no-drop", "grab", "grabbing", "all-scroll", "col-resize", "row-resize", "n-resize",
|
||
"e-resize", "s-resize", "w-resize", "ne-resize", "nw-resize", "se-resize", "sw-resize",
|
||
"ew-resize", "ns-resize", "nesw-resize", "nwse-resize", "zoom-in", "zoom-out",
|
||
} {
|
||
c.staticUtility("cursor-"+value, []staticDecl{sd("cursor", value)})
|
||
}
|
||
c.functionalUtility("cursor", utilityDescription{
|
||
themeKeys: []string{"--cursor"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("cursor", value)) },
|
||
})
|
||
|
||
for _, value := range []string{"auto", "none", "manipulation"} {
|
||
c.staticUtility("touch-"+value, []staticDecl{sd("touch-action", value)})
|
||
}
|
||
touchProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-pan-x", "", ""), property("--tw-pan-y", "", ""), property("--tw-pinch-zoom", "", "")})
|
||
}
|
||
touchAction := "var(--tw-pan-x,) var(--tw-pan-y,) var(--tw-pinch-zoom,)"
|
||
for _, value := range []string{"x", "left", "right"} {
|
||
c.staticUtility("touch-pan-"+value, []staticDecl{sdFn(touchProperties), sd("--tw-pan-x", "pan-"+value), sd("touch-action", touchAction)})
|
||
}
|
||
for _, value := range []string{"y", "up", "down"} {
|
||
c.staticUtility("touch-pan-"+value, []staticDecl{sdFn(touchProperties), sd("--tw-pan-y", "pan-"+value), sd("touch-action", touchAction)})
|
||
}
|
||
c.staticUtility("touch-pinch-zoom", []staticDecl{sdFn(touchProperties), sd("--tw-pinch-zoom", "pinch-zoom"), sd("touch-action", touchAction)})
|
||
|
||
for _, value := range []string{"none", "text", "all", "auto"} {
|
||
c.staticUtility("select-"+value, []staticDecl{sd("-webkit-user-select", value), sd("user-select", value)})
|
||
}
|
||
|
||
c.staticUtility("resize-none", []staticDecl{sd("resize", "none")})
|
||
c.staticUtility("resize-x", []staticDecl{sd("resize", "horizontal")})
|
||
c.staticUtility("resize-y", []staticDecl{sd("resize", "vertical")})
|
||
c.staticUtility("resize", []staticDecl{sd("resize", "both")})
|
||
|
||
c.staticUtility("snap-none", []staticDecl{sd("scroll-snap-type", "none")})
|
||
snapProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-scroll-snap-strictness", "proximity", "*")})
|
||
}
|
||
for _, value := range []string{"x", "y", "both"} {
|
||
c.staticUtility("snap-"+value, []staticDecl{sdFn(snapProperties), sd("scroll-snap-type", value+" var(--tw-scroll-snap-strictness)")})
|
||
}
|
||
c.staticUtility("snap-mandatory", []staticDecl{sdFn(snapProperties), sd("--tw-scroll-snap-strictness", "mandatory")})
|
||
c.staticUtility("snap-proximity", []staticDecl{sdFn(snapProperties), sd("--tw-scroll-snap-strictness", "proximity")})
|
||
c.staticUtility("snap-align-none", []staticDecl{sd("scroll-snap-align", "none")})
|
||
c.staticUtility("snap-start", []staticDecl{sd("scroll-snap-align", "start")})
|
||
c.staticUtility("snap-end", []staticDecl{sd("scroll-snap-align", "end")})
|
||
c.staticUtility("snap-center", []staticDecl{sd("scroll-snap-align", "center")})
|
||
c.staticUtility("snap-normal", []staticDecl{sd("scroll-snap-stop", "normal")})
|
||
c.staticUtility("snap-always", []staticDecl{sd("scroll-snap-stop", "always")})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"scroll-m", "scroll-margin"}, {"scroll-mx", "scroll-margin-inline"}, {"scroll-my", "scroll-margin-block"},
|
||
{"scroll-ms", "scroll-margin-inline-start"}, {"scroll-me", "scroll-margin-inline-end"},
|
||
{"scroll-mbs", "scroll-margin-block-start"}, {"scroll-mbe", "scroll-margin-block-end"},
|
||
{"scroll-mt", "scroll-margin-top"}, {"scroll-mr", "scroll-margin-right"},
|
||
{"scroll-mb", "scroll-margin-bottom"}, {"scroll-ml", "scroll-margin-left"},
|
||
} {
|
||
prop := pair[1]
|
||
c.spacingUtility(pair[0], []string{"--scroll-margin", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{supportsNegative: true})
|
||
}
|
||
for _, pair := range [][2]string{
|
||
{"scroll-p", "scroll-padding"}, {"scroll-px", "scroll-padding-inline"}, {"scroll-py", "scroll-padding-block"},
|
||
{"scroll-ps", "scroll-padding-inline-start"}, {"scroll-pe", "scroll-padding-inline-end"},
|
||
{"scroll-pbs", "scroll-padding-block-start"}, {"scroll-pbe", "scroll-padding-block-end"},
|
||
{"scroll-pt", "scroll-padding-top"}, {"scroll-pr", "scroll-padding-right"},
|
||
{"scroll-pb", "scroll-padding-bottom"}, {"scroll-pl", "scroll-padding-left"},
|
||
} {
|
||
prop := pair[1]
|
||
c.spacingUtility(pair[0], []string{"--scroll-padding", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{})
|
||
}
|
||
|
||
c.staticUtility("list-inside", []staticDecl{sd("list-style-position", "inside")})
|
||
c.staticUtility("list-outside", []staticDecl{sd("list-style-position", "outside")})
|
||
c.functionalUtility("list", utilityDescription{
|
||
themeKeys: []string{"--list-style-type"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("list-style-type", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"none": {d("list-style-type", "none")}, "disc": {d("list-style-type", "disc")}, "decimal": {d("list-style-type", "decimal")},
|
||
},
|
||
})
|
||
c.functionalUtility("list-image", utilityDescription{
|
||
themeKeys: []string{"--list-style-image"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("list-style-image", value)) },
|
||
staticValues: map[string][]*AstNode{"none": {d("list-style-image", "none")}},
|
||
})
|
||
|
||
c.staticUtility("appearance-none", []staticDecl{sd("appearance", "none")})
|
||
c.staticUtility("appearance-auto", []staticDecl{sd("appearance", "auto")})
|
||
c.staticUtility("scheme-normal", []staticDecl{sd("color-scheme", "normal")})
|
||
c.staticUtility("scheme-dark", []staticDecl{sd("color-scheme", "dark")})
|
||
c.staticUtility("scheme-light", []staticDecl{sd("color-scheme", "light")})
|
||
c.staticUtility("scheme-light-dark", []staticDecl{sd("color-scheme", "light dark")})
|
||
c.staticUtility("scheme-only-dark", []staticDecl{sd("color-scheme", "only dark")})
|
||
c.staticUtility("scheme-only-light", []staticDecl{sd("color-scheme", "only light")})
|
||
|
||
c.functionalUtility("columns", utilityDescription{
|
||
themeKeys: []string{"--columns", "--container"},
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("columns", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("columns", "auto")}},
|
||
})
|
||
|
||
for _, value := range []string{"auto", "avoid", "all", "avoid-page", "page", "left", "right", "column"} {
|
||
c.staticUtility("break-before-"+value, []staticDecl{sd("break-before", value)})
|
||
}
|
||
for _, value := range []string{"auto", "avoid", "avoid-page", "avoid-column"} {
|
||
c.staticUtility("break-inside-"+value, []staticDecl{sd("break-inside", value)})
|
||
}
|
||
for _, value := range []string{"auto", "avoid", "all", "avoid-page", "page", "left", "right", "column"} {
|
||
c.staticUtility("break-after-"+value, []staticDecl{sd("break-after", value)})
|
||
}
|
||
|
||
c.staticUtility("grid-flow-row", []staticDecl{sd("grid-auto-flow", "row")})
|
||
c.staticUtility("grid-flow-col", []staticDecl{sd("grid-auto-flow", "column")})
|
||
c.staticUtility("grid-flow-dense", []staticDecl{sd("grid-auto-flow", "dense")})
|
||
c.staticUtility("grid-flow-row-dense", []staticDecl{sd("grid-auto-flow", "row dense")})
|
||
c.staticUtility("grid-flow-col-dense", []staticDecl{sd("grid-auto-flow", "column dense")})
|
||
|
||
autoTrackBare := func(v *UtilityValue) (string, bool) {
|
||
if _, ok := c.theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
|
||
return "", false
|
||
}
|
||
if !isValidSpacingMultiplier(v.Value) {
|
||
return "", false
|
||
}
|
||
return "--spacing(" + v.Value + ")", true
|
||
}
|
||
c.functionalUtility("auto-cols", utilityDescription{
|
||
themeKeys: []string{"--grid-auto-columns"},
|
||
handleBareValue: autoTrackBare,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-auto-columns", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"auto": {d("grid-auto-columns", "auto")}, "min": {d("grid-auto-columns", "min-content")},
|
||
"max": {d("grid-auto-columns", "max-content")}, "fr": {d("grid-auto-columns", "minmax(0, 1fr)")},
|
||
},
|
||
})
|
||
c.functionalUtility("auto-rows", utilityDescription{
|
||
themeKeys: []string{"--grid-auto-rows"},
|
||
handleBareValue: autoTrackBare,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-auto-rows", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"auto": {d("grid-auto-rows", "auto")}, "min": {d("grid-auto-rows", "min-content")},
|
||
"max": {d("grid-auto-rows", "max-content")}, "fr": {d("grid-auto-rows", "minmax(0, 1fr)")},
|
||
},
|
||
})
|
||
|
||
registerUtilities5(c)
|
||
}
|
||
|
||
func registerUtilities5(c *utilCtx) {
|
||
d := decl
|
||
|
||
gridTemplateBare := func(v *UtilityValue) (string, bool) {
|
||
if !isStrictPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return "repeat(" + v.Value + ", minmax(0, 1fr))", true
|
||
}
|
||
c.functionalUtility("grid-cols", utilityDescription{
|
||
themeKeys: []string{"--grid-template-columns"},
|
||
handleBareValue: gridTemplateBare,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-template-columns", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"none": {d("grid-template-columns", "none")}, "subgrid": {d("grid-template-columns", "subgrid")},
|
||
},
|
||
})
|
||
c.functionalUtility("grid-rows", utilityDescription{
|
||
themeKeys: []string{"--grid-template-rows"},
|
||
handleBareValue: gridTemplateBare,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("grid-template-rows", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"none": {d("grid-template-rows", "none")}, "subgrid": {d("grid-template-rows", "subgrid")},
|
||
},
|
||
})
|
||
|
||
c.staticUtility("flex-row", []staticDecl{sd("flex-direction", "row")})
|
||
c.staticUtility("flex-row-reverse", []staticDecl{sd("flex-direction", "row-reverse")})
|
||
c.staticUtility("flex-col", []staticDecl{sd("flex-direction", "column")})
|
||
c.staticUtility("flex-col-reverse", []staticDecl{sd("flex-direction", "column-reverse")})
|
||
c.staticUtility("flex-wrap", []staticDecl{sd("flex-wrap", "wrap")})
|
||
c.staticUtility("flex-nowrap", []staticDecl{sd("flex-wrap", "nowrap")})
|
||
c.staticUtility("flex-wrap-reverse", []staticDecl{sd("flex-wrap", "wrap-reverse")})
|
||
|
||
statics := func(prop string, pairs [][2]string) {
|
||
for _, p := range pairs {
|
||
c.staticUtility(p[0], []staticDecl{sd(prop, p[1])})
|
||
}
|
||
}
|
||
|
||
statics("place-content", [][2]string{
|
||
{"place-content-center", "center"}, {"place-content-start", "start"}, {"place-content-end", "end"},
|
||
{"place-content-center-safe", "safe center"}, {"place-content-end-safe", "safe end"},
|
||
{"place-content-between", "space-between"}, {"place-content-around", "space-around"},
|
||
{"place-content-evenly", "space-evenly"}, {"place-content-baseline", "baseline"}, {"place-content-stretch", "stretch"},
|
||
})
|
||
statics("place-items", [][2]string{
|
||
{"place-items-center", "center"}, {"place-items-start", "start"}, {"place-items-end", "end"},
|
||
{"place-items-center-safe", "safe center"}, {"place-items-end-safe", "safe end"},
|
||
{"place-items-baseline", "baseline"}, {"place-items-stretch", "stretch"},
|
||
})
|
||
statics("align-content", [][2]string{
|
||
{"content-normal", "normal"}, {"content-center", "center"}, {"content-start", "flex-start"},
|
||
{"content-end", "flex-end"}, {"content-center-safe", "safe center"}, {"content-end-safe", "safe flex-end"},
|
||
{"content-between", "space-between"}, {"content-around", "space-around"}, {"content-evenly", "space-evenly"},
|
||
{"content-baseline", "baseline"}, {"content-stretch", "stretch"},
|
||
})
|
||
statics("align-items", [][2]string{
|
||
{"items-center", "center"}, {"items-start", "flex-start"}, {"items-end", "flex-end"},
|
||
{"items-center-safe", "safe center"}, {"items-end-safe", "safe flex-end"},
|
||
{"items-baseline", "baseline"}, {"items-baseline-last", "last baseline"}, {"items-stretch", "stretch"},
|
||
})
|
||
statics("justify-content", [][2]string{
|
||
{"justify-normal", "normal"}, {"justify-center", "center"}, {"justify-start", "flex-start"},
|
||
{"justify-end", "flex-end"}, {"justify-center-safe", "safe center"}, {"justify-end-safe", "safe flex-end"},
|
||
{"justify-between", "space-between"}, {"justify-around", "space-around"}, {"justify-evenly", "space-evenly"},
|
||
{"justify-baseline", "baseline"}, {"justify-stretch", "stretch"},
|
||
})
|
||
statics("justify-items", [][2]string{
|
||
{"justify-items-normal", "normal"}, {"justify-items-center", "center"}, {"justify-items-start", "start"},
|
||
{"justify-items-end", "end"}, {"justify-items-center-safe", "safe center"}, {"justify-items-end-safe", "safe end"},
|
||
{"justify-items-stretch", "stretch"},
|
||
})
|
||
|
||
c.spacingUtility("gap", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("gap", value)) }, spacingOpts{})
|
||
c.spacingUtility("gap-x", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("column-gap", value)) }, spacingOpts{})
|
||
c.spacingUtility("gap-y", []string{"--gap", "--spacing"}, func(value string) *utilResult { return uNodes(d("row-gap", value)) }, spacingOpts{})
|
||
|
||
spaceZero := func(value string) bool {
|
||
if value == "--spacing(0)" || value == "--spacing(-0)" {
|
||
return true
|
||
}
|
||
n, unit, ok := parseDimension(value)
|
||
if ok && n == 0 && (unit == "" || isLength(value)) {
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
c.spacingUtility("space-x", []string{"--space", "--spacing"}, func(value string) *utilResult {
|
||
zero := spaceZero(value)
|
||
ms, me := "calc("+value+" * var(--tw-space-x-reverse))", "calc("+value+" * calc(1 - var(--tw-space-x-reverse)))"
|
||
if zero {
|
||
ms, me = "0", "0"
|
||
}
|
||
return uNodes(
|
||
atRoot([]*AstNode{property("--tw-space-x-reverse", "0", "")}),
|
||
styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "row-gap"), d("--tw-space-x-reverse", "0"),
|
||
d("margin-inline-start", ms), d("margin-inline-end", me)),
|
||
)
|
||
}, spacingOpts{supportsNegative: true})
|
||
c.spacingUtility("space-y", []string{"--space", "--spacing"}, func(value string) *utilResult {
|
||
zero := spaceZero(value)
|
||
ms, me := "calc("+value+" * var(--tw-space-y-reverse))", "calc("+value+" * calc(1 - var(--tw-space-y-reverse)))"
|
||
if zero {
|
||
ms, me = "0", "0"
|
||
}
|
||
return uNodes(
|
||
atRoot([]*AstNode{property("--tw-space-y-reverse", "0", "")}),
|
||
styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "column-gap"), d("--tw-space-y-reverse", "0"),
|
||
d("margin-block-start", ms), d("margin-block-end", me)),
|
||
)
|
||
}, spacingOpts{supportsNegative: true})
|
||
|
||
c.staticUtility("space-x-reverse", []staticDecl{
|
||
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-space-x-reverse", "0", "")}) }),
|
||
sdFn(func() *AstNode {
|
||
return styleRule(":where(& > :not(:last-child))", d("--tw-sort", "row-gap"), d("--tw-space-x-reverse", "1"))
|
||
}),
|
||
})
|
||
c.staticUtility("space-y-reverse", []staticDecl{
|
||
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-space-y-reverse", "0", "")}) }),
|
||
sdFn(func() *AstNode {
|
||
return styleRule(":where(& > :not(:last-child))", d("--tw-sort", "column-gap"), d("--tw-space-y-reverse", "1"))
|
||
}),
|
||
})
|
||
|
||
c.staticUtility("accent-auto", []staticDecl{sd("accent-color", "auto")})
|
||
c.colorUtility("accent", colorUtilityDescription{
|
||
themeKeys: []string{"--accent-color", "--color"},
|
||
handle: func(value string) *utilResult { return uNodes(d("accent-color", value)) },
|
||
})
|
||
c.colorUtility("caret", colorUtilityDescription{
|
||
themeKeys: []string{"--caret-color", "--color"},
|
||
handle: func(value string) *utilResult { return uNodes(d("caret-color", value)) },
|
||
})
|
||
c.colorUtility("divide", colorUtilityDescription{
|
||
themeKeys: []string{"--divide-color", "--border-color", "--color"},
|
||
handle: func(value string) *utilResult {
|
||
return uNodes(styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "divide-color"), d("border-color", value)))
|
||
},
|
||
})
|
||
|
||
statics("place-self", [][2]string{
|
||
{"place-self-auto", "auto"}, {"place-self-start", "start"}, {"place-self-end", "end"},
|
||
{"place-self-center", "center"}, {"place-self-end-safe", "safe end"}, {"place-self-center-safe", "safe center"},
|
||
{"place-self-stretch", "stretch"},
|
||
})
|
||
statics("align-self", [][2]string{
|
||
{"self-auto", "auto"}, {"self-start", "flex-start"}, {"self-end", "flex-end"}, {"self-center", "center"},
|
||
{"self-end-safe", "safe flex-end"}, {"self-center-safe", "safe center"}, {"self-stretch", "stretch"},
|
||
{"self-baseline", "baseline"}, {"self-baseline-last", "last baseline"},
|
||
})
|
||
statics("justify-self", [][2]string{
|
||
{"justify-self-auto", "auto"}, {"justify-self-start", "flex-start"}, {"justify-self-end", "flex-end"},
|
||
{"justify-self-center", "center"}, {"justify-self-end-safe", "safe flex-end"}, {"justify-self-center-safe", "safe center"},
|
||
{"justify-self-stretch", "stretch"},
|
||
})
|
||
|
||
for _, value := range []string{"auto", "hidden", "clip", "visible", "scroll"} {
|
||
c.staticUtility("overflow-"+value, []staticDecl{sd("overflow", value)})
|
||
c.staticUtility("overflow-x-"+value, []staticDecl{sd("overflow-x", value)})
|
||
c.staticUtility("overflow-y-"+value, []staticDecl{sd("overflow-y", value)})
|
||
}
|
||
for _, value := range []string{"auto", "contain", "none"} {
|
||
c.staticUtility("overscroll-"+value, []staticDecl{sd("overscroll-behavior", value)})
|
||
c.staticUtility("overscroll-x-"+value, []staticDecl{sd("overscroll-behavior-x", value)})
|
||
c.staticUtility("overscroll-y-"+value, []staticDecl{sd("overscroll-behavior-y", value)})
|
||
}
|
||
|
||
c.staticUtility("scroll-auto", []staticDecl{sd("scroll-behavior", "auto")})
|
||
c.staticUtility("scroll-smooth", []staticDecl{sd("scroll-behavior", "smooth")})
|
||
|
||
c.staticUtility("scrollbar-auto", []staticDecl{sd("scrollbar-width", "auto")})
|
||
c.staticUtility("scrollbar-thin", []staticDecl{sd("scrollbar-width", "thin")})
|
||
c.staticUtility("scrollbar-none", []staticDecl{sd("scrollbar-width", "none")})
|
||
|
||
scrollbarColorProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-scrollbar-thumb", "#0000", "<color>"),
|
||
property("--tw-scrollbar-track", "#0000", "<color>"),
|
||
})
|
||
}
|
||
c.colorUtility("scrollbar-thumb", colorUtilityDescription{
|
||
themeKeys: []string{"--color"},
|
||
handle: func(value string) *utilResult {
|
||
return uNodes(scrollbarColorProperties(), d("--tw-scrollbar-thumb", value),
|
||
d("scrollbar-color", "var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)"))
|
||
},
|
||
})
|
||
c.colorUtility("scrollbar-track", colorUtilityDescription{
|
||
themeKeys: []string{"--color"},
|
||
handle: func(value string) *utilResult {
|
||
return uNodes(scrollbarColorProperties(), d("--tw-scrollbar-track", value),
|
||
d("scrollbar-color", "var(--tw-scrollbar-thumb) var(--tw-scrollbar-track)"))
|
||
},
|
||
})
|
||
|
||
registerUtilities6(c)
|
||
}
|
||
|
||
func registerUtilities6(c *utilCtx) {
|
||
d := decl
|
||
|
||
c.staticUtility("scrollbar-gutter-auto", []staticDecl{sd("scrollbar-gutter", "auto")})
|
||
c.staticUtility("scrollbar-gutter-stable", []staticDecl{sd("scrollbar-gutter", "stable")})
|
||
c.staticUtility("scrollbar-gutter-both", []staticDecl{sd("scrollbar-gutter", "stable both-edges")})
|
||
|
||
c.staticUtility("truncate", []staticDecl{sd("overflow", "hidden"), sd("text-overflow", "ellipsis"), sd("white-space", "nowrap")})
|
||
c.staticUtility("text-ellipsis", []staticDecl{sd("text-overflow", "ellipsis")})
|
||
c.staticUtility("text-clip", []staticDecl{sd("text-overflow", "clip")})
|
||
|
||
c.staticUtility("hyphens-none", []staticDecl{sd("-webkit-hyphens", "none"), sd("hyphens", "none")})
|
||
c.staticUtility("hyphens-manual", []staticDecl{sd("-webkit-hyphens", "manual"), sd("hyphens", "manual")})
|
||
c.staticUtility("hyphens-auto", []staticDecl{sd("-webkit-hyphens", "auto"), sd("hyphens", "auto")})
|
||
|
||
c.staticUtility("whitespace-normal", []staticDecl{sd("white-space", "normal")})
|
||
c.staticUtility("whitespace-nowrap", []staticDecl{sd("white-space", "nowrap")})
|
||
c.staticUtility("whitespace-pre", []staticDecl{sd("white-space", "pre")})
|
||
c.staticUtility("whitespace-pre-line", []staticDecl{sd("white-space", "pre-line")})
|
||
c.staticUtility("whitespace-pre-wrap", []staticDecl{sd("white-space", "pre-wrap")})
|
||
c.staticUtility("whitespace-break-spaces", []staticDecl{sd("white-space", "break-spaces")})
|
||
|
||
c.functionalUtility("tab", utilityDescription{
|
||
handleBareValue: bareInteger,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("tab-size", value)) },
|
||
})
|
||
|
||
c.staticUtility("text-wrap", []staticDecl{sd("text-wrap", "wrap")})
|
||
c.staticUtility("text-nowrap", []staticDecl{sd("text-wrap", "nowrap")})
|
||
c.staticUtility("text-balance", []staticDecl{sd("text-wrap", "balance")})
|
||
c.staticUtility("text-pretty", []staticDecl{sd("text-wrap", "pretty")})
|
||
c.staticUtility("break-normal", []staticDecl{sd("overflow-wrap", "normal"), sd("word-break", "normal")})
|
||
c.staticUtility("break-all", []staticDecl{sd("word-break", "break-all")})
|
||
c.staticUtility("break-keep", []staticDecl{sd("word-break", "keep-all")})
|
||
c.staticUtility("wrap-anywhere", []staticDecl{sd("overflow-wrap", "anywhere")})
|
||
c.staticUtility("wrap-break-word", []staticDecl{sd("overflow-wrap", "break-word")})
|
||
c.staticUtility("wrap-normal", []staticDecl{sd("overflow-wrap", "normal")})
|
||
|
||
for _, e := range []struct {
|
||
root string
|
||
props []string
|
||
}{
|
||
{"rounded", []string{"border-radius"}},
|
||
{"rounded-s", []string{"border-start-start-radius", "border-end-start-radius"}},
|
||
{"rounded-e", []string{"border-start-end-radius", "border-end-end-radius"}},
|
||
{"rounded-t", []string{"border-top-left-radius", "border-top-right-radius"}},
|
||
{"rounded-r", []string{"border-top-right-radius", "border-bottom-right-radius"}},
|
||
{"rounded-b", []string{"border-bottom-right-radius", "border-bottom-left-radius"}},
|
||
{"rounded-l", []string{"border-top-left-radius", "border-bottom-left-radius"}},
|
||
{"rounded-ss", []string{"border-start-start-radius"}},
|
||
{"rounded-se", []string{"border-start-end-radius"}},
|
||
{"rounded-ee", []string{"border-end-end-radius"}},
|
||
{"rounded-es", []string{"border-end-start-radius"}},
|
||
{"rounded-tl", []string{"border-top-left-radius"}},
|
||
{"rounded-tr", []string{"border-top-right-radius"}},
|
||
{"rounded-br", []string{"border-bottom-right-radius"}},
|
||
{"rounded-bl", []string{"border-bottom-left-radius"}},
|
||
} {
|
||
props := e.props
|
||
mk := func(v string) []*AstNode {
|
||
nodes := make([]*AstNode, len(props))
|
||
for i, p := range props {
|
||
nodes[i] = d(p, v)
|
||
}
|
||
return nodes
|
||
}
|
||
c.functionalUtility(e.root, utilityDescription{
|
||
themeKeys: []string{"--radius"},
|
||
handle: func(value, _ string) *utilResult { return uList(mk(value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"none": mk("0"),
|
||
"full": mk("calc(infinity * 1px)"),
|
||
},
|
||
})
|
||
}
|
||
|
||
c.staticUtility("border-solid", []staticDecl{sd("--tw-border-style", "solid"), sd("border-style", "solid")})
|
||
c.staticUtility("border-dashed", []staticDecl{sd("--tw-border-style", "dashed"), sd("border-style", "dashed")})
|
||
c.staticUtility("border-dotted", []staticDecl{sd("--tw-border-style", "dotted"), sd("border-style", "dotted")})
|
||
c.staticUtility("border-double", []staticDecl{sd("--tw-border-style", "double"), sd("border-style", "double")})
|
||
c.staticUtility("border-hidden", []staticDecl{sd("--tw-border-style", "hidden"), sd("border-style", "hidden")})
|
||
c.staticUtility("border-none", []staticDecl{sd("--tw-border-style", "none"), sd("border-style", "none")})
|
||
|
||
registerUtilities7(c)
|
||
}
|
||
|
||
func registerUtilities7(c *utilCtx) {
|
||
d := decl
|
||
|
||
borderProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-border-style", "solid", "")})
|
||
}
|
||
|
||
borderSideUtility := func(classRoot string, width, color func(string) []*AstNode) {
|
||
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value, ok := c.theme.Get([]string{"--default-border-width"})
|
||
if !ok {
|
||
value = "1px"
|
||
}
|
||
decls := width(value)
|
||
if decls == nil {
|
||
return nil
|
||
}
|
||
return uList(append([]*AstNode{borderProperties()}, decls...))
|
||
}
|
||
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "line-width", "length"})
|
||
}
|
||
switch typ {
|
||
case "line-width", "length":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
decls := width(value)
|
||
if decls == nil {
|
||
return nil
|
||
}
|
||
return uList(append([]*AstNode{borderProperties()}, decls...))
|
||
default:
|
||
cv, ok := asColor(value, candidate.Modifier, c.theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uList(color(cv))
|
||
}
|
||
}
|
||
|
||
if v, ok := resolveThemeColor(candidate, c.theme, []string{"--border-color", "--color"}); ok {
|
||
return uList(color(v))
|
||
}
|
||
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if v, ok := c.theme.resolve(&candidate.Value.Value, []string{"--border-width"}, themeNone); ok {
|
||
decls := width(v)
|
||
if decls == nil {
|
||
return nil
|
||
}
|
||
return uList(append([]*AstNode{borderProperties()}, decls...))
|
||
}
|
||
if isPositiveInteger(candidate.Value.Value) {
|
||
decls := width(candidate.Value.Value + "px")
|
||
if decls == nil {
|
||
return nil
|
||
}
|
||
return uList(append([]*AstNode{borderProperties()}, decls...))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
}
|
||
|
||
type bs struct {
|
||
root string
|
||
styleProp string
|
||
widthProp string
|
||
colorProps []string // properties to set for color
|
||
}
|
||
for _, e := range []bs{
|
||
{"border", "border-style", "border-width", []string{"border-color"}},
|
||
{"border-x", "border-inline-style", "border-inline-width", []string{"border-inline-color"}},
|
||
{"border-y", "border-block-style", "border-block-width", []string{"border-block-color"}},
|
||
{"border-s", "border-inline-start-style", "border-inline-start-width", []string{"border-inline-start-color"}},
|
||
{"border-e", "border-inline-end-style", "border-inline-end-width", []string{"border-inline-end-color"}},
|
||
{"border-bs", "border-block-start-style", "border-block-start-width", []string{"border-block-start-color"}},
|
||
{"border-be", "border-block-end-style", "border-block-end-width", []string{"border-block-end-color"}},
|
||
{"border-t", "border-top-style", "border-top-width", []string{"border-top-color"}},
|
||
{"border-r", "border-right-style", "border-right-width", []string{"border-right-color"}},
|
||
{"border-b", "border-bottom-style", "border-bottom-width", []string{"border-bottom-color"}},
|
||
{"border-l", "border-left-style", "border-left-width", []string{"border-left-color"}},
|
||
} {
|
||
e := e
|
||
width := func(value string) []*AstNode {
|
||
return []*AstNode{d(e.styleProp, "var(--tw-border-style)"), d(e.widthProp, value)}
|
||
}
|
||
color := func(value string) []*AstNode {
|
||
nodes := make([]*AstNode, len(e.colorProps))
|
||
for i, p := range e.colorProps {
|
||
nodes[i] = d(p, value)
|
||
}
|
||
return nodes
|
||
}
|
||
borderSideUtility(e.root, width, color)
|
||
}
|
||
|
||
defaultBorderWidth := func() *string {
|
||
v, ok := c.theme.Get([]string{"--default-border-width"})
|
||
if !ok {
|
||
v = "1px"
|
||
}
|
||
return &v
|
||
}
|
||
|
||
c.functionalUtility("divide-x", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: defaultBorderWidth(),
|
||
themeKeys: []string{"--divide-width", "--border-width"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "px", true
|
||
},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(
|
||
atRoot([]*AstNode{property("--tw-divide-x-reverse", "0", "")}),
|
||
styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "divide-x-width"), borderProperties(), d("--tw-divide-x-reverse", "0"),
|
||
d("border-inline-style", "var(--tw-border-style)"),
|
||
d("border-inline-start-width", "calc("+value+" * var(--tw-divide-x-reverse))"),
|
||
d("border-inline-end-width", "calc("+value+" * calc(1 - var(--tw-divide-x-reverse)))")),
|
||
)
|
||
},
|
||
})
|
||
c.functionalUtility("divide-y", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: defaultBorderWidth(),
|
||
themeKeys: []string{"--divide-width", "--border-width"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "px", true
|
||
},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(
|
||
atRoot([]*AstNode{property("--tw-divide-y-reverse", "0", "")}),
|
||
styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "divide-y-width"), borderProperties(), d("--tw-divide-y-reverse", "0"),
|
||
d("border-bottom-style", "var(--tw-border-style)"), d("border-top-style", "var(--tw-border-style)"),
|
||
d("border-top-width", "calc("+value+" * var(--tw-divide-y-reverse))"),
|
||
d("border-bottom-width", "calc("+value+" * calc(1 - var(--tw-divide-y-reverse)))")),
|
||
)
|
||
},
|
||
})
|
||
|
||
c.staticUtility("divide-x-reverse", []staticDecl{
|
||
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-divide-x-reverse", "0", "")}) }),
|
||
sdFn(func() *AstNode {
|
||
return styleRule(":where(& > :not(:last-child))", d("--tw-divide-x-reverse", "1"))
|
||
}),
|
||
})
|
||
c.staticUtility("divide-y-reverse", []staticDecl{
|
||
sdFn(func() *AstNode { return atRoot([]*AstNode{property("--tw-divide-y-reverse", "0", "")}) }),
|
||
sdFn(func() *AstNode {
|
||
return styleRule(":where(& > :not(:last-child))", d("--tw-divide-y-reverse", "1"))
|
||
}),
|
||
})
|
||
for _, value := range []string{"solid", "dashed", "dotted", "double", "none"} {
|
||
v := value
|
||
c.staticUtility("divide-"+v, []staticDecl{
|
||
sdFn(func() *AstNode {
|
||
return styleRule(":where(& > :not(:last-child))",
|
||
d("--tw-sort", "divide-style"), d("--tw-border-style", v), d("border-style", v))
|
||
}),
|
||
})
|
||
}
|
||
|
||
c.staticUtility("bg-auto", []staticDecl{sd("background-size", "auto")})
|
||
c.staticUtility("bg-cover", []staticDecl{sd("background-size", "cover")})
|
||
c.staticUtility("bg-contain", []staticDecl{sd("background-size", "contain")})
|
||
c.functionalUtility("bg-size", utilityDescription{
|
||
handle: func(value, _ string) *utilResult {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-size", value))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("bg-fixed", []staticDecl{sd("background-attachment", "fixed")})
|
||
c.staticUtility("bg-local", []staticDecl{sd("background-attachment", "local")})
|
||
c.staticUtility("bg-scroll", []staticDecl{sd("background-attachment", "scroll")})
|
||
|
||
c.staticUtility("bg-top", []staticDecl{sd("background-position", "top")})
|
||
c.staticUtility("bg-top-left", []staticDecl{sd("background-position", "left top")})
|
||
c.staticUtility("bg-top-right", []staticDecl{sd("background-position", "right top")})
|
||
c.staticUtility("bg-bottom", []staticDecl{sd("background-position", "bottom")})
|
||
c.staticUtility("bg-bottom-left", []staticDecl{sd("background-position", "left bottom")})
|
||
c.staticUtility("bg-bottom-right", []staticDecl{sd("background-position", "right bottom")})
|
||
c.staticUtility("bg-left", []staticDecl{sd("background-position", "left")})
|
||
c.staticUtility("bg-right", []staticDecl{sd("background-position", "right")})
|
||
c.staticUtility("bg-center", []staticDecl{sd("background-position", "center")})
|
||
c.functionalUtility("bg-position", utilityDescription{
|
||
handle: func(value, _ string) *utilResult {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-position", value))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("bg-repeat", []staticDecl{sd("background-repeat", "repeat")})
|
||
c.staticUtility("bg-no-repeat", []staticDecl{sd("background-repeat", "no-repeat")})
|
||
c.staticUtility("bg-repeat-x", []staticDecl{sd("background-repeat", "repeat-x")})
|
||
c.staticUtility("bg-repeat-y", []staticDecl{sd("background-repeat", "repeat-y")})
|
||
c.staticUtility("bg-repeat-round", []staticDecl{sd("background-repeat", "round")})
|
||
c.staticUtility("bg-repeat-space", []staticDecl{sd("background-repeat", "space")})
|
||
|
||
c.staticUtility("bg-none", []staticDecl{sd("background-image", "none")})
|
||
|
||
registerUtilities8(c)
|
||
}
|
||
|
||
func registerUtilities8(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
linearGradientDirections := map[string]string{
|
||
"to-t": "to top", "to-tr": "to top right", "to-r": "to right", "to-br": "to bottom right",
|
||
"to-b": "to bottom", "to-bl": "to bottom left", "to-l": "to left", "to-tl": "to top left",
|
||
}
|
||
|
||
resolveInterpolationModifier := func(modifier *CandidateModifier) string {
|
||
method := "in oklab"
|
||
if modifier != nil {
|
||
if modifier.Kind == modNamed {
|
||
switch modifier.Value {
|
||
case "longer", "shorter", "increasing", "decreasing":
|
||
method = "in oklch " + modifier.Value + " hue"
|
||
default:
|
||
method = "in " + modifier.Value
|
||
}
|
||
} else {
|
||
method = modifier.Value
|
||
}
|
||
}
|
||
return method
|
||
}
|
||
|
||
handleBgLinear := func(negative bool) func(*Candidate) *utilResult {
|
||
return func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"angle"})
|
||
}
|
||
if typ == "angle" {
|
||
if negative {
|
||
value = "calc(" + value + " * -1)"
|
||
}
|
||
return uNodes(d("--tw-gradient-position", value),
|
||
d("background-image", "linear-gradient(var(--tw-gradient-stops,"+value+"))"))
|
||
}
|
||
if negative {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-gradient-position", value),
|
||
d("background-image", "linear-gradient(var(--tw-gradient-stops,"+value+"))"))
|
||
}
|
||
|
||
value := candidate.Value.Value
|
||
if !negative {
|
||
if dir, ok := linearGradientDirections[value]; ok {
|
||
value = dir
|
||
} else if isPositiveInteger(value) {
|
||
value = value + "deg"
|
||
} else {
|
||
return nil
|
||
}
|
||
} else if isPositiveInteger(value) {
|
||
value = "calc(" + value + "deg * -1)"
|
||
} else {
|
||
return nil
|
||
}
|
||
|
||
interp := resolveInterpolationModifier(candidate.Modifier)
|
||
return uNodes(
|
||
d("--tw-gradient-position", value),
|
||
rule("@supports (background-image: linear-gradient(in lab, red, red))",
|
||
d("--tw-gradient-position", value+" "+interp)),
|
||
d("background-image", "linear-gradient(var(--tw-gradient-stops))"),
|
||
)
|
||
}
|
||
}
|
||
c.utilities.functional("-bg-linear", handleBgLinear(true), nil)
|
||
c.utilities.functional("bg-linear", handleBgLinear(false), nil)
|
||
|
||
handleBgConic := func(negative bool) func(*Candidate) *utilResult {
|
||
return func(candidate *Candidate) *utilResult {
|
||
if candidate.Value != nil && candidate.Value.Kind == uvArbitrary {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value := candidate.Value.Value
|
||
return uNodes(d("--tw-gradient-position", value),
|
||
d("background-image", "conic-gradient(var(--tw-gradient-stops,"+value+"))"))
|
||
}
|
||
interp := resolveInterpolationModifier(candidate.Modifier)
|
||
if candidate.Value == nil {
|
||
return uNodes(d("--tw-gradient-position", interp),
|
||
d("background-image", "conic-gradient(var(--tw-gradient-stops))"))
|
||
}
|
||
value := candidate.Value.Value
|
||
if !isPositiveInteger(value) {
|
||
return nil
|
||
}
|
||
if negative {
|
||
value = "calc(" + value + "deg * -1)"
|
||
} else {
|
||
value = value + "deg"
|
||
}
|
||
return uNodes(d("--tw-gradient-position", "from "+value+" "+interp),
|
||
d("background-image", "conic-gradient(var(--tw-gradient-stops))"))
|
||
}
|
||
}
|
||
c.utilities.functional("-bg-conic", handleBgConic(true), nil)
|
||
c.utilities.functional("bg-conic", handleBgConic(false), nil)
|
||
|
||
c.utilities.functional("bg-radial", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
interp := resolveInterpolationModifier(candidate.Modifier)
|
||
return uNodes(d("--tw-gradient-position", interp),
|
||
d("background-image", "radial-gradient(var(--tw-gradient-stops))"))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value := candidate.Value.Value
|
||
return uNodes(d("--tw-gradient-position", value),
|
||
d("background-image", "radial-gradient(var(--tw-gradient-stops,"+value+"))"))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.utilities.functional("bg", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"image", "color", "percentage", "position", "bg-size", "length", "url"})
|
||
}
|
||
switch typ {
|
||
case "percentage", "position":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-position", value))
|
||
case "bg-size", "length", "size":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-size", value))
|
||
case "image", "url":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-image", value))
|
||
default:
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("background-color", cv))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
|
||
return uNodes(d("background-color", v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if v, ok := theme.resolve(&candidate.Value.Value, []string{"--background-image"}, themeNone); ok {
|
||
return uNodes(d("background-image", v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
gradientStopProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-gradient-position", "", ""),
|
||
property("--tw-gradient-from", "#0000", "<color>"),
|
||
property("--tw-gradient-via", "#0000", "<color>"),
|
||
property("--tw-gradient-to", "#0000", "<color>"),
|
||
property("--tw-gradient-stops", "", ""),
|
||
property("--tw-gradient-via-stops", "", ""),
|
||
property("--tw-gradient-from-position", "0%", "<length-percentage>"),
|
||
property("--tw-gradient-via-position", "50%", "<length-percentage>"),
|
||
property("--tw-gradient-to-position", "100%", "<length-percentage>"),
|
||
})
|
||
}
|
||
|
||
gradientStopUtility := func(classRoot string, color, position func(string) []*AstNode) {
|
||
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length", "percentage"})
|
||
}
|
||
switch typ {
|
||
case "length", "percentage":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uList(position(value))
|
||
default:
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uList(color(cv))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
|
||
return uList(color(v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if v, ok := theme.resolve(&candidate.Value.Value, []string{"--gradient-color-stop-positions"}, themeNone); ok {
|
||
return uList(position(v))
|
||
} else if strings.HasSuffix(candidate.Value.Value, "%") && isPositiveInteger(candidate.Value.Value[:len(candidate.Value.Value)-1]) {
|
||
return uList(position(candidate.Value.Value))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
}
|
||
|
||
gradientStopUtility("from",
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-from"), d("--tw-gradient-from", value),
|
||
d("--tw-gradient-stops", "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))")}
|
||
},
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-gradient-from-position", value)}
|
||
})
|
||
c.staticUtility("via-none", []staticDecl{sd("--tw-gradient-via-stops", "initial")})
|
||
gradientStopUtility("via",
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-via"), d("--tw-gradient-via", value),
|
||
d("--tw-gradient-via-stops", "var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-via) var(--tw-gradient-via-position), var(--tw-gradient-to) var(--tw-gradient-to-position)"),
|
||
d("--tw-gradient-stops", "var(--tw-gradient-via-stops)")}
|
||
},
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-gradient-via-position", value)}
|
||
})
|
||
gradientStopUtility("to",
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-sort", "--tw-gradient-to"), d("--tw-gradient-to", value),
|
||
d("--tw-gradient-stops", "var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position))")}
|
||
},
|
||
func(value string) []*AstNode {
|
||
return []*AstNode{gradientStopProperties(), d("--tw-gradient-to-position", value)}
|
||
})
|
||
|
||
registerUtilities9(c)
|
||
}
|
||
|
||
func registerUtilities9(c *utilCtx) {
|
||
d := decl
|
||
|
||
c.staticUtility("mask-none", []staticDecl{sd("mask-image", "none")})
|
||
|
||
c.utilities.functional("mask", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil || candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind != uvArbitrary {
|
||
return nil
|
||
}
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"image", "percentage", "position", "bg-size", "length", "url"})
|
||
}
|
||
switch typ {
|
||
case "percentage", "position":
|
||
return uNodes(d("mask-position", value))
|
||
case "bg-size", "length", "size":
|
||
return uNodes(d("mask-size", value))
|
||
default:
|
||
return uNodes(d("mask-image", value))
|
||
}
|
||
}, nil)
|
||
|
||
c.staticUtility("mask-add", []staticDecl{sd("mask-composite", "add")})
|
||
c.staticUtility("mask-subtract", []staticDecl{sd("mask-composite", "subtract")})
|
||
c.staticUtility("mask-intersect", []staticDecl{sd("mask-composite", "intersect")})
|
||
c.staticUtility("mask-exclude", []staticDecl{sd("mask-composite", "exclude")})
|
||
|
||
c.staticUtility("mask-alpha", []staticDecl{sd("mask-mode", "alpha")})
|
||
c.staticUtility("mask-luminance", []staticDecl{sd("mask-mode", "luminance")})
|
||
c.staticUtility("mask-match", []staticDecl{sd("mask-mode", "match-source")})
|
||
|
||
c.staticUtility("mask-type-alpha", []staticDecl{sd("mask-type", "alpha")})
|
||
c.staticUtility("mask-type-luminance", []staticDecl{sd("mask-type", "luminance")})
|
||
|
||
c.staticUtility("mask-auto", []staticDecl{sd("mask-size", "auto")})
|
||
c.staticUtility("mask-cover", []staticDecl{sd("mask-size", "cover")})
|
||
c.staticUtility("mask-contain", []staticDecl{sd("mask-size", "contain")})
|
||
c.functionalUtility("mask-size", utilityDescription{
|
||
handle: func(value, _ string) *utilResult {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return uNodes(d("mask-size", value))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("mask-top", []staticDecl{sd("mask-position", "top")})
|
||
c.staticUtility("mask-top-left", []staticDecl{sd("mask-position", "left top")})
|
||
c.staticUtility("mask-top-right", []staticDecl{sd("mask-position", "right top")})
|
||
c.staticUtility("mask-bottom", []staticDecl{sd("mask-position", "bottom")})
|
||
c.staticUtility("mask-bottom-left", []staticDecl{sd("mask-position", "left bottom")})
|
||
c.staticUtility("mask-bottom-right", []staticDecl{sd("mask-position", "right bottom")})
|
||
c.staticUtility("mask-left", []staticDecl{sd("mask-position", "left")})
|
||
c.staticUtility("mask-right", []staticDecl{sd("mask-position", "right")})
|
||
c.staticUtility("mask-center", []staticDecl{sd("mask-position", "center")})
|
||
c.functionalUtility("mask-position", utilityDescription{
|
||
handle: func(value, _ string) *utilResult {
|
||
if value == "" {
|
||
return nil
|
||
}
|
||
return uNodes(d("mask-position", value))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("mask-repeat", []staticDecl{sd("mask-repeat", "repeat")})
|
||
c.staticUtility("mask-no-repeat", []staticDecl{sd("mask-repeat", "no-repeat")})
|
||
c.staticUtility("mask-repeat-x", []staticDecl{sd("mask-repeat", "repeat-x")})
|
||
c.staticUtility("mask-repeat-y", []staticDecl{sd("mask-repeat", "repeat-y")})
|
||
c.staticUtility("mask-repeat-round", []staticDecl{sd("mask-repeat", "round")})
|
||
c.staticUtility("mask-repeat-space", []staticDecl{sd("mask-repeat", "space")})
|
||
|
||
c.staticUtility("mask-clip-border", []staticDecl{sd("mask-clip", "border-box")})
|
||
c.staticUtility("mask-clip-padding", []staticDecl{sd("mask-clip", "padding-box")})
|
||
c.staticUtility("mask-clip-content", []staticDecl{sd("mask-clip", "content-box")})
|
||
c.staticUtility("mask-clip-fill", []staticDecl{sd("mask-clip", "fill-box")})
|
||
c.staticUtility("mask-clip-stroke", []staticDecl{sd("mask-clip", "stroke-box")})
|
||
c.staticUtility("mask-clip-view", []staticDecl{sd("mask-clip", "view-box")})
|
||
c.staticUtility("mask-no-clip", []staticDecl{sd("mask-clip", "no-clip")})
|
||
|
||
c.staticUtility("mask-origin-border", []staticDecl{sd("mask-origin", "border-box")})
|
||
c.staticUtility("mask-origin-padding", []staticDecl{sd("mask-origin", "padding-box")})
|
||
c.staticUtility("mask-origin-content", []staticDecl{sd("mask-origin", "content-box")})
|
||
c.staticUtility("mask-origin-fill", []staticDecl{sd("mask-origin", "fill-box")})
|
||
c.staticUtility("mask-origin-stroke", []staticDecl{sd("mask-origin", "stroke-box")})
|
||
c.staticUtility("mask-origin-view", []staticDecl{sd("mask-origin", "view-box")})
|
||
|
||
registerUtilities10(c)
|
||
}
|
||
|
||
// registerUtilities10: mask-image gradients (edge / linear / radial / conic).
|
||
|
||
func registerUtilities10(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
maskImage := func() *AstNode {
|
||
return d("mask-image", "var(--tw-mask-linear), var(--tw-mask-radial), var(--tw-mask-conic)")
|
||
}
|
||
maskComposite := func() *AstNode { return d("mask-composite", "intersect") }
|
||
maskPropertiesGradient := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-mask-linear", "linear-gradient(#fff, #fff)", ""),
|
||
property("--tw-mask-radial", "linear-gradient(#fff, #fff)", ""),
|
||
property("--tw-mask-conic", "linear-gradient(#fff, #fff)", ""),
|
||
})
|
||
}
|
||
|
||
maskStopUtility := func(classRoot string, colorFn, positionFn func(string) []*AstNode) {
|
||
c.utilities.functional(classRoot, func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"length", "percentage", "color"})
|
||
}
|
||
switch typ {
|
||
case "color":
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uList(colorFn(cv))
|
||
case "percentage":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if len(value) == 0 || !isPositiveInteger(value[:len(value)-1]) {
|
||
return nil
|
||
}
|
||
return uList(positionFn(value))
|
||
default:
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uList(positionFn(value))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--background-color", "--color"}); ok {
|
||
return uList(colorFn(v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
typ := inferDataType(candidate.Value.Value, []string{"number", "percentage"})
|
||
switch typ {
|
||
case "number":
|
||
if _, ok := theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
|
||
return nil
|
||
}
|
||
if !isValidSpacingMultiplier(candidate.Value.Value) {
|
||
return nil
|
||
}
|
||
return uList(positionFn("--spacing(" + candidate.Value.Value + ")"))
|
||
case "percentage":
|
||
v := candidate.Value.Value
|
||
if len(v) == 0 || !isPositiveInteger(v[:len(v)-1]) {
|
||
return nil
|
||
}
|
||
return uList(positionFn(v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
}
|
||
|
||
// --- Edge masks ---
|
||
maskPropertiesEdge := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-mask-left", "linear-gradient(#fff, #fff)", ""),
|
||
property("--tw-mask-right", "linear-gradient(#fff, #fff)", ""),
|
||
property("--tw-mask-bottom", "linear-gradient(#fff, #fff)", ""),
|
||
property("--tw-mask-top", "linear-gradient(#fff, #fff)", ""),
|
||
})
|
||
}
|
||
maskEdgeUtility := func(name, stop string, top, right, bottom, left bool) {
|
||
edges := []struct {
|
||
name string
|
||
on bool
|
||
}{{"top", top}, {"right", right}, {"bottom", bottom}, {"left", left}}
|
||
build := func(value, kind string) []*AstNode {
|
||
nodes := []*AstNode{
|
||
maskPropertiesGradient(), maskPropertiesEdge(), maskImage(), maskComposite(),
|
||
d("--tw-mask-linear", "var(--tw-mask-left), var(--tw-mask-right), var(--tw-mask-bottom), var(--tw-mask-top)"),
|
||
}
|
||
for _, e := range edges {
|
||
if !e.on {
|
||
continue
|
||
}
|
||
nodes = append(nodes,
|
||
d("--tw-mask-"+e.name, "linear-gradient(to "+e.name+", var(--tw-mask-"+e.name+"-from-color) var(--tw-mask-"+e.name+"-from-position), var(--tw-mask-"+e.name+"-to-color) var(--tw-mask-"+e.name+"-to-position))"),
|
||
atRoot([]*AstNode{
|
||
property("--tw-mask-"+e.name+"-from-position", "0%", ""),
|
||
property("--tw-mask-"+e.name+"-to-position", "100%", ""),
|
||
property("--tw-mask-"+e.name+"-from-color", "black", ""),
|
||
property("--tw-mask-"+e.name+"-to-color", "transparent", ""),
|
||
}),
|
||
d("--tw-mask-"+e.name+"-"+stop+"-"+kind, value),
|
||
)
|
||
}
|
||
return nodes
|
||
}
|
||
maskStopUtility(name,
|
||
func(value string) []*AstNode { return build(value, "color") },
|
||
func(value string) []*AstNode { return build(value, "position") })
|
||
}
|
||
|
||
maskEdgeUtility("mask-x-from", "from", false, true, false, true)
|
||
maskEdgeUtility("mask-x-to", "to", false, true, false, true)
|
||
maskEdgeUtility("mask-y-from", "from", true, false, true, false)
|
||
maskEdgeUtility("mask-y-to", "to", true, false, true, false)
|
||
maskEdgeUtility("mask-t-from", "from", true, false, false, false)
|
||
maskEdgeUtility("mask-t-to", "to", true, false, false, false)
|
||
maskEdgeUtility("mask-r-from", "from", false, true, false, false)
|
||
maskEdgeUtility("mask-r-to", "to", false, true, false, false)
|
||
maskEdgeUtility("mask-b-from", "from", false, false, true, false)
|
||
maskEdgeUtility("mask-b-to", "to", false, false, true, false)
|
||
maskEdgeUtility("mask-l-from", "from", false, false, false, true)
|
||
maskEdgeUtility("mask-l-to", "to", false, false, false, true)
|
||
|
||
// --- Linear masks ---
|
||
maskPropertiesLinear := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-mask-linear-position", "0deg", ""),
|
||
property("--tw-mask-linear-from-position", "0%", ""),
|
||
property("--tw-mask-linear-to-position", "100%", ""),
|
||
property("--tw-mask-linear-from-color", "black", ""),
|
||
property("--tw-mask-linear-to-color", "transparent", ""),
|
||
})
|
||
}
|
||
degBare := func(neg bool) func(*UtilityValue) (string, bool) {
|
||
return func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
if v.Value == "0" {
|
||
return "0deg", true
|
||
}
|
||
if v.Value == "1" {
|
||
if neg {
|
||
return "-1deg", true
|
||
}
|
||
return "1deg", true
|
||
}
|
||
if neg {
|
||
return "calc(1deg * -" + v.Value + ")", true
|
||
}
|
||
return "calc(1deg * " + v.Value + ")", true
|
||
}
|
||
}
|
||
c.functionalUtility("mask-linear", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: nil,
|
||
supportsNegative: true,
|
||
handleBareValue: degBare(false),
|
||
handleNegativeBareValue: degBare(true),
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(),
|
||
d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops, var(--tw-mask-linear-position)))"),
|
||
d("--tw-mask-linear-position", value))
|
||
},
|
||
})
|
||
linearStops := "var(--tw-mask-linear-position), var(--tw-mask-linear-from-color) var(--tw-mask-linear-from-position), var(--tw-mask-linear-to-color) var(--tw-mask-linear-to-position)"
|
||
maskStopUtility("mask-linear-from",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-from-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-from-position", v)}
|
||
})
|
||
maskStopUtility("mask-linear-to",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-to-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesLinear(), maskImage(), maskComposite(), d("--tw-mask-linear-stops", linearStops), d("--tw-mask-linear", "linear-gradient(var(--tw-mask-linear-stops))"), d("--tw-mask-linear-to-position", v)}
|
||
})
|
||
|
||
// --- Radial masks ---
|
||
maskPropertiesRadial := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-mask-radial-from-position", "0%", ""),
|
||
property("--tw-mask-radial-to-position", "100%", ""),
|
||
property("--tw-mask-radial-from-color", "black", ""),
|
||
property("--tw-mask-radial-to-color", "transparent", ""),
|
||
property("--tw-mask-radial-shape", "ellipse", ""),
|
||
property("--tw-mask-radial-size", "farthest-corner", ""),
|
||
property("--tw-mask-radial-position", "center", ""),
|
||
})
|
||
}
|
||
c.staticUtility("mask-circle", []staticDecl{sd("--tw-mask-radial-shape", "circle")})
|
||
c.staticUtility("mask-ellipse", []staticDecl{sd("--tw-mask-radial-shape", "ellipse")})
|
||
c.staticUtility("mask-radial-closest-side", []staticDecl{sd("--tw-mask-radial-size", "closest-side")})
|
||
c.staticUtility("mask-radial-farthest-side", []staticDecl{sd("--tw-mask-radial-size", "farthest-side")})
|
||
c.staticUtility("mask-radial-closest-corner", []staticDecl{sd("--tw-mask-radial-size", "closest-corner")})
|
||
c.staticUtility("mask-radial-farthest-corner", []staticDecl{sd("--tw-mask-radial-size", "farthest-corner")})
|
||
for _, p := range [][2]string{
|
||
{"mask-radial-at-top", "top"}, {"mask-radial-at-top-left", "top left"}, {"mask-radial-at-top-right", "top right"},
|
||
{"mask-radial-at-bottom", "bottom"}, {"mask-radial-at-bottom-left", "bottom left"}, {"mask-radial-at-bottom-right", "bottom right"},
|
||
{"mask-radial-at-left", "left"}, {"mask-radial-at-right", "right"}, {"mask-radial-at-center", "center"},
|
||
} {
|
||
c.staticUtility(p[0], []staticDecl{sd("--tw-mask-radial-position", p[1])})
|
||
}
|
||
c.functionalUtility("mask-radial-at", utilityDescription{
|
||
defaultValueSet: true, defaultValue: nil,
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("--tw-mask-radial-position", value)) },
|
||
})
|
||
c.functionalUtility("mask-radial", utilityDescription{
|
||
defaultValueSet: true, defaultValue: nil,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(),
|
||
d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops, var(--tw-mask-radial-size)))"),
|
||
d("--tw-mask-radial-size", value))
|
||
},
|
||
})
|
||
radialStops := "var(--tw-mask-radial-shape) var(--tw-mask-radial-size) at var(--tw-mask-radial-position), var(--tw-mask-radial-from-color) var(--tw-mask-radial-from-position), var(--tw-mask-radial-to-color) var(--tw-mask-radial-to-position)"
|
||
maskStopUtility("mask-radial-from",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-from-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-from-position", v)}
|
||
})
|
||
maskStopUtility("mask-radial-to",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-to-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesRadial(), maskImage(), maskComposite(), d("--tw-mask-radial-stops", radialStops), d("--tw-mask-radial", "radial-gradient(var(--tw-mask-radial-stops))"), d("--tw-mask-radial-to-position", v)}
|
||
})
|
||
|
||
// --- Conic masks ---
|
||
maskPropertiesConic := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-mask-conic-position", "0deg", ""),
|
||
property("--tw-mask-conic-from-position", "0%", ""),
|
||
property("--tw-mask-conic-to-position", "100%", ""),
|
||
property("--tw-mask-conic-from-color", "black", ""),
|
||
property("--tw-mask-conic-to-color", "transparent", ""),
|
||
})
|
||
}
|
||
c.functionalUtility("mask-conic", utilityDescription{
|
||
defaultValueSet: true,
|
||
defaultValue: nil,
|
||
supportsNegative: true,
|
||
handleBareValue: degBare(false),
|
||
handleNegativeBareValue: degBare(true),
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(),
|
||
d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops, var(--tw-mask-conic-position)))"),
|
||
d("--tw-mask-conic-position", value))
|
||
},
|
||
})
|
||
conicStops := "from var(--tw-mask-conic-position), var(--tw-mask-conic-from-color) var(--tw-mask-conic-from-position), var(--tw-mask-conic-to-color) var(--tw-mask-conic-to-position)"
|
||
maskStopUtility("mask-conic-from",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-from-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-from-position", v)}
|
||
})
|
||
maskStopUtility("mask-conic-to",
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-to-color", v)}
|
||
},
|
||
func(v string) []*AstNode {
|
||
return []*AstNode{maskPropertiesGradient(), maskPropertiesConic(), maskImage(), maskComposite(), d("--tw-mask-conic-stops", conicStops), d("--tw-mask-conic", "conic-gradient(var(--tw-mask-conic-stops))"), d("--tw-mask-conic-to-position", v)}
|
||
})
|
||
|
||
registerUtilities11(c)
|
||
}
|
||
|
||
// registerUtilities11: box-decoration, bg-clip/origin, blend modes, fill,
|
||
// stroke, object, padding, text-align, indent, vertical-align, font,
|
||
// text-transform/style/decoration-line, font-stretch, placeholder, decoration.
|
||
|
||
func registerUtilities11(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
c.staticUtility("box-decoration-slice", []staticDecl{sd("-webkit-box-decoration-break", "slice"), sd("box-decoration-break", "slice")})
|
||
c.staticUtility("box-decoration-clone", []staticDecl{sd("-webkit-box-decoration-break", "clone"), sd("box-decoration-break", "clone")})
|
||
|
||
c.staticUtility("bg-clip-text", []staticDecl{sd("background-clip", "text")})
|
||
c.staticUtility("bg-clip-border", []staticDecl{sd("background-clip", "border-box")})
|
||
c.staticUtility("bg-clip-padding", []staticDecl{sd("background-clip", "padding-box")})
|
||
c.staticUtility("bg-clip-content", []staticDecl{sd("background-clip", "content-box")})
|
||
c.staticUtility("bg-origin-border", []staticDecl{sd("background-origin", "border-box")})
|
||
c.staticUtility("bg-origin-padding", []staticDecl{sd("background-origin", "padding-box")})
|
||
c.staticUtility("bg-origin-content", []staticDecl{sd("background-origin", "content-box")})
|
||
|
||
for _, value := range []string{
|
||
"normal", "multiply", "screen", "overlay", "darken", "lighten", "color-dodge",
|
||
"color-burn", "hard-light", "soft-light", "difference", "exclusion", "hue",
|
||
"saturation", "color", "luminosity",
|
||
} {
|
||
c.staticUtility("bg-blend-"+value, []staticDecl{sd("background-blend-mode", value)})
|
||
c.staticUtility("mix-blend-"+value, []staticDecl{sd("mix-blend-mode", value)})
|
||
}
|
||
c.staticUtility("mix-blend-plus-darker", []staticDecl{sd("mix-blend-mode", "plus-darker")})
|
||
c.staticUtility("mix-blend-plus-lighter", []staticDecl{sd("mix-blend-mode", "plus-lighter")})
|
||
|
||
c.staticUtility("fill-none", []staticDecl{sd("fill", "none")})
|
||
c.utilities.functional("fill", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
v, ok := asColor(candidate.Value.Value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("fill", v))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--fill", "--color"}); ok {
|
||
return uNodes(d("fill", v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.staticUtility("stroke-none", []staticDecl{sd("stroke", "none")})
|
||
c.utilities.functional("stroke", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "number", "length", "percentage"})
|
||
}
|
||
switch typ {
|
||
case "number", "length", "percentage":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("stroke-width", value))
|
||
default:
|
||
v, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("stroke", v))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--stroke", "--color"}); ok {
|
||
return uNodes(d("stroke", v))
|
||
}
|
||
vv := candidate.Value.Value
|
||
if v, ok := theme.resolve(&vv, []string{"--stroke-width"}, themeNone); ok {
|
||
return uNodes(d("stroke-width", v))
|
||
} else if isPositiveInteger(vv) {
|
||
return uNodes(d("stroke-width", vv))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.staticUtility("object-contain", []staticDecl{sd("object-fit", "contain")})
|
||
c.staticUtility("object-cover", []staticDecl{sd("object-fit", "cover")})
|
||
c.staticUtility("object-fill", []staticDecl{sd("object-fit", "fill")})
|
||
c.staticUtility("object-none", []staticDecl{sd("object-fit", "none")})
|
||
c.staticUtility("object-scale-down", []staticDecl{sd("object-fit", "scale-down")})
|
||
c.functionalUtility("object", utilityDescription{
|
||
themeKeys: []string{"--object-position"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("object-position", value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"top": {d("object-position", "top")}, "top-left": {d("object-position", "left top")},
|
||
"top-right": {d("object-position", "right top")}, "bottom": {d("object-position", "bottom")},
|
||
"bottom-left": {d("object-position", "left bottom")}, "bottom-right": {d("object-position", "right bottom")},
|
||
"left": {d("object-position", "left")}, "right": {d("object-position", "right")}, "center": {d("object-position", "center")},
|
||
},
|
||
})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"p", "padding"}, {"px", "padding-inline"}, {"py", "padding-block"},
|
||
{"ps", "padding-inline-start"}, {"pe", "padding-inline-end"},
|
||
{"pbs", "padding-block-start"}, {"pbe", "padding-block-end"},
|
||
{"pt", "padding-top"}, {"pr", "padding-right"}, {"pb", "padding-bottom"}, {"pl", "padding-left"},
|
||
} {
|
||
prop := pair[1]
|
||
c.spacingUtility(pair[0], []string{"--padding", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d(prop, value)) }, spacingOpts{})
|
||
}
|
||
|
||
c.staticUtility("text-left", []staticDecl{sd("text-align", "left")})
|
||
c.staticUtility("text-center", []staticDecl{sd("text-align", "center")})
|
||
c.staticUtility("text-right", []staticDecl{sd("text-align", "right")})
|
||
c.staticUtility("text-justify", []staticDecl{sd("text-align", "justify")})
|
||
c.staticUtility("text-start", []staticDecl{sd("text-align", "start")})
|
||
c.staticUtility("text-end", []staticDecl{sd("text-align", "end")})
|
||
|
||
c.spacingUtility("indent", []string{"--text-indent", "--spacing"},
|
||
func(value string) *utilResult { return uNodes(d("text-indent", value)) }, spacingOpts{supportsNegative: true})
|
||
|
||
c.staticUtility("align-baseline", []staticDecl{sd("vertical-align", "baseline")})
|
||
c.staticUtility("align-top", []staticDecl{sd("vertical-align", "top")})
|
||
c.staticUtility("align-middle", []staticDecl{sd("vertical-align", "middle")})
|
||
c.staticUtility("align-bottom", []staticDecl{sd("vertical-align", "bottom")})
|
||
c.staticUtility("align-text-top", []staticDecl{sd("vertical-align", "text-top")})
|
||
c.staticUtility("align-text-bottom", []staticDecl{sd("vertical-align", "text-bottom")})
|
||
c.staticUtility("align-sub", []staticDecl{sd("vertical-align", "sub")})
|
||
c.staticUtility("align-super", []staticDecl{sd("vertical-align", "super")})
|
||
c.functionalUtility("align", utilityDescription{
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("vertical-align", value)) },
|
||
})
|
||
|
||
c.utilities.functional("font", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil || candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"number", "generic-name", "family-name"})
|
||
}
|
||
switch typ {
|
||
case "generic-name", "family-name":
|
||
return uNodes(d("font-family", value))
|
||
default:
|
||
return uNodes(atRoot([]*AstNode{property("--tw-font-weight", "", "")}),
|
||
d("--tw-font-weight", value), d("font-weight", value))
|
||
}
|
||
}
|
||
if families, extra, ok := theme.resolveWith(candidate.Value.Value, []string{"--font"},
|
||
[]string{"--font-feature-settings", "--font-variation-settings"}); ok {
|
||
nodes := []*AstNode{d("font-family", families)}
|
||
if v, has := extra["--font-feature-settings"]; has {
|
||
nodes = append(nodes, d("font-feature-settings", v))
|
||
}
|
||
if v, has := extra["--font-variation-settings"]; has {
|
||
nodes = append(nodes, d("font-variation-settings", v))
|
||
}
|
||
return uList(nodes)
|
||
}
|
||
vv := candidate.Value.Value
|
||
if v, ok := theme.resolve(&vv, []string{"--font-weight"}, themeNone); ok {
|
||
return uNodes(atRoot([]*AstNode{property("--tw-font-weight", "", "")}),
|
||
d("--tw-font-weight", v), d("font-weight", v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.functionalUtility("font-features", utilityDescription{
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("font-feature-settings", value)) },
|
||
})
|
||
|
||
c.staticUtility("uppercase", []staticDecl{sd("text-transform", "uppercase")})
|
||
c.staticUtility("lowercase", []staticDecl{sd("text-transform", "lowercase")})
|
||
c.staticUtility("capitalize", []staticDecl{sd("text-transform", "capitalize")})
|
||
c.staticUtility("normal-case", []staticDecl{sd("text-transform", "none")})
|
||
c.staticUtility("italic", []staticDecl{sd("font-style", "italic")})
|
||
c.staticUtility("not-italic", []staticDecl{sd("font-style", "normal")})
|
||
c.staticUtility("underline", []staticDecl{sd("text-decoration-line", "underline")})
|
||
c.staticUtility("overline", []staticDecl{sd("text-decoration-line", "overline")})
|
||
c.staticUtility("line-through", []staticDecl{sd("text-decoration-line", "line-through")})
|
||
c.staticUtility("no-underline", []staticDecl{sd("text-decoration-line", "none")})
|
||
|
||
for _, pair := range [][2]string{
|
||
{"font-stretch-normal", "normal"}, {"font-stretch-ultra-condensed", "ultra-condensed"},
|
||
{"font-stretch-extra-condensed", "extra-condensed"}, {"font-stretch-condensed", "condensed"},
|
||
{"font-stretch-semi-condensed", "semi-condensed"}, {"font-stretch-semi-expanded", "semi-expanded"},
|
||
{"font-stretch-expanded", "expanded"}, {"font-stretch-extra-expanded", "extra-expanded"},
|
||
{"font-stretch-ultra-expanded", "ultra-expanded"},
|
||
} {
|
||
c.staticUtility(pair[0], []staticDecl{sd("font-stretch", pair[1])})
|
||
}
|
||
c.functionalUtility("font-stretch", utilityDescription{
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !strings.HasSuffix(v.Value, "%") {
|
||
return "", false
|
||
}
|
||
numStr := v.Value[:len(v.Value)-1]
|
||
if !isPositiveInteger(numStr) {
|
||
return "", false
|
||
}
|
||
n, ok := jsParseNumber(numStr)
|
||
if !ok || n < 50 || n > 200 {
|
||
return "", false
|
||
}
|
||
return v.Value, true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("font-stretch", value)) },
|
||
})
|
||
|
||
c.colorUtility("placeholder", colorUtilityDescription{
|
||
themeKeys: []string{"--placeholder-color", "--color"},
|
||
handle: func(value string) *utilResult {
|
||
return uNodes(styleRule("&::placeholder", d("--tw-sort", "placeholder-color"), d("color", value)))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("decoration-solid", []staticDecl{sd("text-decoration-style", "solid")})
|
||
c.staticUtility("decoration-double", []staticDecl{sd("text-decoration-style", "double")})
|
||
c.staticUtility("decoration-dotted", []staticDecl{sd("text-decoration-style", "dotted")})
|
||
c.staticUtility("decoration-dashed", []staticDecl{sd("text-decoration-style", "dashed")})
|
||
c.staticUtility("decoration-wavy", []staticDecl{sd("text-decoration-style", "wavy")})
|
||
c.staticUtility("decoration-auto", []staticDecl{sd("text-decoration-thickness", "auto")})
|
||
c.staticUtility("decoration-from-font", []staticDecl{sd("text-decoration-thickness", "from-font")})
|
||
|
||
registerUtilities12(c)
|
||
}
|
||
|
||
// registerUtilities12: text-decoration (functional) + animation.
|
||
|
||
func registerUtilities12(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
c.utilities.functional("decoration", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length", "percentage"})
|
||
}
|
||
switch typ {
|
||
case "length", "percentage":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("text-decoration-thickness", value))
|
||
default:
|
||
v, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("text-decoration-color", v))
|
||
}
|
||
}
|
||
vv := candidate.Value.Value
|
||
if v, ok := theme.resolve(&vv, []string{"--text-decoration-thickness"}, themeNone); ok {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("text-decoration-thickness", v))
|
||
}
|
||
if isPositiveInteger(vv) {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("text-decoration-thickness", vv+"px"))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-decoration-color", "--color"}); ok {
|
||
return uNodes(d("text-decoration-color", v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.functionalUtility("animate", utilityDescription{
|
||
themeKeys: []string{"--animate"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("animation", value)) },
|
||
staticValues: map[string][]*AstNode{"none": {d("animation", "none")}},
|
||
})
|
||
|
||
registerUtilities13(c)
|
||
}
|
||
|
||
func alphaReplacedDropShadowProperties(prop, value string, alpha *string, varInjector func(string) string, prefix string) []*AstNode {
|
||
requiresFallback := false
|
||
parts := segment(value, ",")
|
||
replacedParts := make([]string, len(parts))
|
||
for i, v := range parts {
|
||
replacedParts[i] = "drop-shadow(" + replaceShadowColors(v, func(color string) string {
|
||
if alpha == nil {
|
||
return varInjector(color)
|
||
}
|
||
if strings.HasPrefix(color, "current") {
|
||
return varInjector(withAlpha(color, *alpha))
|
||
}
|
||
if strings.HasPrefix(color, "var(") || strings.HasPrefix(*alpha, "var(") {
|
||
requiresFallback = true
|
||
}
|
||
return varInjector(replaceAlpha(color, *alpha))
|
||
}) + ")"
|
||
}
|
||
replacedValue := strings.Join(replacedParts, " ")
|
||
if requiresFallback {
|
||
fb := make([]string, len(parts))
|
||
for i, v := range parts {
|
||
fb[i] = "drop-shadow(" + replaceShadowColors(v, varInjector) + ")"
|
||
}
|
||
return []*AstNode{
|
||
decl(prop, prefix+strings.Join(fb, " ")),
|
||
rule("@supports (color: lab(from red l a b))", decl(prop, prefix+replacedValue)),
|
||
}
|
||
}
|
||
return []*AstNode{decl(prop, prefix+replacedValue)}
|
||
}
|
||
|
||
// registerUtilities13: filter / backdrop-filter and all individual filters.
|
||
func registerUtilities13(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
cssFilterValue := "var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)"
|
||
cssBackdropFilterValue := "var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)"
|
||
|
||
filterProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-blur", "", ""), property("--tw-brightness", "", ""), property("--tw-contrast", "", ""),
|
||
property("--tw-grayscale", "", ""), property("--tw-hue-rotate", "", ""), property("--tw-invert", "", ""),
|
||
property("--tw-opacity", "", ""), property("--tw-saturate", "", ""), property("--tw-sepia", "", ""),
|
||
property("--tw-drop-shadow", "", ""), property("--tw-drop-shadow-color", "", ""),
|
||
property("--tw-drop-shadow-alpha", "100%", "<percentage>"), property("--tw-drop-shadow-size", "", ""),
|
||
})
|
||
}
|
||
backdropFilterProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-backdrop-blur", "", ""), property("--tw-backdrop-brightness", "", ""),
|
||
property("--tw-backdrop-contrast", "", ""), property("--tw-backdrop-grayscale", "", ""),
|
||
property("--tw-backdrop-hue-rotate", "", ""), property("--tw-backdrop-invert", "", ""),
|
||
property("--tw-backdrop-opacity", "", ""), property("--tw-backdrop-saturate", "", ""),
|
||
property("--tw-backdrop-sepia", "", ""),
|
||
})
|
||
}
|
||
|
||
c.utilities.functional("filter", func(candidate *Candidate) *utilResult {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if candidate.Value == nil {
|
||
return uNodes(filterProperties(), d("filter", cssFilterValue))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
return uNodes(d("filter", candidate.Value.Value))
|
||
}
|
||
if candidate.Value.Value == "none" {
|
||
return uNodes(d("filter", "none"))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.utilities.functional("backdrop-filter", func(candidate *Candidate) *utilResult {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
if candidate.Value == nil {
|
||
return uNodes(backdropFilterProperties(), d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
return uNodes(d("-webkit-backdrop-filter", candidate.Value.Value), d("backdrop-filter", candidate.Value.Value))
|
||
}
|
||
if candidate.Value.Value == "none" {
|
||
return uNodes(d("-webkit-backdrop-filter", "none"), d("backdrop-filter", "none"))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
pctBare := func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "%", true
|
||
}
|
||
degBareF := func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "deg", true
|
||
}
|
||
|
||
// blur
|
||
c.functionalUtility("blur", utilityDescription{
|
||
themeKeys: []string{"--blur"},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(filterProperties(), d("--tw-blur", "blur("+value+")"), d("filter", cssFilterValue))
|
||
},
|
||
staticValues: map[string][]*AstNode{"none": {filterProperties(), d("--tw-blur", " "), d("filter", cssFilterValue)}},
|
||
})
|
||
c.functionalUtility("backdrop-blur", utilityDescription{
|
||
themeKeys: []string{"--backdrop-blur", "--blur"},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(backdropFilterProperties(), d("--tw-backdrop-blur", "blur("+value+")"),
|
||
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
|
||
},
|
||
staticValues: map[string][]*AstNode{"none": {backdropFilterProperties(), d("--tw-backdrop-blur", " "),
|
||
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue)}},
|
||
})
|
||
|
||
// simple filter factory (filter side)
|
||
simpleFilter := func(name, themeKey, fn string, defaultPct bool) {
|
||
desc := utilityDescription{
|
||
themeKeys: []string{themeKey},
|
||
handleBareValue: pctBare,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(filterProperties(), d("--tw-"+name, fn+"("+value+")"), d("filter", cssFilterValue))
|
||
},
|
||
}
|
||
if defaultPct {
|
||
desc.defaultValueSet = true
|
||
desc.defaultValue = sptr("100%")
|
||
}
|
||
c.functionalUtility(name, desc)
|
||
}
|
||
simpleBackdrop := func(name, twName, themeKeyA, themeKeyB, fn string, defaultPct bool, bare func(*UtilityValue) (string, bool)) {
|
||
desc := utilityDescription{
|
||
themeKeys: []string{themeKeyA, themeKeyB},
|
||
handleBareValue: bare,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(backdropFilterProperties(), d("--tw-"+twName, fn+"("+value+")"),
|
||
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
|
||
},
|
||
}
|
||
if defaultPct {
|
||
desc.defaultValueSet = true
|
||
desc.defaultValue = sptr("100%")
|
||
}
|
||
c.functionalUtility(name, desc)
|
||
}
|
||
|
||
simpleFilter("brightness", "--brightness", "brightness", false)
|
||
simpleBackdrop("backdrop-brightness", "backdrop-brightness", "--backdrop-brightness", "--brightness", "brightness", false, pctBare)
|
||
simpleFilter("contrast", "--contrast", "contrast", false)
|
||
simpleBackdrop("backdrop-contrast", "backdrop-contrast", "--backdrop-contrast", "--contrast", "contrast", false, pctBare)
|
||
simpleFilter("grayscale", "--grayscale", "grayscale", true)
|
||
simpleBackdrop("backdrop-grayscale", "backdrop-grayscale", "--backdrop-grayscale", "--grayscale", "grayscale", true, pctBare)
|
||
|
||
// hue-rotate (supportsNegative, deg)
|
||
c.functionalUtility("hue-rotate", utilityDescription{
|
||
supportsNegative: true, themeKeys: []string{"--hue-rotate"}, handleBareValue: degBareF,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(filterProperties(), d("--tw-hue-rotate", "hue-rotate("+value+")"), d("filter", cssFilterValue))
|
||
},
|
||
})
|
||
c.functionalUtility("backdrop-hue-rotate", utilityDescription{
|
||
supportsNegative: true, themeKeys: []string{"--backdrop-hue-rotate", "--hue-rotate"}, handleBareValue: degBareF,
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(backdropFilterProperties(), d("--tw-backdrop-hue-rotate", "hue-rotate("+value+")"),
|
||
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
|
||
},
|
||
})
|
||
|
||
simpleFilter("invert", "--invert", "invert", true)
|
||
simpleBackdrop("backdrop-invert", "backdrop-invert", "--backdrop-invert", "--invert", "invert", true, pctBare)
|
||
simpleFilter("saturate", "--saturate", "saturate", false)
|
||
simpleBackdrop("backdrop-saturate", "backdrop-saturate", "--backdrop-saturate", "--saturate", "saturate", false, pctBare)
|
||
simpleFilter("sepia", "--sepia", "sepia", true)
|
||
simpleBackdrop("backdrop-sepia", "backdrop-sepia", "--backdrop-sepia", "--sepia", "sepia", true, pctBare)
|
||
|
||
c.staticUtility("drop-shadow-none", []staticDecl{sdFn(filterProperties), sd("--tw-drop-shadow", " "), sd("filter", cssFilterValue)})
|
||
|
||
varInjector := func(color string) string { return "var(--tw-drop-shadow-color, " + color + ")" }
|
||
c.utilities.functional("drop-shadow", func(candidate *Candidate) *utilResult {
|
||
var alphaPtr *string
|
||
if candidate.Modifier != nil {
|
||
if candidate.Modifier.Kind == modArbitrary {
|
||
v := candidate.Modifier.Value
|
||
alphaPtr = &v
|
||
} else if isPositiveInteger(candidate.Modifier.Value) {
|
||
v := candidate.Modifier.Value + "%"
|
||
alphaPtr = &v
|
||
}
|
||
}
|
||
alphaDecl := func() *AstNode {
|
||
if alphaPtr != nil {
|
||
return d("--tw-drop-shadow-alpha", *alphaPtr)
|
||
}
|
||
return &AstNode{Kind: nDeclaration, Property: "--tw-drop-shadow-alpha", Undefined: true}
|
||
}
|
||
|
||
if candidate.Value == nil {
|
||
value, okGet := theme.Get([]string{"--drop-shadow"})
|
||
resolved, okRes := theme.resolve(nil, []string{"--drop-shadow"}, themeNone)
|
||
if !okGet || !okRes {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{filterProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
|
||
nodes = append(nodes, d("--tw-drop-shadow", dropShadowJoin(resolved)), d("filter", cssFilterValue))
|
||
return uList(nodes)
|
||
}
|
||
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color"})
|
||
}
|
||
if typ == "color" {
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(filterProperties(),
|
||
d("--tw-drop-shadow-color", withAlpha(cv, "var(--tw-drop-shadow-alpha)")),
|
||
d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
|
||
}
|
||
if candidate.Modifier != nil && alphaPtr == nil {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{filterProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
|
||
nodes = append(nodes, d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"), d("filter", cssFilterValue))
|
||
return uList(nodes)
|
||
}
|
||
|
||
cv := candidate.Value.Value
|
||
value, okGet := theme.Get([]string{"--drop-shadow-" + cv})
|
||
resolved, okRes := theme.resolve(&cv, []string{"--drop-shadow"}, themeNone)
|
||
if okGet && okRes {
|
||
if candidate.Modifier != nil && alphaPtr == nil {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{filterProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedDropShadowProperties("--tw-drop-shadow-size", value, alphaPtr, varInjector, "")...)
|
||
if alphaPtr != nil {
|
||
nodes = append(nodes, d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"), d("filter", cssFilterValue))
|
||
} else {
|
||
nodes = append(nodes, d("--tw-drop-shadow", dropShadowJoin(resolved)), d("filter", cssFilterValue))
|
||
}
|
||
return uList(nodes)
|
||
}
|
||
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--drop-shadow-color", "--color"}); ok {
|
||
if v == "inherit" {
|
||
return uNodes(filterProperties(), d("--tw-drop-shadow-color", "inherit"), d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
|
||
}
|
||
return uNodes(filterProperties(), d("--tw-drop-shadow-color", withAlpha(v, "var(--tw-drop-shadow-alpha)")), d("--tw-drop-shadow", "var(--tw-drop-shadow-size)"))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.functionalUtility("backdrop-opacity", utilityDescription{
|
||
themeKeys: []string{"--backdrop-opacity", "--opacity"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isValidOpacityValue(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "%", true
|
||
},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(backdropFilterProperties(), d("--tw-backdrop-opacity", "opacity("+value+")"),
|
||
d("-webkit-backdrop-filter", cssBackdropFilterValue), d("backdrop-filter", cssBackdropFilterValue))
|
||
},
|
||
})
|
||
|
||
registerUtilities14(c)
|
||
}
|
||
|
||
func dropShadowJoin(resolved string) string {
|
||
parts := segment(resolved, ",")
|
||
for i, p := range parts {
|
||
parts[i] = "drop-shadow(" + p + ")"
|
||
}
|
||
return strings.Join(parts, " ")
|
||
}
|
||
|
||
// registerUtilities14: transition/delay/duration/ease, will-change, content,
|
||
// contain, forced-color-adjust, leading, tracking, antialiasing,
|
||
// font-variant-numeric.
|
||
|
||
func registerUtilities14(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
dtf := "ease"
|
||
if v, ok := theme.resolve(nil, []string{"--default-transition-timing-function"}, themeNone); ok {
|
||
dtf = v
|
||
}
|
||
defaultTimingFunction := "var(--tw-ease, " + dtf + ")"
|
||
dd := "0s"
|
||
if v, ok := theme.resolve(nil, []string{"--default-transition-duration"}, themeNone); ok {
|
||
dd = v
|
||
}
|
||
defaultDuration := "var(--tw-duration, " + dd + ")"
|
||
|
||
transitionDefault := "color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events"
|
||
tdecls := func(prop string) []*AstNode {
|
||
return []*AstNode{d("transition-property", prop), d("transition-timing-function", defaultTimingFunction), d("transition-duration", defaultDuration)}
|
||
}
|
||
c.functionalUtility("transition", utilityDescription{
|
||
defaultValueSet: true, defaultValue: sptr(transitionDefault),
|
||
themeKeys: []string{"--transition-property"},
|
||
handle: func(value, _ string) *utilResult { return uList(tdecls(value)) },
|
||
staticValues: map[string][]*AstNode{
|
||
"none": {d("transition-property", "none")},
|
||
"all": tdecls("all"),
|
||
"colors": tdecls("color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to"),
|
||
"opacity": tdecls("opacity"),
|
||
"shadow": tdecls("box-shadow"),
|
||
"transform": tdecls("transform, translate, scale, rotate"),
|
||
},
|
||
})
|
||
c.staticUtility("transition-discrete", []staticDecl{sd("transition-behavior", "allow-discrete")})
|
||
c.staticUtility("transition-normal", []staticDecl{sd("transition-behavior", "normal")})
|
||
|
||
c.functionalUtility("delay", utilityDescription{
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "ms", true
|
||
},
|
||
themeKeys: []string{"--transition-delay"},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("transition-delay", value)) },
|
||
})
|
||
|
||
transitionDurationProperty := func() *AstNode { return atRoot([]*AstNode{property("--tw-duration", "", "")}) }
|
||
c.staticUtility("duration-initial", []staticDecl{sdFn(transitionDurationProperty), sd("--tw-duration", "initial")})
|
||
c.utilities.functional("duration", func(candidate *Candidate) *utilResult {
|
||
if candidate.Modifier != nil || candidate.Value == nil {
|
||
return nil
|
||
}
|
||
var value string
|
||
ok := false
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value, ok = candidate.Value.Value, true
|
||
} else {
|
||
key := candidate.Value.Value
|
||
if candidate.Value.Fraction != "" {
|
||
key = candidate.Value.Fraction
|
||
}
|
||
value, ok = theme.resolve(&key, []string{"--transition-duration"}, themeNone)
|
||
if !ok && isPositiveInteger(candidate.Value.Value) {
|
||
value, ok = candidate.Value.Value+"ms", true
|
||
}
|
||
}
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(transitionDurationProperty(), d("--tw-duration", value), d("transition-duration", value))
|
||
}, nil)
|
||
|
||
transitionTimingFunctionProperty := func() *AstNode { return atRoot([]*AstNode{property("--tw-ease", "", "")}) }
|
||
c.functionalUtility("ease", utilityDescription{
|
||
themeKeys: []string{"--ease"},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(transitionTimingFunctionProperty(), d("--tw-ease", value), d("transition-timing-function", value))
|
||
},
|
||
staticValues: map[string][]*AstNode{
|
||
"initial": {transitionTimingFunctionProperty(), d("--tw-ease", "initial")},
|
||
"linear": {transitionTimingFunctionProperty(), d("--tw-ease", "linear"), d("transition-timing-function", "linear")},
|
||
},
|
||
})
|
||
|
||
c.staticUtility("will-change-auto", []staticDecl{sd("will-change", "auto")})
|
||
c.staticUtility("will-change-scroll", []staticDecl{sd("will-change", "scroll-position")})
|
||
c.staticUtility("will-change-contents", []staticDecl{sd("will-change", "contents")})
|
||
c.staticUtility("will-change-transform", []staticDecl{sd("will-change", "transform")})
|
||
c.functionalUtility("will-change", utilityDescription{
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("will-change", value)) },
|
||
})
|
||
|
||
c.staticUtility("content-none", []staticDecl{sd("--tw-content", "none"), sd("content", "none")})
|
||
c.functionalUtility("content", utilityDescription{
|
||
themeKeys: []string{"--content"},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(atRoot([]*AstNode{property("--tw-content", `""`, "")}), d("--tw-content", value), d("content", "var(--tw-content)"))
|
||
},
|
||
})
|
||
|
||
cssContainValue := "var(--tw-contain-size,) var(--tw-contain-layout,) var(--tw-contain-paint,) var(--tw-contain-style,)"
|
||
cssContainProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-contain-size", "", ""), property("--tw-contain-layout", "", ""), property("--tw-contain-paint", "", ""), property("--tw-contain-style", "", "")})
|
||
}
|
||
c.staticUtility("contain-none", []staticDecl{sd("contain", "none")})
|
||
c.staticUtility("contain-content", []staticDecl{sd("contain", "content")})
|
||
c.staticUtility("contain-strict", []staticDecl{sd("contain", "strict")})
|
||
for _, pair := range [][2]string{
|
||
{"contain-size", "size"}, {"contain-inline-size", "inline-size"}, {"contain-layout", "layout"},
|
||
{"contain-paint", "paint"}, {"contain-style", "style"},
|
||
} {
|
||
twVar := "--tw-contain-" + strings.TrimPrefix(pair[0], "contain-")
|
||
if pair[0] == "contain-inline-size" {
|
||
twVar = "--tw-contain-size"
|
||
}
|
||
c.staticUtility(pair[0], []staticDecl{sdFn(cssContainProperties), sd(twVar, pair[1]), sd("contain", cssContainValue)})
|
||
}
|
||
c.functionalUtility("contain", utilityDescription{
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("contain", value)) },
|
||
})
|
||
|
||
c.staticUtility("forced-color-adjust-none", []staticDecl{sd("forced-color-adjust", "none")})
|
||
c.staticUtility("forced-color-adjust-auto", []staticDecl{sd("forced-color-adjust", "auto")})
|
||
|
||
c.spacingUtility("leading", []string{"--leading", "--spacing"},
|
||
func(value string) *utilResult {
|
||
return uNodes(atRoot([]*AstNode{property("--tw-leading", "", "")}), d("--tw-leading", value), d("line-height", value))
|
||
}, spacingOpts{staticValues: map[string][]*AstNode{
|
||
"none": {atRoot([]*AstNode{property("--tw-leading", "", "")}), d("--tw-leading", "1"), d("line-height", "1")},
|
||
}})
|
||
|
||
c.functionalUtility("tracking", utilityDescription{
|
||
supportsNegative: true,
|
||
themeKeys: []string{"--tracking"},
|
||
handle: func(value, _ string) *utilResult {
|
||
return uNodes(atRoot([]*AstNode{property("--tw-tracking", "", "")}), d("--tw-tracking", value), d("letter-spacing", value))
|
||
},
|
||
})
|
||
|
||
c.staticUtility("antialiased", []staticDecl{sd("-webkit-font-smoothing", "antialiased"), sd("-moz-osx-font-smoothing", "grayscale")})
|
||
c.staticUtility("subpixel-antialiased", []staticDecl{sd("-webkit-font-smoothing", "auto"), sd("-moz-osx-font-smoothing", "auto")})
|
||
|
||
cssFVN := "var(--tw-ordinal,) var(--tw-slashed-zero,) var(--tw-numeric-figure,) var(--tw-numeric-spacing,) var(--tw-numeric-fraction,)"
|
||
fvnProps := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-ordinal", "", ""), property("--tw-slashed-zero", "", ""), property("--tw-numeric-figure", "", ""), property("--tw-numeric-spacing", "", ""), property("--tw-numeric-fraction", "", "")})
|
||
}
|
||
c.staticUtility("normal-nums", []staticDecl{sd("font-variant-numeric", "normal")})
|
||
for _, e := range []struct{ name, twVar, val string }{
|
||
{"ordinal", "--tw-ordinal", "ordinal"}, {"slashed-zero", "--tw-slashed-zero", "slashed-zero"},
|
||
{"lining-nums", "--tw-numeric-figure", "lining-nums"}, {"oldstyle-nums", "--tw-numeric-figure", "oldstyle-nums"},
|
||
{"proportional-nums", "--tw-numeric-spacing", "proportional-nums"}, {"tabular-nums", "--tw-numeric-spacing", "tabular-nums"},
|
||
{"diagonal-fractions", "--tw-numeric-fraction", "diagonal-fractions"}, {"stacked-fractions", "--tw-numeric-fraction", "stacked-fractions"},
|
||
} {
|
||
e := e
|
||
c.staticUtility(e.name, []staticDecl{sdFn(fvnProps), sd(e.twVar, e.val), sd("font-variant-numeric", cssFVN)})
|
||
}
|
||
|
||
registerUtilities15(c)
|
||
}
|
||
|
||
func alphaReplacedShadowProperties(prop, value string, alpha *string, varInjector func(string) string, prefix string) []*AstNode {
|
||
requiresFallback := false
|
||
replacedValue := replaceShadowColors(value, func(color string) string {
|
||
if alpha == nil {
|
||
return varInjector(color)
|
||
}
|
||
if strings.HasPrefix(color, "current") {
|
||
return varInjector(withAlpha(color, *alpha))
|
||
}
|
||
if strings.HasPrefix(color, "var(") || strings.HasPrefix(*alpha, "var(") {
|
||
requiresFallback = true
|
||
}
|
||
return varInjector(replaceAlpha(color, *alpha))
|
||
})
|
||
applyPrefix := func(x string) string {
|
||
if prefix == "" {
|
||
return x
|
||
}
|
||
parts := segment(x, ",")
|
||
for i, v := range parts {
|
||
parts[i] = strings.TrimSpace(prefix) + " " + strings.TrimSpace(v)
|
||
}
|
||
return strings.Join(parts, ", ")
|
||
}
|
||
if requiresFallback {
|
||
return []*AstNode{
|
||
decl(prop, applyPrefix(replaceShadowColors(value, varInjector))),
|
||
rule("@supports (color: lab(from red l a b))", decl(prop, applyPrefix(replacedValue))),
|
||
}
|
||
}
|
||
return []*AstNode{decl(prop, applyPrefix(replacedValue))}
|
||
}
|
||
|
||
// shadowAlpha extracts an opacity modifier as an alpha (*string), matching the
|
||
// shadow/text-shadow/drop-shadow modifier handling.
|
||
func shadowAlpha(modifier *CandidateModifier) *string {
|
||
if modifier == nil {
|
||
return nil
|
||
}
|
||
if modifier.Kind == modArbitrary {
|
||
v := modifier.Value
|
||
return &v
|
||
}
|
||
if isPositiveInteger(modifier.Value) {
|
||
v := modifier.Value + "%"
|
||
return &v
|
||
}
|
||
return nil
|
||
}
|
||
|
||
// registerUtilities15: outline, opacity, underline-offset, text, text-shadow.
|
||
func registerUtilities15(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
outlineProperties := func() *AstNode { return atRoot([]*AstNode{property("--tw-outline-style", "solid", "")}) }
|
||
c.utilities.static("outline-hidden", func(_ *Candidate) *utilResult {
|
||
return uNodes(d("--tw-outline-style", "none"), d("outline-style", "none"),
|
||
atRule("@media", "(forced-colors: active)", d("outline", "2px solid transparent"), d("outline-offset", "2px")))
|
||
})
|
||
c.staticUtility("outline-none", []staticDecl{sd("--tw-outline-style", "none"), sd("outline-style", "none")})
|
||
c.staticUtility("outline-solid", []staticDecl{sd("--tw-outline-style", "solid"), sd("outline-style", "solid")})
|
||
c.staticUtility("outline-dashed", []staticDecl{sd("--tw-outline-style", "dashed"), sd("outline-style", "dashed")})
|
||
c.staticUtility("outline-dotted", []staticDecl{sd("--tw-outline-style", "dotted"), sd("outline-style", "dotted")})
|
||
c.staticUtility("outline-double", []staticDecl{sd("--tw-outline-style", "double"), sd("outline-style", "double")})
|
||
|
||
c.utilities.functional("outline", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value, ok := theme.Get([]string{"--default-outline-width"})
|
||
if !ok {
|
||
value = "1px"
|
||
}
|
||
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", value))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length", "number", "percentage"})
|
||
}
|
||
switch typ {
|
||
case "length", "number", "percentage":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", value))
|
||
default:
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("outline-color", cv))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--outline-color", "--color"}); ok {
|
||
return uNodes(d("outline-color", v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
vv := candidate.Value.Value
|
||
if v, ok := theme.resolve(&vv, []string{"--outline-width"}, themeNone); ok {
|
||
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", v))
|
||
} else if isPositiveInteger(vv) {
|
||
return uNodes(outlineProperties(), d("outline-style", "var(--tw-outline-style)"), d("outline-width", vv+"px"))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.functionalUtility("outline-offset", utilityDescription{
|
||
supportsNegative: true,
|
||
themeKeys: []string{"--outline-offset"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "px", true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("outline-offset", value)) },
|
||
})
|
||
|
||
c.functionalUtility("opacity", utilityDescription{
|
||
themeKeys: []string{"--opacity"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isValidOpacityValue(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "%", true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("opacity", value)) },
|
||
})
|
||
|
||
c.functionalUtility("underline-offset", utilityDescription{
|
||
supportsNegative: true,
|
||
themeKeys: []string{"--text-underline-offset"},
|
||
handleBareValue: func(v *UtilityValue) (string, bool) {
|
||
if !isPositiveInteger(v.Value) {
|
||
return "", false
|
||
}
|
||
return v.Value + "px", true
|
||
},
|
||
handle: func(value, _ string) *utilResult { return uNodes(d("text-underline-offset", value)) },
|
||
staticValues: map[string][]*AstNode{"auto": {d("text-underline-offset", "auto")}},
|
||
})
|
||
|
||
// resolveTextModifier resolves a `/<modifier>` on text-* into a line-height.
|
||
resolveTextModifier := func(modifier *CandidateModifier) (string, bool) {
|
||
var mod string
|
||
mok := false
|
||
if modifier.Kind == modArbitrary {
|
||
mod, mok = modifier.Value, true
|
||
} else if v, ok := theme.resolve(&modifier.Value, []string{"--leading"}, themeNone); ok {
|
||
mod, mok = v, true
|
||
}
|
||
if !mok && isValidSpacingMultiplier(modifier.Value) {
|
||
if _, ok := theme.resolve(nil, []string{"--spacing"}, themeNone); !ok {
|
||
return "", false
|
||
}
|
||
mod, mok = "--spacing("+modifier.Value+")", true
|
||
}
|
||
if !mok && modifier.Value == "none" {
|
||
mod, mok = "1", true
|
||
}
|
||
return mod, mok
|
||
}
|
||
|
||
c.utilities.functional("text", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length", "percentage", "absolute-size", "relative-size"})
|
||
}
|
||
switch typ {
|
||
case "size", "length", "percentage", "absolute-size", "relative-size":
|
||
if candidate.Modifier != nil {
|
||
mod, ok := resolveTextModifier(candidate.Modifier)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("font-size", value), d("line-height", mod))
|
||
}
|
||
return uNodes(d("font-size", value))
|
||
default:
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("color", cv))
|
||
}
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-color", "--color"}); ok {
|
||
return uNodes(d("color", v))
|
||
}
|
||
if fontSize, options, ok := theme.resolveWith(candidate.Value.Value, []string{"--text"},
|
||
[]string{"--line-height", "--letter-spacing", "--font-weight"}); ok {
|
||
if candidate.Modifier != nil {
|
||
mod, mok := resolveTextModifier(candidate.Modifier)
|
||
if !mok {
|
||
return nil
|
||
}
|
||
return uNodes(d("font-size", fontSize), d("line-height", mod))
|
||
}
|
||
nodes := []*AstNode{d("font-size", fontSize)}
|
||
if lh, has := options["--line-height"]; has {
|
||
nodes = append(nodes, d("line-height", "var(--tw-leading, "+lh+")"))
|
||
}
|
||
if ls, has := options["--letter-spacing"]; has {
|
||
nodes = append(nodes, d("letter-spacing", "var(--tw-tracking, "+ls+")"))
|
||
}
|
||
if fw, has := options["--font-weight"]; has {
|
||
nodes = append(nodes, d("font-weight", "var(--tw-font-weight, "+fw+")"))
|
||
}
|
||
return uList(nodes)
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
textShadowProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{property("--tw-text-shadow-color", "", ""), property("--tw-text-shadow-alpha", "100%", "<percentage>")})
|
||
}
|
||
c.staticUtility("text-shadow-initial", []staticDecl{sdFn(textShadowProperties), sd("--tw-text-shadow-color", "initial")})
|
||
tsInjector := func(color string) string { return "var(--tw-text-shadow-color, " + color + ")" }
|
||
c.utilities.functional("text-shadow", func(candidate *Candidate) *utilResult {
|
||
alpha := shadowAlpha(candidate.Modifier)
|
||
alphaDecl := func() *AstNode {
|
||
if alpha != nil {
|
||
return d("--tw-text-shadow-alpha", *alpha)
|
||
}
|
||
return &AstNode{Kind: nDeclaration, Property: "--tw-text-shadow-alpha", Undefined: true}
|
||
}
|
||
if candidate.Value == nil {
|
||
value, ok := theme.Get([]string{"--text-shadow"})
|
||
if !ok {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
|
||
return uList(nodes)
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color"})
|
||
}
|
||
if typ == "color" {
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", withAlpha(cv, "var(--tw-text-shadow-alpha)")))
|
||
}
|
||
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
|
||
return uList(nodes)
|
||
}
|
||
switch candidate.Value.Value {
|
||
case "none":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(textShadowProperties(), d("text-shadow", "none"))
|
||
case "inherit":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", "inherit"))
|
||
}
|
||
if value, ok := theme.Get([]string{"--text-shadow-" + candidate.Value.Value}); ok {
|
||
nodes := []*AstNode{textShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("text-shadow", value, alpha, tsInjector, "")...)
|
||
return uList(nodes)
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--text-shadow-color", "--color"}); ok {
|
||
return uNodes(textShadowProperties(), d("--tw-text-shadow-color", withAlpha(v, "var(--tw-text-shadow-alpha)")))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
registerUtilities16(c)
|
||
}
|
||
|
||
func undecl(p string) *AstNode { return &AstNode{Kind: nDeclaration, Property: p, Undefined: true} }
|
||
|
||
// registerUtilities16: box-shadow, inset-shadow, ring, inset-ring, ring-offset.
|
||
func registerUtilities16(c *utilCtx) {
|
||
d := decl
|
||
theme := c.theme
|
||
|
||
cssBoxShadowValue := "var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)"
|
||
nullShadow := "0 0 #0000"
|
||
boxShadowProperties := func() *AstNode {
|
||
return atRoot([]*AstNode{
|
||
property("--tw-shadow", nullShadow, ""), property("--tw-shadow-color", "", ""),
|
||
property("--tw-shadow-alpha", "100%", "<percentage>"),
|
||
property("--tw-inset-shadow", nullShadow, ""), property("--tw-inset-shadow-color", "", ""),
|
||
property("--tw-inset-shadow-alpha", "100%", "<percentage>"),
|
||
property("--tw-ring-color", "", ""), property("--tw-ring-shadow", nullShadow, ""),
|
||
property("--tw-inset-ring-color", "", ""), property("--tw-inset-ring-shadow", nullShadow, ""),
|
||
property("--tw-ring-inset", "", ""), property("--tw-ring-offset-width", "0px", "<length>"),
|
||
property("--tw-ring-offset-color", "#fff", ""), property("--tw-ring-offset-shadow", nullShadow, ""),
|
||
})
|
||
}
|
||
|
||
c.staticUtility("shadow-initial", []staticDecl{sdFn(boxShadowProperties), sd("--tw-shadow-color", "initial")})
|
||
|
||
shadowInj := func(color string) string { return "var(--tw-shadow-color, " + color + ")" }
|
||
c.utilities.functional("shadow", func(candidate *Candidate) *utilResult {
|
||
alpha := shadowAlpha(candidate.Modifier)
|
||
alphaDecl := func() *AstNode {
|
||
if alpha != nil {
|
||
return d("--tw-shadow-alpha", *alpha)
|
||
}
|
||
return undecl("--tw-shadow-alpha")
|
||
}
|
||
if candidate.Value == nil {
|
||
value, ok := theme.Get([]string{"--shadow"})
|
||
if !ok {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color"})
|
||
}
|
||
if typ == "color" {
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-shadow-color", withAlpha(cv, "var(--tw-shadow-alpha)")))
|
||
}
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
switch candidate.Value.Value {
|
||
case "none":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-shadow", nullShadow), d("box-shadow", cssBoxShadowValue))
|
||
case "inherit":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-shadow-color", "inherit"))
|
||
}
|
||
if value, ok := theme.Get([]string{"--shadow-" + candidate.Value.Value}); ok {
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-shadow", value, alpha, shadowInj, "")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--box-shadow-color", "--color"}); ok {
|
||
return uNodes(boxShadowProperties(), d("--tw-shadow-color", withAlpha(v, "var(--tw-shadow-alpha)")))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.staticUtility("inset-shadow-initial", []staticDecl{sdFn(boxShadowProperties), sd("--tw-inset-shadow-color", "initial")})
|
||
insetShadowInj := func(color string) string { return "var(--tw-inset-shadow-color, " + color + ")" }
|
||
c.utilities.functional("inset-shadow", func(candidate *Candidate) *utilResult {
|
||
alpha := shadowAlpha(candidate.Modifier)
|
||
alphaDecl := func() *AstNode {
|
||
if alpha != nil {
|
||
return d("--tw-inset-shadow-alpha", *alpha)
|
||
}
|
||
return undecl("--tw-inset-shadow-alpha")
|
||
}
|
||
if candidate.Value == nil {
|
||
value, ok := theme.Get([]string{"--inset-shadow"})
|
||
if !ok {
|
||
return nil
|
||
}
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color"})
|
||
}
|
||
if typ == "color" {
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", withAlpha(cv, "var(--tw-inset-shadow-alpha)")))
|
||
}
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "inset")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
switch candidate.Value.Value {
|
||
case "none":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-shadow", "inset "+nullShadow), d("box-shadow", cssBoxShadowValue))
|
||
case "inherit":
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", "inherit"))
|
||
}
|
||
if value, ok := theme.Get([]string{"--inset-shadow-" + candidate.Value.Value}); ok {
|
||
nodes := []*AstNode{boxShadowProperties(), alphaDecl()}
|
||
nodes = append(nodes, alphaReplacedShadowProperties("--tw-inset-shadow", value, alpha, insetShadowInj, "")...)
|
||
return uList(append(nodes, d("box-shadow", cssBoxShadowValue)))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--box-shadow-color", "--color"}); ok {
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-shadow-color", withAlpha(v, "var(--tw-inset-shadow-alpha)")))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
c.staticUtility("ring-inset", []staticDecl{sdFn(boxShadowProperties), sd("--tw-ring-inset", "inset")})
|
||
|
||
defaultRingColor := "currentcolor"
|
||
if v, ok := theme.Get([]string{"--default-ring-color"}); ok {
|
||
defaultRingColor = v
|
||
}
|
||
ringShadowValue := func(value string) string {
|
||
return "var(--tw-ring-inset,) 0 0 0 calc(" + value + " + var(--tw-ring-offset-width)) var(--tw-ring-color, " + defaultRingColor + ")"
|
||
}
|
||
c.utilities.functional("ring", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
value, ok := theme.Get([]string{"--default-ring-width"})
|
||
if !ok {
|
||
value = "1px"
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length"})
|
||
}
|
||
if typ == "length" {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-ring-color", cv))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-color", "--color"}); ok {
|
||
return uNodes(d("--tw-ring-color", v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
vv := candidate.Value.Value
|
||
value, ok := theme.resolve(&vv, []string{"--ring-width"}, themeNone)
|
||
if !ok && isPositiveInteger(vv) {
|
||
value, ok = vv+"px", true
|
||
}
|
||
if ok {
|
||
return uNodes(boxShadowProperties(), d("--tw-ring-shadow", ringShadowValue(value)), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
insetRingShadowValue := func(value string) string {
|
||
return "inset 0 0 0 " + value + " var(--tw-inset-ring-color, currentcolor)"
|
||
}
|
||
c.utilities.functional("inset-ring", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue("1px")), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length"})
|
||
}
|
||
if typ == "length" {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue(value)), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-inset-ring-color", cv))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-color", "--color"}); ok {
|
||
return uNodes(d("--tw-inset-ring-color", v))
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
vv := candidate.Value.Value
|
||
value, ok := theme.resolve(&vv, []string{"--ring-width"}, themeNone)
|
||
if !ok && isPositiveInteger(vv) {
|
||
value, ok = vv+"px", true
|
||
}
|
||
if ok {
|
||
return uNodes(boxShadowProperties(), d("--tw-inset-ring-shadow", insetRingShadowValue(value)), d("box-shadow", cssBoxShadowValue))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
ringOffsetShadowValue := "var(--tw-ring-inset,) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color)"
|
||
c.utilities.functional("ring-offset", func(candidate *Candidate) *utilResult {
|
||
if candidate.Value == nil {
|
||
return nil
|
||
}
|
||
if candidate.Value.Kind == uvArbitrary {
|
||
value := candidate.Value.Value
|
||
typ := candidate.Value.DataType
|
||
if typ == "" {
|
||
typ = inferDataType(value, []string{"color", "length"})
|
||
}
|
||
if typ == "length" {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-ring-offset-width", value), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
|
||
}
|
||
cv, ok := asColor(value, candidate.Modifier, theme)
|
||
if !ok {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-ring-offset-color", cv))
|
||
}
|
||
vv := candidate.Value.Value
|
||
if v, ok := theme.resolve(&vv, []string{"--ring-offset-width"}, themeNone); ok {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-ring-offset-width", v), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
|
||
} else if isPositiveInteger(vv) {
|
||
if candidate.Modifier != nil {
|
||
return nil
|
||
}
|
||
return uNodes(d("--tw-ring-offset-width", vv+"px"), d("--tw-ring-offset-shadow", ringOffsetShadowValue))
|
||
}
|
||
if v, ok := resolveThemeColor(candidate, theme, []string{"--ring-offset-color", "--color"}); ok {
|
||
return uNodes(d("--tw-ring-offset-color", v))
|
||
}
|
||
return nil
|
||
}, nil)
|
||
|
||
registerUtilities17(c)
|
||
}
|
||
|
||
// registerUtilities17: @container (container-type).
|
||
func registerUtilities17(c *utilCtx) {
|
||
d := decl
|
||
c.utilities.functional("@container", func(candidate *Candidate) *utilResult {
|
||
value := ""
|
||
ok := false
|
||
if candidate.Value == nil {
|
||
value, ok = "inline-size", true
|
||
} else if candidate.Value.Kind == uvArbitrary {
|
||
value, ok = candidate.Value.Value, true
|
||
} else if candidate.Value.Kind == uvNamed && candidate.Value.Value == "normal" {
|
||
value, ok = "normal", true
|
||
} else if candidate.Value.Kind == uvNamed && candidate.Value.Value == "size" {
|
||
value, ok = "size", true
|
||
}
|
||
if !ok {
|
||
return nil
|
||
}
|
||
if candidate.Modifier != nil {
|
||
return uNodes(d("container-type", value), d("container-name", candidate.Modifier.Value))
|
||
}
|
||
return uNodes(d("container-type", value))
|
||
}, nil)
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/value-parser.ts
|
||
//
|
||
// Parses a CSS value into a small AST of words, function calls, and
|
||
// separators. Used by arbitrary-value decoding and the theme()/calc() helpers.
|
||
// Nodes are pointers so passes (e.g. underscore decoding) can mutate them.
|
||
|
||
type ValueNode interface{ valueNode() }
|
||
|
||
type ValueWord struct{ Value string }
|
||
type ValueFunction struct {
|
||
Value string
|
||
Nodes []ValueNode
|
||
}
|
||
type ValueSeparator struct{ Value string }
|
||
|
||
func (*ValueWord) valueNode() {}
|
||
func (*ValueFunction) valueNode() {}
|
||
func (*ValueSeparator) valueNode() {}
|
||
|
||
func valueToCss(ast []ValueNode) string {
|
||
var b strings.Builder
|
||
writeValueNodes(&b, ast)
|
||
return b.String()
|
||
}
|
||
|
||
func writeValueNodes(b *strings.Builder, ast []ValueNode) {
|
||
for _, node := range ast {
|
||
switch n := node.(type) {
|
||
case *ValueWord:
|
||
b.WriteString(n.Value)
|
||
case *ValueSeparator:
|
||
b.WriteString(n.Value)
|
||
case *ValueFunction:
|
||
b.WriteString(n.Value)
|
||
b.WriteByte('(')
|
||
writeValueNodes(b, n.Nodes)
|
||
b.WriteByte(')')
|
||
}
|
||
}
|
||
}
|
||
|
||
func isValueSeparatorByte(c byte) bool {
|
||
switch c {
|
||
case ':', ',', '=', '>', '<', '\n', ' ', '\t':
|
||
return true
|
||
}
|
||
return false
|
||
}
|
||
|
||
func valueParse(input string) []ValueNode {
|
||
input = strings.ReplaceAll(input, "\r\n", "\n")
|
||
|
||
var ast []ValueNode
|
||
var stack []*ValueFunction
|
||
var parent *ValueFunction
|
||
var buf []byte
|
||
|
||
push := func(n ValueNode) {
|
||
if parent != nil {
|
||
parent.Nodes = append(parent.Nodes, n)
|
||
} else {
|
||
ast = append(ast, n)
|
||
}
|
||
}
|
||
flushWord := func() {
|
||
if len(buf) > 0 {
|
||
push(&ValueWord{Value: string(buf)})
|
||
buf = buf[:0]
|
||
}
|
||
}
|
||
|
||
for i := 0; i < len(input); i++ {
|
||
c := input[i]
|
||
switch {
|
||
case c == '\\':
|
||
// Escaped character: consume this and the next byte.
|
||
if i+1 < len(input) {
|
||
buf = append(buf, input[i], input[i+1])
|
||
i++
|
||
} else {
|
||
buf = append(buf, c)
|
||
}
|
||
|
||
case c == '/':
|
||
// `/` is its own word (e.g. theme(colors.red.500/10)).
|
||
flushWord()
|
||
push(&ValueWord{Value: "/"})
|
||
|
||
case isValueSeparatorByte(c):
|
||
flushWord()
|
||
start := i
|
||
end := i + 1
|
||
for ; end < len(input); end++ {
|
||
if !isValueSeparatorByte(input[end]) {
|
||
break
|
||
}
|
||
}
|
||
i = end - 1
|
||
push(&ValueSeparator{Value: input[start:end]})
|
||
|
||
case c == '\'' || c == '"':
|
||
start := i
|
||
for j := i + 1; j < len(input); j++ {
|
||
p := input[j]
|
||
if p == '\\' {
|
||
j++
|
||
continue
|
||
}
|
||
if p == c {
|
||
i = j
|
||
break
|
||
}
|
||
}
|
||
buf = append(buf, input[start:i+1]...)
|
||
|
||
case c == '(':
|
||
n := &ValueFunction{Value: string(buf)}
|
||
buf = buf[:0]
|
||
push(n)
|
||
stack = append(stack, n)
|
||
parent = n
|
||
|
||
case c == ')':
|
||
var tail *ValueFunction
|
||
if len(stack) > 0 {
|
||
tail = stack[len(stack)-1]
|
||
stack = stack[:len(stack)-1]
|
||
}
|
||
if len(buf) > 0 {
|
||
if tail != nil {
|
||
tail.Nodes = append(tail.Nodes, &ValueWord{Value: string(buf)})
|
||
}
|
||
buf = buf[:0]
|
||
}
|
||
if len(stack) > 0 {
|
||
parent = stack[len(stack)-1]
|
||
} else {
|
||
parent = nil
|
||
}
|
||
|
||
default:
|
||
buf = append(buf, c)
|
||
}
|
||
}
|
||
|
||
if len(buf) > 0 {
|
||
ast = append(ast, &ValueWord{Value: string(buf)})
|
||
}
|
||
|
||
return ast
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/variants.ts
|
||
//
|
||
// The Variants registry plus createVariants (the full variant catalog).
|
||
// suggest()/completions are stubbed (IntelliSense only). substituteAtVariant
|
||
// returns nothing (the Features bitset is not modeled).
|
||
|
||
type Compounds int
|
||
|
||
const (
|
||
CompoundsNever Compounds = 0
|
||
CompoundsAtRules Compounds = 1 << 0
|
||
CompoundsStyleRules Compounds = 1 << 1
|
||
)
|
||
|
||
type variantApplyFn func(r *AstNode, v *Variant) bool
|
||
|
||
type variantInfo struct {
|
||
kind variantKind
|
||
order int
|
||
applyFn variantApplyFn
|
||
compoundsWith Compounds
|
||
compounds Compounds
|
||
}
|
||
|
||
type vOpts struct {
|
||
compounds Compounds
|
||
hasCompounds bool
|
||
order int
|
||
hasOrder bool
|
||
}
|
||
|
||
type Variants struct {
|
||
variants map[string]*variantInfo
|
||
order []string
|
||
compareFns map[int]func(a, z *Variant) int
|
||
groupOrder *int
|
||
lastOrder int
|
||
}
|
||
|
||
func NewVariants() *Variants {
|
||
return &Variants{variants: map[string]*variantInfo{}, compareFns: map[int]func(a, z *Variant) int{}}
|
||
}
|
||
|
||
func (v *Variants) nextOrder() int {
|
||
if v.groupOrder != nil {
|
||
return *v.groupOrder
|
||
}
|
||
return v.lastOrder + 1
|
||
}
|
||
|
||
func (v *Variants) set(name string, kind variantKind, fn variantApplyFn, compoundsWith, compounds Compounds, o vOpts) {
|
||
if existing, ok := v.variants[name]; ok {
|
||
existing.kind = kind
|
||
existing.applyFn = fn
|
||
existing.compounds = compounds
|
||
return
|
||
}
|
||
order := o.order
|
||
if !o.hasOrder {
|
||
v.lastOrder = v.nextOrder()
|
||
order = v.lastOrder
|
||
}
|
||
v.variants[name] = &variantInfo{kind: kind, order: order, applyFn: fn, compoundsWith: compoundsWith, compounds: compounds}
|
||
v.order = append(v.order, name)
|
||
}
|
||
|
||
func compoundsOrDefault(o vOpts) Compounds {
|
||
if o.hasCompounds {
|
||
return o.compounds
|
||
}
|
||
return CompoundsStyleRules
|
||
}
|
||
|
||
func (v *Variants) static(name string, fn variantApplyFn, o vOpts) {
|
||
v.set(name, varStatic, fn, CompoundsNever, compoundsOrDefault(o), o)
|
||
}
|
||
|
||
func (v *Variants) functional(name string, fn variantApplyFn, o vOpts) {
|
||
v.set(name, varFunctional, fn, CompoundsNever, compoundsOrDefault(o), o)
|
||
}
|
||
|
||
func (v *Variants) compound(name string, compoundsWith Compounds, fn variantApplyFn, o vOpts) {
|
||
v.set(name, varCompound, fn, compoundsWith, compoundsOrDefault(o), o)
|
||
}
|
||
|
||
func (v *Variants) group(fn func(), compareFn func(a, z *Variant) int) {
|
||
o := v.nextOrder()
|
||
v.groupOrder = &o
|
||
if compareFn != nil {
|
||
v.compareFns[o] = compareFn
|
||
}
|
||
fn()
|
||
v.groupOrder = nil
|
||
}
|
||
|
||
func (v *Variants) has(name string) bool { _, ok := v.variants[name]; return ok }
|
||
|
||
func (v *Variants) get(name string) *variantInfo { return v.variants[name] }
|
||
|
||
func (v *Variants) kind(name string) variantKind {
|
||
if info, ok := v.variants[name]; ok {
|
||
return info.kind
|
||
}
|
||
return varStatic
|
||
}
|
||
|
||
func (v *Variants) keys() []string { return v.order }
|
||
|
||
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
|
||
}
|
||
|
||
// parseCustomVariant reads an @custom-variant at-rule into a name and the AST body that
|
||
// fromAst expects (a body whose rules contain an @slot where the utility goes).
|
||
//
|
||
// Two forms, both from Tailwind:
|
||
//
|
||
// @custom-variant dark (&:where(.dark, .dark *)); // shorthand
|
||
//
|
||
// @custom-variant dark { // block, explicit slot
|
||
// &:where(.dark, .dark *) { @slot; }
|
||
// }
|
||
//
|
||
// In the shorthand, a parenthesised selector starting with '@' is an at-rule
|
||
// (`@custom-variant any-hover (@media (any-hover: hover))`), and anything else is a
|
||
// selector. Several may be given, comma-separated at the top level.
|
||
func parseCustomVariant(node *AstNode) (name string, body []*AstNode, ok bool) {
|
||
params := strings.TrimSpace(node.Params)
|
||
if params == "" {
|
||
return "", nil, false
|
||
}
|
||
|
||
// The name is the first token; whatever follows is the shorthand's parenthesised part.
|
||
i := strings.IndexAny(params, " \t(")
|
||
if i < 0 {
|
||
// No shorthand: it must be the block form, which carries its own @slot.
|
||
if len(node.Nodes) == 0 {
|
||
return "", nil, false
|
||
}
|
||
return params, node.Nodes, true
|
||
}
|
||
name = strings.TrimSpace(params[:i])
|
||
rest := strings.TrimSpace(params[i:])
|
||
|
||
if rest == "" {
|
||
if len(node.Nodes) == 0 {
|
||
return "", nil, false
|
||
}
|
||
return name, node.Nodes, true
|
||
}
|
||
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
|
||
return "", nil, false
|
||
}
|
||
inner := strings.TrimSpace(rest[1 : len(rest)-1])
|
||
if inner == "" {
|
||
return "", nil, false
|
||
}
|
||
|
||
for _, sel := range splitTopLevel(inner, ',') {
|
||
sel = strings.TrimSpace(sel)
|
||
if sel == "" {
|
||
continue
|
||
}
|
||
slot := atRule("@slot", "")
|
||
if strings.HasPrefix(sel, "@") {
|
||
// "@media (any-hover: hover)" -> name "@media", params "(any-hover: hover)"
|
||
at, params, _ := strings.Cut(sel, " ")
|
||
body = append(body, atRule(at, strings.TrimSpace(params), slot))
|
||
continue
|
||
}
|
||
body = append(body, styleRule(sel, slot))
|
||
}
|
||
if len(body) == 0 {
|
||
return "", nil, false
|
||
}
|
||
return name, body, true
|
||
}
|
||
|
||
// splitTopLevel splits on sep, ignoring separators nested inside brackets — a selector
|
||
// list like `&:where(.dark, .dark *)` is ONE selector, and splitting it on its inner
|
||
// comma would produce two broken halves.
|
||
func splitTopLevel(s string, sep byte) []string {
|
||
var parts []string
|
||
depth := 0
|
||
start := 0
|
||
for i := 0; i < len(s); i++ {
|
||
switch s[i] {
|
||
case '(', '[':
|
||
depth++
|
||
case ')', ']':
|
||
depth--
|
||
case sep:
|
||
if depth == 0 {
|
||
parts = append(parts, s[start:i])
|
||
start = i + 1
|
||
}
|
||
}
|
||
}
|
||
return append(parts, s[start:])
|
||
}
|
||
|
||
// fromAst registers a variant whose body comes from CSS (@custom-variant).
|
||
func (v *Variants) fromAst(name string, ast []*AstNode, ds *DesignSystem) {
|
||
var selectors []string
|
||
usesAtVariant := false
|
||
astCopy := ast
|
||
walkAst(&astCopy, func(node *AstNode, _ *VisitContext) WalkResult {
|
||
if node.Kind == nRule {
|
||
selectors = append(selectors, node.Selector)
|
||
} else if node.Kind == nAtRule && node.Name == "@variant" {
|
||
usesAtVariant = true
|
||
} else if node.Kind == nAtRule && node.Name != "@slot" {
|
||
selectors = append(selectors, node.Name+" "+node.Params)
|
||
}
|
||
return WContinue
|
||
})
|
||
v.static(name, func(r *AstNode, _ *Variant) bool {
|
||
body := cloneAstNodes(ast)
|
||
if usesAtVariant {
|
||
substituteAtVariant(body, ds)
|
||
}
|
||
substituteAtSlot(body, r.Nodes)
|
||
r.Nodes = body
|
||
return true
|
||
}, vOpts{compounds: compoundsForSelectors(selectors), hasCompounds: true})
|
||
}
|
||
|
||
func compoundsForSelectors(selectors []string) Compounds {
|
||
compounds := CompoundsNever
|
||
for _, sel := range selectors {
|
||
if len(sel) > 0 && sel[0] == '@' {
|
||
if !strings.HasPrefix(sel, "@media") && !strings.HasPrefix(sel, "@supports") && !strings.HasPrefix(sel, "@container") {
|
||
return CompoundsNever
|
||
}
|
||
compounds |= CompoundsAtRules
|
||
continue
|
||
}
|
||
if strings.Contains(sel, "::") {
|
||
return CompoundsNever
|
||
}
|
||
compounds |= CompoundsStyleRules
|
||
}
|
||
return compounds
|
||
}
|
||
|
||
func addStaticVariant(variants *Variants, name string, selectors []string, o vOpts) {
|
||
if !o.hasCompounds {
|
||
o.compounds = compoundsForSelectors(selectors)
|
||
o.hasCompounds = true
|
||
}
|
||
sels := selectors
|
||
variants.static(name, func(r *AstNode, _ *Variant) bool {
|
||
orig := r.Nodes
|
||
newNodes := make([]*AstNode, len(sels))
|
||
for i, sel := range sels {
|
||
newNodes[i] = rule(sel, orig...)
|
||
}
|
||
r.Nodes = newNodes
|
||
return true
|
||
}, o)
|
||
}
|
||
|
||
func createVariants(theme *Theme) *Variants {
|
||
variants := NewVariants()
|
||
|
||
addStaticVariant(variants, "*", []string{":is(& > *)"}, vOpts{compounds: CompoundsNever, hasCompounds: true})
|
||
addStaticVariant(variants, "**", []string{":is(& *)"}, vOpts{compounds: CompoundsNever, hasCompounds: true})
|
||
|
||
registerCompoundVariants(variants, theme)
|
||
registerPseudoVariants(variants)
|
||
registerFunctionalVariants(variants)
|
||
registerBreakpointVariants(variants, theme)
|
||
registerMediaVariants(variants)
|
||
|
||
return variants
|
||
}
|
||
|
||
// ---- helpers shared by the variant catalog ------------------------------
|
||
|
||
func negateSelector(selector string) (string, bool) {
|
||
if strings.Contains(selector, "::") {
|
||
return "", false
|
||
}
|
||
parts := segment(selector, ",")
|
||
for i, sel := range parts {
|
||
parts[i] = strings.ReplaceAll(sel, "&", "*")
|
||
}
|
||
return "&:not(" + strings.Join(parts, ", ") + ")", true
|
||
}
|
||
|
||
var conditionalRules = []string{"@media", "@supports", "@container"}
|
||
|
||
func negateConditions(ruleName string, conditions []string) []string {
|
||
out := make([]string, len(conditions))
|
||
for i, condition := range conditions {
|
||
if ruleName == "@container" {
|
||
ast := valueParse(strings.TrimSpace(condition))
|
||
switch {
|
||
case len(ast) >= 1 && isValueFn(ast[0]):
|
||
out[i] = "not " + condition
|
||
case len(ast) >= 3 && isValueWordVal(ast[0], "not") && isValueFn(ast[2]):
|
||
ast = ast[2:]
|
||
out[i] = valueToCss(ast)
|
||
case len(ast) >= 5 && isValueWord(ast[0]) && isValueWordVal(ast[2], "not") && isValueFn(ast[4]):
|
||
ast = append(ast[:2], ast[4:]...)
|
||
out[i] = valueToCss(ast)
|
||
case len(ast) >= 3 && isValueWord(ast[0]) && !isValueWordVal(ast[0], "not") && isValueFn(ast[2]):
|
||
rest := append([]ValueNode{&ValueSeparator{Value: " "}, &ValueWord{Value: "not"}}, ast[1:]...)
|
||
ast = append([]ValueNode{ast[0]}, rest...)
|
||
out[i] = valueToCss(ast)
|
||
default:
|
||
out[i] = "not " + condition
|
||
}
|
||
} else {
|
||
condition = strings.TrimSpace(condition)
|
||
parts := segment(condition, " ")
|
||
if parts[0] == "not" {
|
||
out[i] = strings.Join(parts[1:], " ")
|
||
} else {
|
||
out[i] = "not " + condition
|
||
}
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
func isValueFn(n ValueNode) bool { _, ok := n.(*ValueFunction); return ok }
|
||
func isValueWord(n ValueNode) bool { _, ok := n.(*ValueWord); return ok }
|
||
func isValueWordVal(n ValueNode, val string) bool {
|
||
w, ok := n.(*ValueWord)
|
||
return ok && w.Value == val
|
||
}
|
||
|
||
func negateAtRule(node *AstNode) *AstNode {
|
||
for _, ruleName := range conditionalRules {
|
||
if ruleName != node.Name {
|
||
continue
|
||
}
|
||
conditions := segment(node.Params, ",")
|
||
if len(conditions) > 1 {
|
||
return nil
|
||
}
|
||
conditions = negateConditions(node.Name, conditions)
|
||
return atRule(node.Name, strings.Join(conditions, ", "))
|
||
}
|
||
return nil
|
||
}
|
||
|
||
var reSupportsFn = regexp.MustCompile(`^[\w-]*\s*\(`)
|
||
var reSupportsBool = regexp.MustCompile(`\b(and|or|not)\b`)
|
||
|
||
func quoteAttributeValue(input string) string {
|
||
if strings.Contains(input, "=") {
|
||
segs := segment(input, "=")
|
||
attribute := segs[0]
|
||
value := strings.TrimSpace(strings.Join(segs[1:], "="))
|
||
if len(value) > 0 && (value[0] == '\'' || value[0] == '"') {
|
||
return input
|
||
}
|
||
if len(value) > 1 {
|
||
tc := value[len(value)-1]
|
||
if value[len(value)-2] == ' ' && (tc == 'i' || tc == 'I' || tc == 's' || tc == 'S') {
|
||
return attribute + "=\"" + value[:len(value)-2] + "\" " + string(tc)
|
||
}
|
||
}
|
||
return attribute + "=\"" + value + "\""
|
||
}
|
||
return input
|
||
}
|
||
|
||
func substituteAtSlot(ast []*AstNode, nodes []*AstNode) {
|
||
astCopy := ast
|
||
walkAst(&astCopy, func(node *AstNode, _ *VisitContext) WalkResult {
|
||
if node.Kind == nAtRule && node.Name == "@slot" {
|
||
return WReplaceSkip(nodes...)
|
||
}
|
||
if node.Kind == nAtRule && (node.Name == "@keyframes" || node.Name == "@property") {
|
||
*node = *atRoot([]*AstNode{atRule(node.Name, node.Params, node.Nodes...)})
|
||
return WSkip
|
||
}
|
||
return WContinue
|
||
})
|
||
}
|
||
|
||
func substituteAtVariant(ast []*AstNode, ds *DesignSystem) {
|
||
astCopy := ast
|
||
walkAst(&astCopy, func(variantNode *AstNode, _ *VisitContext) WalkResult {
|
||
if variantNode.Kind != nAtRule || variantNode.Name != "@variant" {
|
||
return WContinue
|
||
}
|
||
var nodes []*AstNode
|
||
compoundVariants := segment(variantNode.Params, ",")
|
||
for idx, compoundVariant := range compoundVariants {
|
||
var childNodes []*AstNode
|
||
if idx == len(compoundVariants)-1 {
|
||
childNodes = variantNode.Nodes
|
||
} else {
|
||
childNodes = cloneAstNodes(variantNode.Nodes)
|
||
}
|
||
node := styleRule("&", childNodes...)
|
||
stacked := segment(compoundVariant, ":")
|
||
for i := len(stacked) - 1; i >= 0; i-- {
|
||
name := strings.TrimSpace(stacked[i])
|
||
if name == "" {
|
||
return WContinue
|
||
}
|
||
variantAst := ds.parseVariant(name)
|
||
if variantAst == nil {
|
||
return WContinue
|
||
}
|
||
if !applyVariant(node, variantAst, ds.variants, 0) {
|
||
return WContinue
|
||
}
|
||
}
|
||
nodes = append(nodes, node)
|
||
}
|
||
return WReplace(nodes...)
|
||
})
|
||
}
|
||
|
||
func registerCompoundVariants(variants *Variants, theme *Theme) {
|
||
prefixDot := func(base string) string {
|
||
if theme.Prefix != "" {
|
||
return ":where(." + theme.Prefix + "\\:" + base
|
||
}
|
||
return ":where(." + base
|
||
}
|
||
|
||
variants.compound("not", CompoundsStyleRules|CompoundsAtRules, func(ruleNode *AstNode, variant *Variant) bool {
|
||
if variant.Variant.Kind == varArbitrary && variant.Variant.Relative {
|
||
return false
|
||
}
|
||
if variant.Modifier != nil {
|
||
return false
|
||
}
|
||
didApply := false
|
||
list := []*AstNode{ruleNode}
|
||
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
|
||
if node.Kind != nRule && node.Kind != nAtRule {
|
||
return WContinue
|
||
}
|
||
if len(node.Nodes) > 0 {
|
||
return WContinue
|
||
}
|
||
var atRules, styleRules []*AstNode
|
||
path := ctx.Path()
|
||
path = append(path, node)
|
||
for _, p := range path {
|
||
if p.Kind == nAtRule {
|
||
atRules = append(atRules, p)
|
||
} else if p.Kind == nRule {
|
||
styleRules = append(styleRules, p)
|
||
}
|
||
}
|
||
if len(atRules) > 1 {
|
||
return WStop
|
||
}
|
||
if len(styleRules) > 1 {
|
||
return WStop
|
||
}
|
||
var rules []*AstNode
|
||
for _, sr := range styleRules {
|
||
sel, ok := negateSelector(sr.Selector)
|
||
if !ok {
|
||
didApply = false
|
||
return WStop
|
||
}
|
||
rules = append(rules, styleRule(sel))
|
||
}
|
||
for _, ar := range atRules {
|
||
neg := negateAtRule(ar)
|
||
if neg == nil {
|
||
didApply = false
|
||
return WStop
|
||
}
|
||
rules = append(rules, neg)
|
||
}
|
||
*ruleNode = *styleRule("&", rules...)
|
||
didApply = true
|
||
return WSkip
|
||
})
|
||
if ruleNode.Kind == nRule && ruleNode.Selector == "&" && len(ruleNode.Nodes) == 1 {
|
||
*ruleNode = *ruleNode.Nodes[0]
|
||
}
|
||
return didApply
|
||
}, vOpts{})
|
||
|
||
groupPeer := func(name, suffix string) {
|
||
variants.compound(name, CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
|
||
if variant.Variant.Kind == varArbitrary && variant.Variant.Relative {
|
||
return false
|
||
}
|
||
var variantSelector string
|
||
if variant.Modifier != nil {
|
||
variantSelector = prefixDot(name+"\\/"+variant.Modifier.Value) + ")"
|
||
} else {
|
||
variantSelector = prefixDot(name) + ")"
|
||
}
|
||
didApply := false
|
||
list := []*AstNode{ruleNode}
|
||
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
|
||
if node.Kind != nRule {
|
||
return WContinue
|
||
}
|
||
for _, parent := range ctx.Path() {
|
||
if parent.Kind != nRule {
|
||
continue
|
||
}
|
||
didApply = false
|
||
return WStop
|
||
}
|
||
selector := strings.ReplaceAll(node.Selector, "&", variantSelector)
|
||
if len(segment(selector, ",")) > 1 {
|
||
selector = ":is(" + selector + ")"
|
||
}
|
||
node.Selector = "&:is(" + selector + suffix + ")"
|
||
didApply = true
|
||
return WContinue
|
||
})
|
||
return didApply
|
||
}, vOpts{})
|
||
}
|
||
groupPeer("group", " *")
|
||
groupPeer("peer", " ~ *")
|
||
|
||
variants.compound("in", CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
|
||
if variant.Modifier != nil {
|
||
return false
|
||
}
|
||
didApply := false
|
||
list := []*AstNode{ruleNode}
|
||
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
|
||
if node.Kind != nRule {
|
||
return WContinue
|
||
}
|
||
for _, parent := range ctx.Path() {
|
||
if parent.Kind != nRule {
|
||
continue
|
||
}
|
||
didApply = false
|
||
return WStop
|
||
}
|
||
node.Selector = ":where(" + strings.ReplaceAll(node.Selector, "&", "*") + ") &"
|
||
didApply = true
|
||
return WContinue
|
||
})
|
||
return didApply
|
||
}, vOpts{})
|
||
|
||
variants.compound("has", CompoundsStyleRules, func(ruleNode *AstNode, variant *Variant) bool {
|
||
if variant.Modifier != nil {
|
||
return false
|
||
}
|
||
didApply := false
|
||
list := []*AstNode{ruleNode}
|
||
walkAst(&list, func(node *AstNode, ctx *VisitContext) WalkResult {
|
||
if node.Kind != nRule {
|
||
return WContinue
|
||
}
|
||
for _, parent := range ctx.Path() {
|
||
if parent.Kind != nRule {
|
||
continue
|
||
}
|
||
didApply = false
|
||
return WStop
|
||
}
|
||
node.Selector = "&:has(" + strings.ReplaceAll(node.Selector, "&", "*") + ")"
|
||
didApply = true
|
||
return WContinue
|
||
})
|
||
return didApply
|
||
}, vOpts{})
|
||
}
|
||
|
||
func registerPseudoVariants(variants *Variants) {
|
||
sv := func(name string, selectors ...string) { addStaticVariant(variants, name, selectors, vOpts{}) }
|
||
|
||
sv("first-letter", "&::first-letter")
|
||
sv("first-line", "&::first-line")
|
||
sv("marker", "& *::marker", "&::marker", "& *::-webkit-details-marker", "&::-webkit-details-marker")
|
||
sv("selection", "& *::selection", "&::selection")
|
||
sv("file", "&::file-selector-button")
|
||
sv("placeholder", "&::placeholder")
|
||
sv("backdrop", "&::backdrop")
|
||
sv("details-content", "&::details-content")
|
||
|
||
contentProps := func() *AstNode {
|
||
return atRoot([]*AstNode{atRule("@property", "--tw-content",
|
||
decl("syntax", `"*"`), decl("initial-value", `""`), decl("inherits", "false"))})
|
||
}
|
||
pseudoContent := func(name, sel string) {
|
||
variants.static(name, func(v *AstNode, _ *Variant) bool {
|
||
inner := append([]*AstNode{contentProps(), decl("content", "var(--tw-content)")}, v.Nodes...)
|
||
v.Nodes = []*AstNode{styleRule(sel, inner...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsNever, hasCompounds: true})
|
||
}
|
||
pseudoContent("before", "&::before")
|
||
pseudoContent("after", "&::after")
|
||
|
||
sv("first", "&:first-child")
|
||
sv("last", "&:last-child")
|
||
sv("only", "&:only-child")
|
||
sv("odd", "&:nth-child(odd)")
|
||
sv("even", "&:nth-child(even)")
|
||
sv("first-of-type", "&:first-of-type")
|
||
sv("last-of-type", "&:last-of-type")
|
||
sv("only-of-type", "&:only-of-type")
|
||
|
||
sv("visited", "&:visited")
|
||
sv("target", "&:target")
|
||
sv("open", "&:is([open], :popover-open, :open)")
|
||
|
||
sv("default", "&:default")
|
||
sv("checked", "&:checked")
|
||
sv("indeterminate", "&:indeterminate")
|
||
sv("placeholder-shown", "&:placeholder-shown")
|
||
sv("autofill", "&:autofill")
|
||
sv("optional", "&:optional")
|
||
sv("required", "&:required")
|
||
sv("valid", "&:valid")
|
||
sv("invalid", "&:invalid")
|
||
sv("user-valid", "&:user-valid")
|
||
sv("user-invalid", "&:user-invalid")
|
||
sv("in-range", "&:in-range")
|
||
sv("out-of-range", "&:out-of-range")
|
||
sv("read-only", "&:read-only")
|
||
|
||
sv("empty", "&:empty")
|
||
|
||
sv("focus-within", "&:focus-within")
|
||
variants.static("hover", func(r *AstNode, _ *Variant) bool {
|
||
r.Nodes = []*AstNode{styleRule("&:hover", atRule("@media", "(hover: hover)", r.Nodes...))}
|
||
return true
|
||
}, vOpts{})
|
||
sv("focus", "&:focus")
|
||
sv("focus-visible", "&:focus-visible")
|
||
sv("active", "&:active")
|
||
sv("enabled", "&:enabled")
|
||
sv("disabled", "&:disabled")
|
||
sv("inert", "&:is([inert], [inert] *)")
|
||
}
|
||
|
||
func registerFunctionalVariants(variants *Variants) {
|
||
variants.functional("aria", func(r *AstNode, variant *Variant) bool {
|
||
if variant.Value == nil || variant.Modifier != nil {
|
||
return false
|
||
}
|
||
if variant.Value.Kind == vvArbitrary {
|
||
r.Nodes = []*AstNode{styleRule("&[aria-"+quoteAttributeValue(variant.Value.Value)+"]", r.Nodes...)}
|
||
} else {
|
||
r.Nodes = []*AstNode{styleRule("&[aria-"+variant.Value.Value+"=\"true\"]", r.Nodes...)}
|
||
}
|
||
return true
|
||
}, vOpts{})
|
||
|
||
variants.functional("data", func(r *AstNode, variant *Variant) bool {
|
||
if variant.Value == nil || variant.Modifier != nil {
|
||
return false
|
||
}
|
||
r.Nodes = []*AstNode{styleRule("&[data-"+quoteAttributeValue(variant.Value.Value)+"]", r.Nodes...)}
|
||
return true
|
||
}, vOpts{})
|
||
|
||
nthVariant := func(name, pseudo string) {
|
||
variants.functional(name, func(r *AstNode, variant *Variant) bool {
|
||
if variant.Value == nil || variant.Modifier != nil {
|
||
return false
|
||
}
|
||
if variant.Value.Kind == vvNamed && !isPositiveInteger(variant.Value.Value) {
|
||
return false
|
||
}
|
||
r.Nodes = []*AstNode{styleRule("&:"+pseudo+"("+variant.Value.Value+")", r.Nodes...)}
|
||
return true
|
||
}, vOpts{})
|
||
}
|
||
nthVariant("nth", "nth-child")
|
||
nthVariant("nth-last", "nth-last-child")
|
||
nthVariant("nth-of-type", "nth-of-type")
|
||
nthVariant("nth-last-of-type", "nth-last-of-type")
|
||
|
||
variants.functional("supports", func(r *AstNode, variant *Variant) bool {
|
||
if variant.Value == nil || variant.Modifier != nil {
|
||
return false
|
||
}
|
||
value := variant.Value.Value
|
||
if value == "" {
|
||
return false
|
||
}
|
||
if reSupportsFn.MatchString(value) {
|
||
query := reSupportsBool.ReplaceAllString(value, " $1 ")
|
||
r.Nodes = []*AstNode{atRule("@supports", query, r.Nodes...)}
|
||
return true
|
||
}
|
||
if !strings.Contains(value, ":") {
|
||
value = value + ": var(--tw)"
|
||
}
|
||
if value[0] != '(' || value[len(value)-1] != ')' {
|
||
value = "(" + value + ")"
|
||
}
|
||
r.Nodes = []*AstNode{atRule("@supports", value, r.Nodes...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}
|
||
|
||
func registerBreakpointVariants(variants *Variants, theme *Theme) {
|
||
compareBP := func(a, z *Variant, direction string, lookup func(*Variant) (string, bool)) int {
|
||
if a == z {
|
||
return 0
|
||
}
|
||
av, aok := lookup(a)
|
||
if !aok {
|
||
if direction == "asc" {
|
||
return -1
|
||
}
|
||
return 1
|
||
}
|
||
zv, zok := lookup(z)
|
||
if !zok {
|
||
if direction == "asc" {
|
||
return 1
|
||
}
|
||
return -1
|
||
}
|
||
return compareBreakpoints(av, zv, direction)
|
||
}
|
||
|
||
breakpoints := theme.namespace("--breakpoint")
|
||
resolvedBreakpoints := func(variant *Variant) (string, bool) {
|
||
switch variant.Kind {
|
||
case varStatic:
|
||
return theme.resolveValue(sptr(variant.Root), []string{"--breakpoint"})
|
||
case varFunctional:
|
||
if variant.Value == nil || variant.Modifier != nil {
|
||
return "", false
|
||
}
|
||
var value string
|
||
var ok bool
|
||
if variant.Value.Kind == vvArbitrary {
|
||
value, ok = variant.Value.Value, true
|
||
} else {
|
||
value, ok = theme.resolveValue(sptr(variant.Value.Value), []string{"--breakpoint"})
|
||
}
|
||
if !ok || value == "" || strings.Contains(value, "var(") {
|
||
return "", false
|
||
}
|
||
return value, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
variants.group(func() {
|
||
variants.functional("max", func(r *AstNode, variant *Variant) bool {
|
||
if variant.Modifier != nil {
|
||
return false
|
||
}
|
||
value, ok := resolvedBreakpoints(variant)
|
||
if !ok {
|
||
return false
|
||
}
|
||
r.Nodes = []*AstNode{atRule("@media", "(width < "+value+")", r.Nodes...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}, func(a, z *Variant) int { return compareBP(a, z, "desc", resolvedBreakpoints) })
|
||
|
||
variants.group(func() {
|
||
for _, key := range breakpoints.Keys() {
|
||
value, _ := breakpoints.Get(key)
|
||
val := value
|
||
variants.static(key, func(r *AstNode, _ *Variant) bool {
|
||
r.Nodes = []*AstNode{atRule("@media", "(width >= "+val+")", r.Nodes...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}
|
||
variants.functional("min", func(r *AstNode, variant *Variant) bool {
|
||
if variant.Modifier != nil {
|
||
return false
|
||
}
|
||
value, ok := resolvedBreakpoints(variant)
|
||
if !ok {
|
||
return false
|
||
}
|
||
r.Nodes = []*AstNode{atRule("@media", "(width >= "+value+")", r.Nodes...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}, func(a, z *Variant) int { return compareBP(a, z, "asc", resolvedBreakpoints) })
|
||
|
||
resolvedWidths := func(variant *Variant) (string, bool) {
|
||
if variant.Kind == varFunctional {
|
||
if variant.Value == nil {
|
||
return "", false
|
||
}
|
||
var value string
|
||
var ok bool
|
||
if variant.Value.Kind == vvArbitrary {
|
||
value, ok = variant.Value.Value, true
|
||
} else {
|
||
value, ok = theme.resolveValue(sptr(variant.Value.Value), []string{"--container"})
|
||
}
|
||
if !ok || value == "" || strings.Contains(value, "var(") {
|
||
return "", false
|
||
}
|
||
return value, true
|
||
}
|
||
return "", false
|
||
}
|
||
|
||
variants.group(func() {
|
||
variants.functional("@max", func(r *AstNode, variant *Variant) bool {
|
||
value, ok := resolvedWidths(variant)
|
||
if !ok {
|
||
return false
|
||
}
|
||
params := "(width < " + value + ")"
|
||
if variant.Modifier != nil {
|
||
params = variant.Modifier.Value + " " + params
|
||
}
|
||
r.Nodes = []*AstNode{atRule("@container", params, r.Nodes...)}
|
||
return true
|
||
}, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}, func(a, z *Variant) int { return compareBP(a, z, "desc", resolvedWidths) })
|
||
|
||
variants.group(func() {
|
||
atFn := func(r *AstNode, variant *Variant) bool {
|
||
value, ok := resolvedWidths(variant)
|
||
if !ok {
|
||
return false
|
||
}
|
||
params := "(width >= " + value + ")"
|
||
if variant.Modifier != nil {
|
||
params = variant.Modifier.Value + " " + params
|
||
}
|
||
r.Nodes = []*AstNode{atRule("@container", params, r.Nodes...)}
|
||
return true
|
||
}
|
||
variants.functional("@", atFn, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
variants.functional("@min", atFn, vOpts{compounds: CompoundsAtRules, hasCompounds: true})
|
||
}, func(a, z *Variant) int { return compareBP(a, z, "asc", resolvedWidths) })
|
||
}
|
||
|
||
func registerMediaVariants(variants *Variants) {
|
||
sv := func(name string, selectors ...string) { addStaticVariant(variants, name, selectors, vOpts{}) }
|
||
|
||
sv("motion-safe", "@media (prefers-reduced-motion: no-preference)")
|
||
sv("motion-reduce", "@media (prefers-reduced-motion: reduce)")
|
||
sv("contrast-more", "@media (prefers-contrast: more)")
|
||
sv("contrast-less", "@media (prefers-contrast: less)")
|
||
sv("portrait", "@media (orientation: portrait)")
|
||
sv("landscape", "@media (orientation: landscape)")
|
||
sv("ltr", `&:where(:dir(ltr), [dir="ltr"], [dir="ltr"] *)`)
|
||
sv("rtl", `&:where(:dir(rtl), [dir="rtl"], [dir="rtl"] *)`)
|
||
sv("dark", "@media (prefers-color-scheme: dark)")
|
||
sv("starting", "@starting-style")
|
||
sv("print", "@media print")
|
||
sv("forced-colors", "@media (forced-colors: active)")
|
||
sv("inverted-colors", "@media (inverted-colors: inverted)")
|
||
sv("pointer-none", "@media (pointer: none)")
|
||
sv("pointer-coarse", "@media (pointer: coarse)")
|
||
sv("pointer-fine", "@media (pointer: fine)")
|
||
sv("any-pointer-none", "@media (any-pointer: none)")
|
||
sv("any-pointer-coarse", "@media (any-pointer: coarse)")
|
||
sv("any-pointer-fine", "@media (any-pointer: fine)")
|
||
sv("noscript", "@media (scripting: none)")
|
||
}
|
||
|
||
// Port of packages/tailwindcss/src/walk.ts
|
||
//
|
||
// Depth-first AST traversal with enter/exit hooks that may continue, skip
|
||
// children, stop, or replace the current node.
|
||
|
||
type walkKind int
|
||
|
||
const (
|
||
wkContinue walkKind = iota
|
||
wkSkip
|
||
wkStop
|
||
wkReplace
|
||
wkReplaceSkip
|
||
wkReplaceStop
|
||
)
|
||
|
||
type WalkResult struct {
|
||
kind walkKind
|
||
nodes []*AstNode
|
||
}
|
||
|
||
var (
|
||
WContinue = WalkResult{kind: wkContinue}
|
||
WSkip = WalkResult{kind: wkSkip}
|
||
WStop = WalkResult{kind: wkStop}
|
||
)
|
||
|
||
func WReplace(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplace, nodes: nodes} }
|
||
func WReplaceSkip(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplaceSkip, nodes: nodes} }
|
||
func WReplaceStop(nodes ...*AstNode) WalkResult { return WalkResult{kind: wkReplaceStop, nodes: nodes} }
|
||
|
||
type VisitContext struct {
|
||
Parent *AstNode
|
||
Depth int
|
||
Index int
|
||
Siblings []*AstNode
|
||
ancestor []*AstNode
|
||
}
|
||
|
||
func (c *VisitContext) Path() []*AstNode {
|
||
return append([]*AstNode{}, c.ancestor...)
|
||
}
|
||
|
||
func spliceNodes(nodes *[]*AstNode, idx int, repl []*AstNode) {
|
||
s := *nodes
|
||
out := make([]*AstNode, 0, len(s)-1+len(repl))
|
||
out = append(out, s[:idx]...)
|
||
out = append(out, repl...)
|
||
out = append(out, s[idx+1:]...)
|
||
*nodes = out
|
||
}
|
||
|
||
type walkFn func(node *AstNode, ctx *VisitContext) WalkResult
|
||
|
||
func walkAst(nodes *[]*AstNode, enter walkFn) {
|
||
walkAstImpl(nodes, nil, 0, nil, enter, nil)
|
||
}
|
||
|
||
func walkAstEnterExit(nodes *[]*AstNode, enter, exit walkFn) {
|
||
walkAstImpl(nodes, nil, 0, nil, enter, exit)
|
||
}
|
||
|
||
func walkAstImpl(nodes *[]*AstNode, parent *AstNode, depth int, ancestors []*AstNode, enter, exit walkFn) bool {
|
||
i := 0
|
||
for i < len(*nodes) {
|
||
node := (*nodes)[i]
|
||
ctx := &VisitContext{Parent: parent, Depth: depth, Index: i, Siblings: *nodes, ancestor: ancestors}
|
||
|
||
res := WContinue
|
||
if enter != nil {
|
||
res = enter(node, ctx)
|
||
}
|
||
|
||
switch res.kind {
|
||
case wkStop:
|
||
return false
|
||
case wkReplaceStop:
|
||
spliceNodes(nodes, i, res.nodes)
|
||
return false
|
||
case wkReplace:
|
||
spliceNodes(nodes, i, res.nodes)
|
||
continue
|
||
case wkReplaceSkip:
|
||
spliceNodes(nodes, i, res.nodes)
|
||
i += len(res.nodes)
|
||
continue
|
||
case wkSkip:
|
||
case wkContinue:
|
||
if ch := nodeChildren(node); ch != nil && len(*ch) > 0 {
|
||
childAnc := append(append([]*AstNode{}, ancestors...), node)
|
||
if !walkAstImpl(ch, node, depth+1, childAnc, enter, exit) {
|
||
return false
|
||
}
|
||
}
|
||
}
|
||
|
||
if exit != nil {
|
||
res2 := exit(node, ctx)
|
||
switch res2.kind {
|
||
case wkStop:
|
||
return false
|
||
case wkReplaceStop:
|
||
spliceNodes(nodes, i, res2.nodes)
|
||
return false
|
||
case wkReplace, wkReplaceSkip:
|
||
spliceNodes(nodes, i, res2.nodes)
|
||
i += len(res2.nodes)
|
||
continue
|
||
}
|
||
}
|
||
|
||
i++
|
||
}
|
||
return true
|
||
}
|