deven broke tailwind
This commit is contained in:
@@ -1304,6 +1304,7 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
var keyframes []*AstNode
|
||||
var passthrough []*AstNode
|
||||
var customUtilities []*AstNode
|
||||
var customVariants []*AstNode
|
||||
var safelistAdd []string
|
||||
var safelistRemove []string
|
||||
hasPreflight := false
|
||||
@@ -1361,6 +1362,8 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
processTheme(node)
|
||||
case node.Kind == nAtRule && node.Name == "@utility":
|
||||
customUtilities = append(customUtilities, node)
|
||||
case node.Kind == nAtRule && node.Name == "@custom-variant":
|
||||
customVariants = append(customVariants, node)
|
||||
case node.Kind == nAtRule && node.Name == "@source":
|
||||
literals, negate := parseSourceDirective(node.Params)
|
||||
if negate {
|
||||
@@ -1399,6 +1402,20 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
|
||||
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)
|
||||
@@ -8813,6 +8830,95 @@ func (v *Variants) kind(name string) variantKind {
|
||||
|
||||
func (v *Variants) keys() []string { return v.order }
|
||||
|
||||
// parseCustomVariant reads an @custom-variant at-rule into a name and the AST body that
|
||||
// fromAst expects (a body whose rules contain an @slot where the utility goes).
|
||||
//
|
||||
// Two forms, both from Tailwind:
|
||||
//
|
||||
// @custom-variant dark (&:where(.dark, .dark *)); // shorthand
|
||||
//
|
||||
// @custom-variant dark { // block, explicit slot
|
||||
// &:where(.dark, .dark *) { @slot; }
|
||||
// }
|
||||
//
|
||||
// In the shorthand, a parenthesised selector starting with '@' is an at-rule
|
||||
// (`@custom-variant any-hover (@media (any-hover: hover))`), and anything else is a
|
||||
// selector. Several may be given, comma-separated at the top level.
|
||||
func parseCustomVariant(node *AstNode) (name string, body []*AstNode, ok bool) {
|
||||
params := strings.TrimSpace(node.Params)
|
||||
if params == "" {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
// The name is the first token; whatever follows is the shorthand's parenthesised part.
|
||||
i := strings.IndexAny(params, " \t(")
|
||||
if i < 0 {
|
||||
// No shorthand: it must be the block form, which carries its own @slot.
|
||||
if len(node.Nodes) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
return params, node.Nodes, true
|
||||
}
|
||||
name = strings.TrimSpace(params[:i])
|
||||
rest := strings.TrimSpace(params[i:])
|
||||
|
||||
if rest == "" {
|
||||
if len(node.Nodes) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
return name, node.Nodes, true
|
||||
}
|
||||
if !strings.HasPrefix(rest, "(") || !strings.HasSuffix(rest, ")") {
|
||||
return "", nil, false
|
||||
}
|
||||
inner := strings.TrimSpace(rest[1 : len(rest)-1])
|
||||
if inner == "" {
|
||||
return "", nil, false
|
||||
}
|
||||
|
||||
for _, sel := range splitTopLevel(inner, ',') {
|
||||
sel = strings.TrimSpace(sel)
|
||||
if sel == "" {
|
||||
continue
|
||||
}
|
||||
slot := atRule("@slot", "")
|
||||
if strings.HasPrefix(sel, "@") {
|
||||
// "@media (any-hover: hover)" -> name "@media", params "(any-hover: hover)"
|
||||
at, params, _ := strings.Cut(sel, " ")
|
||||
body = append(body, atRule(at, strings.TrimSpace(params), slot))
|
||||
continue
|
||||
}
|
||||
body = append(body, styleRule(sel, slot))
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return "", nil, false
|
||||
}
|
||||
return name, body, true
|
||||
}
|
||||
|
||||
// splitTopLevel splits on sep, ignoring separators nested inside brackets — a selector
|
||||
// list like `&:where(.dark, .dark *)` is ONE selector, and splitting it on its inner
|
||||
// comma would produce two broken halves.
|
||||
func splitTopLevel(s string, sep byte) []string {
|
||||
var parts []string
|
||||
depth := 0
|
||||
start := 0
|
||||
for i := 0; i < len(s); i++ {
|
||||
switch s[i] {
|
||||
case '(', '[':
|
||||
depth++
|
||||
case ')', ']':
|
||||
depth--
|
||||
case sep:
|
||||
if depth == 0 {
|
||||
parts = append(parts, s[start:i])
|
||||
start = i + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
return append(parts, s[start:])
|
||||
}
|
||||
|
||||
func (v *Variants) compoundsWith(parent string, child *Variant) bool {
|
||||
parentInfo, ok := v.variants[parent]
|
||||
if !ok {
|
||||
|
||||
Reference in New Issue
Block a user