Documentation
¶
Overview ¶
Package cssgen provides CSS-to-Go code generation for type-safe CSS class constants.
Package cssgen provides CSS constant generation and linting for Go/templ projects.
cssgen generates type-safe Go constants from CSS files and provides a linter to eliminate hardcoded class strings and catch typos at build time.
Generation ¶
Generate Go constants from CSS files:
config := cssgen.Config{
SourceDir: "web/styles",
OutputDir: "internal/ui",
PackageName: "ui",
Includes: []string{"**/*.css"},
}
result, err := cssgen.Generate(config)
Linting ¶
Lint CSS class usage in Go/templ files:
lintConfig := cssgen.LintConfig{
SourceDir: "web/styles",
OutputDir: "internal/ui",
LintPaths: []string{"internal/**/*.{templ,go}"},
StrictMode: false,
}
result, err := cssgen.Lint(lintConfig)
CLI Tool ¶
cssgen also provides a CLI tool. Install with:
go install github.com/yacobolo/cssgen/cmd/cssgen@latest
See cmd/cssgen/README.md for CLI documentation.
Package cssgen provides CSS class constant generation and linting.
Pure 1:1 Class Mapping ¶
The generator creates one Go constant for each CSS class with exact 1:1 mapping:
- CSS: .btn → Go: Btn = "btn"
- CSS: .btn--brand → Go: BtnBrand = "btn--brand"
- CSS: .card__header → Go: CardHeader = "card__header"
Usage in templates:
<button class={ ui.Btn, ui.BtnBrand }>Click</button>
// Produces: <button class="btn btn--brand">
<div class={ ui.Card, ui.CardHeader }>Content</div>
// Produces: <div class="card card__header">
Smart Linter with Greedy Token Matching ¶
When analyzing class="btn btn--brand", the linter:
- Checks if exact match exists for full string
- If no match, splits into tokens: ["btn", "btn--brand"]
- Matches each token individually: btn → ui.Btn, btn--brand → ui.BtnBrand
- Suggests: { ui.Btn, ui.BtnBrand }
This results in predictable, accurate suggestions with zero "pollution."
Match Results ¶
- Matched: Constant available (e.g., "btn" → ui.Btn)
- Unmatched: Class exists in CSS but no constant generated (e.g., utility classes)
- Invalid: Class doesn't exist in CSS (typo or missing)
Index ¶
- Constants
- func AnalyzeClasses(classes []*CSSClass) error
- func GetRelativePath(absPath string) string
- func ParseGeneratedFile(path string) (map[string]string, map[string]bool, error)
- func PrintLintReport(result *LintResult, w io.Writer, verbose bool)
- func ScanFiles(scanPatterns []string, verbose bool) ([]ClassReference, ScanStats, error)
- func WriteGoFile(publicClasses []*CSSClass, allClasses []*CSSClass, config Config, ...) error
- func WriteGoFiles(publicClasses []*CSSClass, allClasses []*CSSClass, config Config, ...) error
- func WriteJSON(w io.Writer, result *LintResult) error
- func WriteMarkdown(w io.Writer, result *LintResult) error
- func WriteOutput(w io.Writer, result *LintResult, format OutputFormat, config LintConfig)
- type CSSClass
- type CSSLookup
- type CategorizedProperty
- type ClassAnalysis
- type ClassReference
- type ClassificationResult
- type Config
- type ConstantSuggestion
- type FileLocation
- type GenerateResult
- type HardcodedString
- type InvalidClass
- type Issue
- type IssuePos
- type JSONIssue
- type JSONOutput
- type JSONQuickWin
- type JSONQuickWins
- type JSONStats
- type JSONSummary
- type Layer
- type LineRange
- type LintConfig
- type LintResult
- type MatchType
- type OutputFormat
- type PropertyCategory
- type PropertyDiff
- type PseudoStateProperties
- type QuickWin
- type QuickWinsSummary
- type Replacement
- type Reporter
- type ScanStats
- type UnusedClass
- type VerboseReporter
Constants ¶
const ( SeverityError = "error" SeverityWarning = "warning" SeverityInfo = "" )
IssueSeverity constants
const ( IssueInvalidClass = "invalid CSS class %q not found in stylesheet" IssueHardcodedClass = "hardcoded CSS class %q should use %s constant" IssueUnusedConstant = "exported constant %s is unused" )
IssueType constants matching linter categories
Variables ¶
This section is empty.
Functions ¶
func AnalyzeClasses ¶
AnalyzeClasses builds inheritance graph and resolves full class names
func GetRelativePath ¶
GetRelativePath returns a relative path from the current working directory
func ParseGeneratedFile ¶
ParseGeneratedFile reads styles.gen.go and all related split files (styles_*.gen.go) and extracts constant definitions and AllCSSClasses
func PrintLintReport ¶
func PrintLintReport(result *LintResult, w io.Writer, verbose bool)
PrintLintReport formats and prints the lint report
func ScanFiles ¶
func ScanFiles(scanPatterns []string, verbose bool) ([]ClassReference, ScanStats, error)
ScanFiles scans files matching the given patterns for CSS class references
func WriteGoFile ¶
func WriteGoFile(publicClasses []*CSSClass, allClasses []*CSSClass, config Config, stats GenerateResult) error
WriteGoFile generates multiple output .go files split by component
func WriteGoFiles ¶
func WriteGoFiles(publicClasses []*CSSClass, allClasses []*CSSClass, config Config, stats GenerateResult) error
WriteGoFiles generates multiple output .go files split by component
func WriteJSON ¶
func WriteJSON(w io.Writer, result *LintResult) error
WriteJSON writes the lint result as JSON
func WriteMarkdown ¶
func WriteMarkdown(w io.Writer, result *LintResult) error
WriteMarkdown generates a Markdown report (shareable in GitHub, Slack, wikis)
func WriteOutput ¶
func WriteOutput(w io.Writer, result *LintResult, format OutputFormat, config LintConfig)
WriteOutput writes the lint result in the specified format
Types ¶
type CSSClass ¶
type CSSClass struct {
Name string // "btn--primary"
GoName string // "BtnPrimary"
// FullClasses field REMOVED - no longer needed with 1:1 mapping
Layer string // "components"
Properties map[string]string // CSS properties (cleaned)
ParentClass *CSSClass // Link to base class (for comments/context only)
PseudoStates []string // [":hover", ":focus"] - included in comments
PseudoStateProperties []PseudoStateProperties // Property changes in pseudo-states
PropertyDiff *PropertyDiff // Diff vs. parent class
Intent string // Human intent from @intent comment
IsUtility bool // True if atomic utility class (no BEM)
IsInternal bool // True if starts with _ (skip public const)
SourceFile string // For debugging/conflict resolution
}
CSSClass represents a parsed CSS class with full context
type CSSLookup ¶
type CSSLookup struct {
// ExactMap: 1:1 mapping - "btn" -> "Btn", "btn--brand" -> "BtnBrand"
ExactMap map[string]string
// AllConstants: All constants (unchanged)
AllConstants map[string]string // ConstName -> CSSClass
// ConstantParts: Track which classes a constant contains
// With 1:1 mapping, this is always single-element array
ConstantParts map[string][]string
// AllCSSClasses: All classes found in CSS (for static analysis)
// Used to detect invalid class references (typos)
AllCSSClasses map[string]bool
}
CSSLookup provides fast lookups for CSS class -> constant mapping
type CategorizedProperty ¶
type CategorizedProperty struct {
Name string
Value string
Category PropertyCategory
IsToken bool // True if value matches var(--ui-*)
}
CategorizedProperty represents a property with its category
type ClassAnalysis ¶
type ClassAnalysis struct {
ClassName string // "btn--ghost"
Match MatchType // MatchModifier
Suggestion string // "BtnGhost"
Context string // "included in ui.BtnGhost"
}
ClassAnalysis provides detailed breakdown of how a class was analyzed
type ClassReference ¶
type ClassReference struct {
ClassName string // Individual class: "btn--ghost" (DEPRECATED: use FullClassValue)
FullClassValue string // Full attribute: "btn btn--ghost btn--sm"
Location FileLocation // Where it was found
IsConstant bool // true if using ui.Foo, false if "foo"
ConstName string // "Foo" if IsConstant is true
LineContent string // The full line for context
}
ClassReference represents a CSS class reference found in code
type ClassificationResult ¶
type ClassificationResult int
ClassificationResult categorizes a class reference
const ( ClassMatched ClassificationResult = iota // Has a constant (migration opportunity) ClassBypassed // Valid CSS, no constant (allow) ClassZombie // Doesn't exist in CSS (error) )
type Config ¶
type Config struct {
SourceDir string // "web/ui/src/styles"
OutputDir string // "internal/web/ui" (output directory for generated files)
PackageName string // "ui"
Includes []string // ["layers/components/**/*.css", "layers/utilities.css"]
Verbose bool // Enable debug logging
LayerInferFromPath bool // Infer layer from file path (default: true)
Format string // Output format: "markdown", "compact" (default: "markdown")
PropertyLimit int // Max properties to show per category (default: 5)
ShowInternal bool // Show -webkit-* properties (default: false)
ExtractIntent bool // Parse @intent comments (default: true)
}
Config holds generator configuration
type ConstantSuggestion ¶
type ConstantSuggestion struct {
Constants []string // ["BtnGhost", "BtnSm"] (deduplicated)
Analysis []ClassAnalysis // Per-class breakdown (for verbose mode)
HasUnmatched bool // True if some classes had no match
UnmatchedClasses []string // List of classes that didn't match any constant
HasInvalid bool // Contains invalid (non-existent) classes
InvalidClasses []string // List of invalid classes
}
ConstantSuggestion contains the analysis and recommendation for a class string
func ResolveBestConstants ¶
func ResolveBestConstants(classString string, lookup *CSSLookup) ConstantSuggestion
ResolveBestConstants analyzes a full class string and returns the optimal constant combination.
Algorithm (Greedy Token Matching):
- Try exact match for entire string
- Split into tokens and match each individually (1:1 mapping)
- No deduplication needed with 1:1 mapping
Example:
Input: "btn btn--brand"
Output: ConstantSuggestion{
Type: SuggestionMultiClass,
Constants: ["Btn", "BtnBrand"], // Each token maps exactly
Analysis: [
{ClassName: "btn", Match: MatchExact, Suggestion: "Btn"},
{ClassName: "btn--brand", Match: MatchExact, Suggestion: "BtnBrand"},
],
}
type FileLocation ¶
type FileLocation struct {
File string
Line int
Column int // 1-based column (exact start of class name)
Text string // Full line content for source display
}
FileLocation tracks where a class reference was found
type GenerateResult ¶
type GenerateResult struct {
ClassesGenerated int
FilesScanned int
IntentsExtracted int // Number of @intent comments extracted
Warnings []string
Errors []error
}
GenerateResult contains generation stats
func Generate ¶
func Generate(config Config) (*GenerateResult, error)
Generate is the main entry point
type HardcodedString ¶
type HardcodedString struct {
FullClassValue string // "btn btn--ghost btn--sm"
Suggestion ConstantSuggestion // Smart suggestion with analysis
Location FileLocation
LineContent string // Full line for context
}
HardcodedString represents a CSS class string that could use a constant
type InvalidClass ¶
type InvalidClass struct {
ClassName string // "btn--outline"
Location FileLocation // Where it was found
LineContent string // Full line for context
}
InvalidClass represents a class that doesn't exist in CSS
type Issue ¶
type Issue struct {
FromLinter string `json:"FromLinter"` // "csslint"
Text string `json:"Text"` // "invalid CSS class \"btn--outline\" not found in stylesheet"
Severity string `json:"Severity"` // "", "warning", "error"
SourceLines []string `json:"SourceLines"` // Lines of code with issue
Pos IssuePos `json:"Pos"` // File location
LineRange *LineRange `json:"LineRange"` // Optional range
Replacement *Replacement `json:"Replacement"` // Optional fix suggestion
}
Issue represents a single linting violation in golangci-lint format
type IssuePos ¶
type IssuePos struct {
Filename string `json:"Filename"` // "internal/web/features/scheduleview/pages/scheduleview.templ"
Line int `json:"Line"` // 35
Column int `json:"Column"` // 15 (1-based, exact start of invalid class)
}
IssuePos specifies the exact location of an issue
type JSONIssue ¶
type JSONIssue struct {
File string `json:"file"`
Line int `json:"line"`
Column int `json:"column"`
Severity string `json:"severity"`
Message string `json:"message"`
Linter string `json:"linter"`
Source string `json:"source,omitempty"` // Optional source line
}
JSONIssue represents a single linting issue
type JSONOutput ¶
type JSONOutput struct {
Version string `json:"version"`
Timestamp string `json:"timestamp"`
Summary JSONSummary `json:"summary"`
Stats JSONStats `json:"stats"`
Issues []JSONIssue `json:"issues"`
QuickWins JSONQuickWins `json:"quick_wins"`
}
JSONOutput represents the structured JSON export schema
type JSONQuickWin ¶
type JSONQuickWin struct {
Class string `json:"class"`
Occurrences int `json:"occurrences"`
Suggestion string `json:"suggestion"`
}
JSONQuickWin represents a high-impact refactoring opportunity
type JSONQuickWins ¶
type JSONQuickWins struct {
SingleClass []JSONQuickWin `json:"single_class"`
MultiClass []JSONQuickWin `json:"multi_class"`
}
JSONQuickWins contains migration opportunities
type JSONStats ¶
type JSONStats struct {
TotalConstants int `json:"total_constants"`
ActuallyUsed int `json:"actually_used"`
MigrationOpportunities int `json:"migration_opportunities"`
CompletelyUnused int `json:"completely_unused"`
UsagePercentage float64 `json:"usage_percentage"`
HardcodedClasses int `json:"hardcoded_classes"`
ConstantReferences int `json:"constant_references"`
}
JSONStats contains adoption and usage statistics
type JSONSummary ¶
type JSONSummary struct {
TotalIssues int `json:"total_issues"`
Errors int `json:"errors"`
Warnings int `json:"warnings"`
FilesScanned int `json:"files_scanned"`
}
JSONSummary contains high-level issue counts
type Layer ¶
type Layer struct {
Name string
Classes []*CSSClass
Order int // For priority (base=0, components=1, utilities=2)
}
Layer represents a CSS cascade layer with priority
type LintConfig ¶
type LintConfig struct {
ScanPaths []string // Patterns to scan (e.g., "internal/web/features/**/*.templ")
GeneratedFile string // Path to styles.gen.go
PackageName string // "ui"
Verbose bool
Strict bool // Exit with code 1 if issues found
Threshold float64 // Minimum adoption percentage (for -strict mode)
// New golangci-style configuration
MaxIssuesPerLinter int // 0 = unlimited (default)
MaxSameIssues int // 0 = unlimited (default)
ShowStats bool // Show statistics summary (auto-enabled with Verbose)
PrintIssuedLines bool // Show source lines with issues (default: true)
PrintLinterName bool // Show (csslint) suffix (default: true)
UseColors bool // Enable color output (default: auto-detect)
}
LintConfig holds linting configuration
type LintResult ¶
type LintResult struct {
// Statistics
TotalConstants int // 229
ActuallyUsed int // Constants referenced via ui.ConstName (e.g., 0)
AvailableForMigration int // Constants that match hardcoded strings (e.g., 111)
CompletelyUnused int // No usage, no matches (e.g., 118)
UsagePercentage float64 // Percentage of actually used constants (e.g., 0%)
// Issues in golangci-lint format
Issues []Issue // All issues found
IssuesByCategory map[string][]Issue // Grouped by type for stats
// Legacy detailed findings (used for verbose mode only)
UnusedClasses []UnusedClass
HardcodedStrings []HardcodedString
InvalidClasses []InvalidClass // Classes that don't exist in CSS
FilesScanned int
ClassesFound int // Total hardcoded classes found
ConstantsFound int // Total ui.Foo references found
ErrorCount int // Count of invalid classes
TruncatedCount int // Issues removed due to limits
// Summary
Warnings []string
Suggestions []string
QuickWins QuickWinsSummary // Most frequently hardcoded classes
}
LintResult contains linting analysis results
func Lint ¶
func Lint(config LintConfig) (*LintResult, error)
Lint performs linting analysis on the codebase
type OutputFormat ¶
type OutputFormat string
OutputFormat represents the linter output format
const ( // OutputIssues shows only errors/warnings in golangci-lint format (CI-friendly) OutputIssues OutputFormat = "issues" // OutputSummary shows statistics and Quick Wins only (weekly reports) OutputSummary OutputFormat = "summary" // OutputFull shows issues + statistics + Quick Wins (interactive development) OutputFull OutputFormat = "full" // OutputJSON exports structured data in JSON format (tooling integration) OutputJSON OutputFormat = "json" // OutputMarkdown generates a Markdown report (shareable reports) OutputMarkdown OutputFormat = "markdown" )
func DetermineDefaultOutputFormat ¶
func DetermineDefaultOutputFormat() OutputFormat
DetermineDefaultOutputFormat returns the default output format Following golangci-lint's UX: issues only by default (clean, fast, consistent everywhere)
func DetermineOutputFormat ¶
func DetermineOutputFormat(formatFlag string, quiet bool) OutputFormat
DetermineOutputFormat selects the appropriate output format based on flags and environment
type PropertyCategory ¶
type PropertyCategory string
PropertyCategory groups related CSS properties
const ( CategoryVisual PropertyCategory = "Visual" CategoryLayout PropertyCategory = "Layout" CategoryTypography PropertyCategory = "Typography" CategoryEffects PropertyCategory = "Effects" CategoryTokens PropertyCategory = "Tokens" CategoryInternal PropertyCategory = "Internal" )
Property categories for organizing CSS properties
type PropertyDiff ¶
type PropertyDiff struct {
Added map[string]string // New properties in modifier
Changed map[string]string // Properties that override base
Unchanged []string // Properties inherited as-is
}
PropertyDiff tracks changes between modifier and base
func DiffProperties ¶
func DiffProperties(modifier, base *CSSClass) *PropertyDiff
DiffProperties compares modifier properties to base class
type PseudoStateProperties ¶
type PseudoStateProperties struct {
PseudoState string // ":hover", ":focus", etc.
Changes map[string]string // Properties that change in this state
}
PseudoStateProperties tracks property changes in pseudo-states
type QuickWin ¶
type QuickWin struct {
ClassName string // "btn"
Occurrences int // 45
Suggestion string // "ui.Btn"
}
QuickWin represents a high-impact refactoring opportunity
type QuickWinsSummary ¶
type QuickWinsSummary struct {
SingleClass []QuickWin // Single class: "btn" -> ui.Btn
MultiClass []QuickWin // Multiple classes: "btn btn--brand" -> { ui.Btn, ui.BtnBrand }
}
QuickWinsSummary categorizes quick wins by refactoring pattern
type Replacement ¶
type Replacement struct {
NewText string // "ui.Icon" or "btn--outlined"
InlineLength int // Length of text to replace
}
Replacement provides automated fix suggestion (future --fix flag)
type Reporter ¶
type Reporter struct {
// contains filtered or unexported fields
}
Reporter handles formatting and outputting linting results
func NewReporter ¶
func NewReporter(w io.Writer, config LintConfig) *Reporter
NewReporter creates a new reporter with the given configuration
func (*Reporter) PrintIssues ¶
PrintIssues outputs issues in golangci-lint format
func (*Reporter) PrintSummary ¶
func (r *Reporter) PrintSummary(result LintResult)
PrintSummary outputs the issue count summary
type ScanStats ¶
type ScanStats struct {
FilesDiscovered int // Total files found by glob patterns
FilesScanned int // Files actually scanned (after filtering)
FilesSkipped int // Files skipped due to filtering
}
ScanStats tracks file scanning statistics
type UnusedClass ¶
type UnusedClass struct {
ConstName string // "AppSidebar"
CSSClass string // "app-sidebar"
Layer string // "components"
DefinedIn string // "styles.gen.go:123"
}
UnusedClass represents a generated constant with no usage
type VerboseReporter ¶
type VerboseReporter struct {
// contains filtered or unexported fields
}
VerboseReporter handles detailed statistics and suggestions
func NewVerboseReporter ¶
func NewVerboseReporter(w io.Writer, useColors bool) *VerboseReporter
NewVerboseReporter creates a verbose reporter
func (*VerboseReporter) PrintAdoptionProgress ¶
func (r *VerboseReporter) PrintAdoptionProgress(result LintResult)
PrintAdoptionProgress shows visual progress bar
func (*VerboseReporter) PrintQuickWins ¶
func (r *VerboseReporter) PrintQuickWins(result LintResult)
PrintQuickWins shows migration opportunities
func (*VerboseReporter) PrintStatistics ¶
func (r *VerboseReporter) PrintStatistics(result LintResult)
PrintStatistics outputs detailed linting statistics
func (*VerboseReporter) PrintWarnings ¶
func (r *VerboseReporter) PrintWarnings(result LintResult)
PrintWarnings shows linter warnings