Manually merge from 'pre-kjol' into 'master'
This commit is contained in:
@@ -1261,8 +1261,7 @@ func compileCandidates(rawCandidates []string, ds *DesignSystem, onInvalid func(
|
||||
// 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
|
||||
// @utility/--value()); @custom-variant is not yet wired. -mta
|
||||
|
||||
//go:embed tw_theme.css
|
||||
var defaultThemeCSS string
|
||||
@@ -1305,7 +1304,8 @@ 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
|
||||
hasUtilities := false
|
||||
|
||||
@@ -1361,8 +1361,13 @@ 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 {
|
||||
safelistRemove = append(safelistRemove, literals...)
|
||||
} else {
|
||||
safelistAdd = append(safelistAdd, literals...)
|
||||
}
|
||||
default:
|
||||
passthrough = append(passthrough, node)
|
||||
}
|
||||
@@ -1375,21 +1380,24 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
}
|
||||
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)
|
||||
}
|
||||
if len(safelistAdd) > 0 {
|
||||
candidates = append(candidates, safelistAdd...)
|
||||
}
|
||||
if len(safelistRemove) > 0 {
|
||||
remove := make(map[string]bool, len(safelistRemove))
|
||||
for _, c := range safelistRemove {
|
||||
remove[c] = true
|
||||
}
|
||||
filtered := candidates[:0:0]
|
||||
for _, c := range candidates {
|
||||
if !remove[c] {
|
||||
filtered = append(filtered, c)
|
||||
}
|
||||
}
|
||||
candidates = filtered
|
||||
}
|
||||
|
||||
ds := buildDesignSystem(theme)
|
||||
|
||||
// Register @utility blocks as static utilities.
|
||||
for _, u := range customUtilities {
|
||||
@@ -1438,14 +1446,6 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
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...))
|
||||
}
|
||||
|
||||
@@ -1459,6 +1459,95 @@ func twCompile(input, baseDir string, candidates []string) (string, int, error)
|
||||
return toCss(out), len(astNodes), nil
|
||||
}
|
||||
|
||||
// parseSourceDirective parses the params of an `@source` at-rule, supporting
|
||||
// the safelist form `@source inline("<pattern>")` / `@source not inline("<pattern>")`
|
||||
// (mirrors upstream Tailwind v4's inline source safelist). Each quoted string
|
||||
// literal is brace-expanded (e.g. `{text,bg}-{red,blue}-{100,200}`) into the
|
||||
// literal candidate classes it names. File-glob `@source "./path/**/*.html"`
|
||||
// directives are not supported and are ignored.
|
||||
func parseSourceDirective(params string) (literals []string, negate bool) {
|
||||
trimmed := strings.TrimSpace(params)
|
||||
if rest, ok := strings.CutPrefix(trimmed, "not "); ok {
|
||||
negate = true
|
||||
trimmed = strings.TrimSpace(rest)
|
||||
}
|
||||
if !strings.HasPrefix(trimmed, "inline(") || !strings.HasSuffix(trimmed, ")") {
|
||||
return nil, false
|
||||
}
|
||||
inner := trimmed[len("inline(") : len(trimmed)-1]
|
||||
for _, lit := range extractQuotedLiterals(inner) {
|
||||
literals = append(literals, expandBraces(lit)...)
|
||||
}
|
||||
return literals, negate
|
||||
}
|
||||
|
||||
// extractQuotedLiterals returns the contents of every single- or
|
||||
// double-quoted string literal found in s.
|
||||
func extractQuotedLiterals(s string) []string {
|
||||
var out []string
|
||||
for i := 0; i < len(s); i++ {
|
||||
quote := s[i]
|
||||
if quote != '\'' && quote != '"' {
|
||||
continue
|
||||
}
|
||||
j := i + 1
|
||||
for j < len(s) && s[j] != quote {
|
||||
if s[j] == '\\' && j+1 < len(s) {
|
||||
j++
|
||||
}
|
||||
j++
|
||||
}
|
||||
out = append(out, s[i+1:min(j, len(s))])
|
||||
i = j
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// expandBraces expands shell-style brace groups in pattern, e.g.
|
||||
// "{text,bg}-red-{100,200}" -> ["text-red-100", "text-red-200",
|
||||
// "bg-red-100", "bg-red-200"]. A brace group may also be a numeric range
|
||||
// ("{1..3}" -> "1", "2", "3"). Groups are expanded left to right; a pattern
|
||||
// with no braces expands to itself.
|
||||
func expandBraces(pattern string) []string {
|
||||
start := strings.IndexByte(pattern, '{')
|
||||
if start == -1 {
|
||||
return []string{pattern}
|
||||
}
|
||||
relEnd := strings.IndexByte(pattern[start:], '}')
|
||||
if relEnd == -1 {
|
||||
return []string{pattern}
|
||||
}
|
||||
end := start + relEnd
|
||||
prefix, inner, suffix := pattern[:start], pattern[start+1:end], pattern[end+1:]
|
||||
|
||||
var parts []string
|
||||
if a, b, ok := strings.Cut(inner, ".."); ok && !strings.Contains(inner, ",") {
|
||||
lo, loErr := strconv.Atoi(strings.TrimSpace(a))
|
||||
hi, hiErr := strconv.Atoi(strings.TrimSpace(b))
|
||||
if loErr == nil && hiErr == nil {
|
||||
step := 1
|
||||
if lo > hi {
|
||||
step = -1
|
||||
}
|
||||
for n := lo; ; n += step {
|
||||
parts = append(parts, strconv.Itoa(n))
|
||||
if n == hi {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if parts == nil {
|
||||
parts = strings.Split(inner, ",")
|
||||
}
|
||||
|
||||
var out []string
|
||||
for _, p := range parts {
|
||||
out = append(out, expandBraces(prefix+p+suffix)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// scanSources scans the given glob/** patterns (relative to baseDir) for
|
||||
// candidate class names using the bundler's scanner.
|
||||
func scanSources(baseDir string, patterns []string) []string {
|
||||
@@ -8831,95 +8920,6 @@ func (v *Variants) compare(a, z *Variant) int {
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user