AGENTS.md - RefreshCSS Library Analysis
Documents a Python library that removes unused CSS selectors by comparing them against HTML templates.
What this file does
Documents a Python library that removes unused CSS selectors by comparing them against HTML templates.
When to use it
- Adding CSS tree-shaking to a Django or Jinja project
- Integrating CSS purging into a Python build pipeline
- Replacing a Node.js CSS optimizer in a Python-only stack
- Understanding how to parse and filter CSS with tinycss2
Assumes this stack
AGENTS.md - RefreshCSS Library Analysis
Overview
RefreshCSS is a pure Python library that removes unused CSS selectors (classes, ids, and elements) from CSS files based on what's actually used in HTML templates. It's designed to work seamlessly with Django/Jinja-style templates and can be integrated into build processes.
Version: 0.5.1
License: MIT
Python: 3.10+
Core Purpose
RefreshCSS performs CSS "tree-shaking" or "purging" - it analyzes HTML templates to find which CSS selectors are actually being used, then outputs a cleaned CSS file containing only those selectors. This reduces CSS file size and improves performance.
Architecture
High-Level Flow
- Parse HTML → Extract all classes, ids, and elements from HTML templates.
- Uses JustHTML for static HTML structure.
- Uses Aggressive Regex Extraction for classes and IDs inside Django/Jinja template tags (limited to
classandidattributes).
- Parse CSS → Extract all rules and selectors from CSS files using tinycss2.
- Compare → Match CSS selectors against HTML usage using efficient set operations.
- Output → Return cleaned CSS containing only used selectors, preserving comments and at-rules.
Core Components
1. HTML Parsing (src/refreshcss/html/)
File class (html/file.py):
- Parses individual HTML files using a combination of JustHTML and targeted regex.
- Extracts three types of selectors:
- Classes:
- Static: Extracted via
JustHTMLfrom the DOM (template tags are replaced with spaces to avoid "smashing" adjacent classes). - Aggressive: Extracted from Django/Jinja template tags (e.g.,
{{ ... }},{% ... %}) found strictly withinclass="..."HTML attributes.
- Static: Extracted via
- IDs:
- Static: Extracted via
JustHTML. - Aggressive: Extracted from template tags found strictly within
id="..."HTML attributes.
- Static: Extracted via
- Elements: Extracted via
JustHTMLtag name traversal.
- Classes:
- Uses cached properties for performance.
Key Regex Patterns:
CLASS_ATTR_RE: Matchesclass="..."attributes, handling nested template tags and quotes.ID_ATTR_RE: Matchesid="..."attributes.TEMPLATE_TAG_RE: Matches{% ... %}and{{ ... }}.CLASS_IN_TEMPLATE_RE: Matches quoted strings within template tags that look like CSS classes/IDs.
Site classes (html/site.py):
Site: Abstract base class that aggregates classes, ids, and elements from multiple filesDjangoSite: Automatically discovers Django template directories and uses Django's default charsetPathSite: Accepts explicit file paths, supports recursive directory scanningUrlSite: Placeholder for future URL-based crawling (not yet implemented)
2. CSS Parsing (src/refreshcss/css/)
parser.py (css/parser.py):
- Uses tinycss2 to parse CSS into an AST-like stream of tokens and rules.
- Key algorithm:
tinycss2.parse_stylesheetconverts CSS to rules while preserving comments.- For each QualifiedRule (regular CSS rule):
_rule_is_keptperforms a single-pass scan over the selector prelude.- Short-circuits as soon as any selector in a comma-separated list is deemed "used".
- Validates against
site.classes,site.ids, andsite.elements.
- For each AtRule (e.g.,
@media,@container,@supports):- Recursively filters nested rules.
- Removes at-rules that become empty after filtering.
- Preserves non-nested at-rules like
@font-faceor@keyframes.
- Serializes the kept rules back to CSS text.
Special Cases Handled:
- Modern CSS: Full support for nesting, container queries, and complex selectors via
tinycss2. - Comments: Preserved by default as they are included in the
tinycss2rule list. - Universal selector (
*): Always preserved. - Pseudo-elements/classes: Correctly ignored during base-selector matching.
3. Main Interface (refreshcss/main.py)
RefreshCSS class:
- Tie common logic together.
- The
clean()method accepts CSS text and returns cleaned CSS.
4. Utilities (refreshcss/utils/)
path.py:
read_text(): Reads files with optional encoding detection usingcharset-normalizer.
string.py:
finditer(): Yields start/end indices for substring matchesremove_string_at(): Removes substring by index range
Integration Points
1. Command-Line Interface (src/refreshcss/cli.py)
Uses Click framework to provide a CLI:
refreshcss [OPTIONS] CSS HTML...
Options:
-o, --output FILENAME # Write to file instead of stdout
-R, -r, --recursive # Recursively search subdirectories
--encoding TEXT # Character encoding
Example:
refreshcss styles.css templates/ -r -o cleaned.css
2. Django Compressor Filter (src/refreshcss/filters.py)
RefreshCSSFilter:
- Integrates with
django-compressoras a filter. - Caches the parsed
DjangoSiteobject for performance. - Automatically runs during
python manage.py compress.
Usage in Django settings:
COMPRESS_FILTERS = {
"css": [
"refreshcss.filters.RefreshCSSFilter",
],
}
Key Design Decisions
1. Spec-Compliant Parsing
- Why: Moved from regex to tinycss2 (CSS) and JustHTML (HTML) to ensure correctness with modern standards.
- Trade-off: Slightly higher execution cost for small files, but significantly faster for real-world CSS frameworks (Bulma is ~7x faster with
tinycss2).
2. Context-Aware Aggressive Extraction
- Why: Extracts class names and IDs from template logic (e.g.,
{% if ... %}) but only within relevant HTML attributes. - Benefit: Prevents false positives from filter arguments or other template code outside of
classandidtags.
3. Build-System: UV
- Why: Utilizes
uvanduv_buildfor extremely fast dependency management and building. - Layout: Adopted the standard
srclayout for better package isolation and testing correctness.
4. Caching Strategy
Fileclass uses@cached_propertyfor expensive parsing operations.- Django filter caches the entire
DjangoSiteobject to avoid re-parsing templates.
Limitations & Known Issues
- Not suitable for SPAs: Only inspects static HTML templates, not dynamically generated classes
- No URL crawling: Cannot analyze live websites (yet -
UrlSiteis a placeholder) - Regex limitations: May struggle with extremely complex or unusual CSS syntax
- No JavaScript analysis: Won't detect classes added/removed via JavaScript
Testing Strategy
The library has comprehensive tests covering:
- HTML parsing (classes, ids, elements)
- CSS parsing (rules, selectors, at-rules)
- Integration tests with real CSS frameworks (Bulma 0.9.3)
- Edge cases (element/class name collisions, media queries)
- Benchmark tests for performance
Test files are organized by component:
tests/html/- HTML parsing teststests/css/- CSS parsing teststests/main/- Integration teststests/benchmarks/- Performance tests
Performance Considerations
- Cached properties: Expensive operations (regex matching) are cached
- Set operations: Fast membership testing using Python sets
- Django filter caching: Entire site is cached to avoid re-parsing on every CSS file
- Regex compilation: Patterns are compiled once at module level
Dependencies
Core:
tinycss2- Spec-compliant CSS parsingjusthtml- Spec-compliant HTML parsing
Optional:
click- CLI interface (extras:cli)charset-normalizer- Encoding detection (extras:cli)
Dev:
pytest,pytest-django,pytest-cov,pytest-benchmarkdjango,django-compressorruff- Linting and formattinguv_build- Build backend
Use Cases
- Django projects: Reduce CSS bundle size in production
- Static site generators: Clean up CSS for static HTML sites
- CI/CD pipelines: Automated CSS optimization
- Template-based frameworks: Any Python web framework using templates
Example Workflow
from pathlib import Path
from refreshcss import RefreshCSS, PathSite
# 1. Define HTML template locations
site = PathSite(paths=["templates", "components"], recursive=True)
# 2. Parse all HTML files to extract selectors
site.parse()
# site.classes = {'container', 'header', 'button', ...}
# site.ids = {'main', 'nav', ...}
# site.elements = {'div', 'p', 'a', ...}
# 3. Read CSS file
css_text = Path("styles.css").read_text()
# 4. Clean CSS
refresher = RefreshCSS(site)
cleaned_css = refresher.clean(css_text)
# 5. Write output
Path("styles.min.css").write_text(cleaned_css)
Future Enhancements (Based on TODO and Code)
- URL crawling: Implement
UrlSiteto analyze live websites - Better CSS parsing: Create a
Stylesheetclass to encapsulate parsing logic - Improved media query handling: More sophisticated nested rule processing
- JavaScript analysis: Detect dynamically added classes
- Source maps: Track which HTML files use which CSS rules
Related Projects
Python:
treeshake- Similar concept but had compatibility issuescssutils- Struggles with modern CSS syntaxcss-optimizer- Alternative approach
JavaScript/Node:
PurgeCSS- Popular Node.js solutionuncss- Another Node.js alternative
RefreshCSS fills the gap for Python-based projects that want CSS optimization without adding Node.js to their build process.
What's inside
6 architecture sections, 4 component descriptions, 3 integration points, 2 code examples, 1 CLI reference
Change this for your project
- Replace
adamghill/refreshcsswith your own repository name - Replace
refreshcsspackage name in imports and CLI commands with your own - Replace
src/refreshcss/paths with your own package layout
Where it goes
Save as AGENTS.md in your repository root. Read by Codex, Cursor and other agents that follow the AGENTS.md convention.
Worth borrowing
- Use cached properties for expensive parsing operations
- Separate HTML and CSS parsing into distinct modules for testability
- Provide multiple site discovery strategies (Django, path, URL placeholder)
Related Documents
Browser-only development
Guides AI assistants on an Electron + React + TypeScript desktop app for browsing and organizing AI-generated images locally.
Claude Agents — Reference & Recommendations
Catalogues 40+ Claude agents and marketing skills for building a cat adoption charity landing page, with a ready-to-paste prompt and backend API reference.
Golden DKG Prototype -- Master Plan
Defines an 8-phase implementation plan for a Rust prototype of the Golden non-interactive DKG protocol using BLS12-381 and tokio.
Swarms Examples Index
Lists 60+ example scripts for building single and multi-agent systems with the Swarms framework, organized by category and use case.