Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 43 additions & 2 deletions src/internal/builder/css.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,15 +56,56 @@ func ToCSS(tokens []parser.Token) string {
})

for _, t := range catTokens {
// Use the original var() reference if available, resolved literal otherwise
value := t.InitialValue
if t.RawInitialValue != "" {
value = t.RawInitialValue
}

if t.Deprecated {
sb.WriteString(fmt.Sprintf(" /* @deprecated */ %s: %s;\n", t.Name, t.InitialValue))
sb.WriteString(fmt.Sprintf(" /* @deprecated */ %s: %s;\n", t.Name, value))
} else {
sb.WriteString(fmt.Sprintf(" %s: %s;\n", t.Name, t.InitialValue))
sb.WriteString(fmt.Sprintf(" %s: %s;\n", t.Name, value))
}
}
}

sb.WriteString("}\n")

// Collect mode overrides across all tokens
modeTokens := make(map[string][]parser.Token) // modeName → tokens with that mode
var modeOrder []string

for _, t := range tokens {
for modeName := range t.Modes {
if _, exists := modeTokens[modeName]; !exists {
modeOrder = append(modeOrder, modeName)
}
modeTokens[modeName] = append(modeTokens[modeName], t)
}
}

sort.Strings(modeOrder)

for _, modeName := range modeOrder {
sb.WriteString(fmt.Sprintf("\n:root[data-color-mode='%s'] {\n", modeName))

mTokens := modeTokens[modeName]
sort.Slice(mTokens, func(i, j int) bool {
return mTokens[i].Name < mTokens[j].Name
})

for _, t := range mTokens {
// Use the original var() reference if available
value := t.Modes[modeName]
if raw, exists := t.RawModes[modeName]; exists {
value = raw
}
sb.WriteString(fmt.Sprintf(" %s: %s;\n", t.Name, value))
}

sb.WriteString("}\n")
}

return sb.String()
}
71 changes: 71 additions & 0 deletions src/internal/parser/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var (
descriptorRe = regexp.MustCompile(`([\w-]+)\s*:\s*("[^"]*"|[^;]+);?`)
rootBlockRe = regexp.MustCompile(`:root\s*\{([^}]+)\}`)
customPropRe = regexp.MustCompile(`(--[\w-]+)\s*:\s*([^;]+);`)
varRefRe = regexp.MustCompile(`var\((--[\w-]+)\)`)
)

// ParseFiles parses multiple CSS files and returns all tokens
Expand Down Expand Up @@ -84,9 +85,68 @@ func Parse(css string, filename string) ([]Token, error) {
result = append(result, *token)
}

// Resolve var() references in initial values
resolveVarReferences(result)

return result, nil
}

// resolveVarReferences resolves var(--X) references in InitialValue fields.
// When a token's initial-value contains var(--other-token), the reference is
// chased through the token graph until a literal value is found.
// The original unresolved value is preserved in RawInitialValue for CSS output.
func resolveVarReferences(tokens []Token) {
byName := make(map[string]*Token)
for i := range tokens {
byName[tokens[i].Name] = &tokens[i]
}

for i := range tokens {
if varRefRe.MatchString(tokens[i].InitialValue) {
tokens[i].RawInitialValue = tokens[i].InitialValue
tokens[i].InitialValue = resolveVarValue(tokens[i].InitialValue, byName, nil)
}

for modeName, modeValue := range tokens[i].Modes {
if varRefRe.MatchString(modeValue) {
if tokens[i].RawModes == nil {
tokens[i].RawModes = make(map[string]string)
}
tokens[i].RawModes[modeName] = modeValue
tokens[i].Modes[modeName] = resolveVarValue(modeValue, byName, nil)
}
}
}
}

func resolveVarValue(value string, byName map[string]*Token, seen map[string]bool) string {
if seen == nil {
seen = make(map[string]bool)
}

return varRefRe.ReplaceAllStringFunc(value, func(match string) string {
sub := varRefRe.FindStringSubmatch(match)
name := sub[1]

if seen[name] {
return match // circular reference — keep as-is
}
seen[name] = true

target, exists := byName[name]
if !exists {
return match // unknown var — keep as-is
}

resolved := target.InitialValue
if varRefRe.MatchString(resolved) {
resolved = resolveVarValue(resolved, byName, seen)
}

return resolved
})
}

func parseDescriptors(body string, token *Token) {
matches := descriptorRe.FindAllStringSubmatch(body, -1)

Expand Down Expand Up @@ -126,6 +186,17 @@ func parseDescriptors(body string, token *Token) {
}
}
token.Examples = cleaned
default:
// mode-<name> descriptors (e.g., mode-dark, mode-high-contrast)
if strings.HasPrefix(key, "mode-") {
modeName := strings.TrimPrefix(key, "mode-")
if modeName != "" {
if token.Modes == nil {
token.Modes = make(map[string]string)
}
token.Modes[modeName] = value
}
}
}
}
}
Expand Down
Loading
Loading