Loading source
Pulling the file list, source metadata, and syntax-aware rendering for this listing.
Source from repo
AI-powered design system generator that produces complete, tailored design systems from project requirements.
Files
Skill
Size
Entrypoint
Format
Open file
Syntax-highlighted preview of this file as included in the skill package.
scripts/html-token-validator.py
1#!/usr/bin/env python32"""3HTML Design Token Validator4Ensures all HTML assets (slides, infographics, etc.) use design tokens.5Source of truth: assets/design-tokens.css67Usage:8python html-token-validator.py # Validate all HTML assets9python html-token-validator.py --type slides # Validate only slides10python html-token-validator.py --type infographics # Validate only infographics11python html-token-validator.py path/to/file.html # Validate specific file12python html-token-validator.py --fix # Auto-fix issues (WIP)13"""1415import re16import json17import sys18from pathlib import Path19from typing import Dict, List, Tuple, Optional2021# Project root relative to this script22PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent23TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json'24TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css'2526# Asset directories to validate27ASSET_DIRS = {28'slides': PROJECT_ROOT / 'assets' / 'designs' / 'slides',29'infographics': PROJECT_ROOT / 'assets' / 'infographics',30}3132# Patterns that indicate hardcoded values (should use tokens)33FORBIDDEN_PATTERNS = [34(r'#[0-9A-Fa-f]{3,8}\b', 'hex color'),35(r'rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)', 'rgb color'),36(r'rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)', 'rgba color'),37(r'hsl\([^)]+\)', 'hsl color'),38(r"font-family:\s*'[^v][^a][^r][^']*',", 'hardcoded font'), # Exclude var()39(r'font-family:\s*"[^v][^a][^r][^"]*",', 'hardcoded font'),40]4142# Allowed rgba patterns (brand colors with transparency - CSS limitation)43# These are derived from brand tokens but need rgba for transparency44ALLOWED_RGBA_PATTERNS = [45r'rgba\(\s*59\s*,\s*130\s*,\s*246', # --color-primary (#3B82F6)46r'rgba\(\s*245\s*,\s*158\s*,\s*11', # --color-secondary (#F59E0B)47r'rgba\(\s*16\s*,\s*185\s*,\s*129', # --color-accent (#10B981)48r'rgba\(\s*20\s*,\s*184\s*,\s*166', # --color-accent alt (#14B8A6)49r'rgba\(\s*0\s*,\s*0\s*,\s*0', # black transparency (common)50r'rgba\(\s*255\s*,\s*255\s*,\s*255', # white transparency (common)51r'rgba\(\s*15\s*,\s*23\s*,\s*42', # --color-surface (#0F172A)52r'rgba\(\s*7\s*,\s*11\s*,\s*20', # --color-background (#070B14)53]5455# Allowed exceptions (external images, etc.)56ALLOWED_EXCEPTIONS = [57'pexels.com', 'unsplash.com', 'youtube.com', 'ytimg.com',58'googlefonts', 'fonts.googleapis.com', 'fonts.gstatic.com',59]606162class ValidationResult:63"""Validation result for a single file."""64def __init__(self, file_path: Path):65self.file_path = file_path66self.errors: List[str] = []67self.warnings: List[str] = []68self.passed = True6970def add_error(self, msg: str):71self.errors.append(msg)72self.passed = False7374def add_warning(self, msg: str):75self.warnings.append(msg)767778def load_css_variables() -> Dict[str, str]:79"""Load CSS variables from design-tokens.css."""80variables = {}81if TOKENS_CSS_PATH.exists():82content = TOKENS_CSS_PATH.read_text()83# Extract --var-name: value patterns84for match in re.finditer(r'(--[\w-]+):\s*([^;]+);', content):85variables[match.group(1)] = match.group(2).strip()86return variables878889def is_inside_block(content: str, match_pos: int, open_tag: str, close_tag: str) -> bool:90"""Check if position is inside a specific HTML block."""91pre = content[:match_pos]92tag_open = pre.rfind(open_tag)93tag_close = pre.rfind(close_tag)94return tag_open > tag_close959697def is_allowed_exception(context: str) -> bool:98"""Check if the hardcoded value is in an allowed exception context."""99context_lower = context.lower()100return any(exc in context_lower for exc in ALLOWED_EXCEPTIONS)101102103def is_allowed_rgba(match_text: str) -> bool:104"""Check if rgba pattern uses brand colors (allowed for transparency)."""105return any(re.match(pattern, match_text) for pattern in ALLOWED_RGBA_PATTERNS)106107108def get_context(content: str, pos: int, chars: int = 100) -> str:109"""Get surrounding context for a match position."""110start = max(0, pos - chars)111end = min(len(content), pos + chars)112return content[start:end]113114115def validate_html(content: str, file_path: Path, verbose: bool = False) -> ValidationResult:116"""117Validate HTML content for design token compliance.118119Checks:1201. design-tokens.css import present1212. No hardcoded colors in CSS (except in <script> for Chart.js)1223. No hardcoded fonts1234. Uses var(--token-name) pattern124"""125result = ValidationResult(file_path)126127# 1. Check for design-tokens.css import128if 'design-tokens.css' not in content:129result.add_error("Missing design-tokens.css import")130131# 2. Check for forbidden patterns in CSS132for pattern, description in FORBIDDEN_PATTERNS:133for match in re.finditer(pattern, content):134match_text = match.group()135match_pos = match.start()136context = get_context(content, match_pos)137138# Skip if in <script> block (Chart.js allowed)139if is_inside_block(content, match_pos, '<script', '</script>'):140if verbose:141result.add_warning(f"Allowed in <script>: {match_text}")142continue143144# Skip if in allowed exception context (external URLs)145if is_allowed_exception(context):146if verbose:147result.add_warning(f"Allowed external: {match_text}")148continue149150# Skip rgba using brand colors (needed for transparency effects)151if description == 'rgba color' and is_allowed_rgba(match_text):152if verbose:153result.add_warning(f"Allowed brand rgba: {match_text}")154continue155156# Skip if part of var() reference (false positive)157if 'var(' in context and match_text in context:158# Check if it's a fallback value in var()159var_pattern = rf'var\([^)]*{re.escape(match_text)}[^)]*\)'160if re.search(var_pattern, context):161continue162163# Error if in <style> or inline style164if is_inside_block(content, match_pos, '<style', '</style>'):165result.add_error(f"Hardcoded {description} in <style>: {match_text}")166elif 'style="' in context:167result.add_error(f"Hardcoded {description} in inline style: {match_text}")168169# 3. Check for required var() usage indicators170token_patterns = [171r'var\(--color-',172r'var\(--primitive-',173r'var\(--typography-',174r'var\(--card-',175r'var\(--button-',176]177token_count = sum(len(re.findall(p, content)) for p in token_patterns)178179if token_count < 5:180result.add_warning(f"Low token usage ({token_count} var() references). Consider using more design tokens.")181182return result183184185def validate_file(file_path: Path, verbose: bool = False) -> ValidationResult:186"""Validate a single HTML file."""187if not file_path.exists():188result = ValidationResult(file_path)189result.add_error("File not found")190return result191192content = file_path.read_text()193return validate_html(content, file_path, verbose)194195196def validate_directory(dir_path: Path, verbose: bool = False) -> List[ValidationResult]:197"""Validate all HTML files in a directory."""198results = []199if dir_path.exists():200for html_file in sorted(dir_path.glob('*.html')):201results.append(validate_file(html_file, verbose))202return results203204205def print_result(result: ValidationResult, verbose: bool = False):206"""Print validation result for a file."""207status = "✓" if result.passed else "✗"208print(f" {status} {result.file_path.name}")209210if result.errors:211for error in result.errors[:5]: # Limit output212print(f" ├─ {error}")213if len(result.errors) > 5:214print(f" └─ ... and {len(result.errors) - 5} more errors")215216if verbose and result.warnings:217for warning in result.warnings[:3]:218print(f" [warn] {warning}")219220221def print_summary(all_results: Dict[str, List[ValidationResult]]):222"""Print summary of all validation results."""223total_files = 0224total_passed = 0225total_errors = 0226227print("\n" + "=" * 60)228print("HTML DESIGN TOKEN VALIDATION SUMMARY")229print("=" * 60)230231for asset_type, results in all_results.items():232if not results:233continue234235passed = sum(1 for r in results if r.passed)236failed = len(results) - passed237errors = sum(len(r.errors) for r in results)238239total_files += len(results)240total_passed += passed241total_errors += errors242243status = "✓" if failed == 0 else "✗"244print(f"\n{status} {asset_type.upper()}: {passed}/{len(results)} passed")245246for result in results:247if not result.passed:248print_result(result)249250print("\n" + "-" * 60)251if total_errors == 0:252print(f"✓ ALL PASSED: {total_passed}/{total_files} files valid")253else:254print(f"✗ FAILED: {total_files - total_passed}/{total_files} files have issues ({total_errors} total errors)")255print("-" * 60)256257return total_errors == 0258259260def main():261"""CLI entry point."""262import argparse263264parser = argparse.ArgumentParser(265description='Validate HTML assets for design token compliance',266formatter_class=argparse.RawDescriptionHelpFormatter,267epilog="""268Examples:269%(prog)s # Validate all HTML assets270%(prog)s --type slides # Validate only slides271%(prog)s --type infographics # Validate only infographics272%(prog)s path/to/file.html # Validate specific file273%(prog)s --colors # Show brand colors from tokens274"""275)276parser.add_argument('files', nargs='*', help='Specific HTML files to validate')277parser.add_argument('-t', '--type', choices=['slides', 'infographics', 'all'],278default='all', help='Asset type to validate')279parser.add_argument('-v', '--verbose', action='store_true', help='Show warnings')280parser.add_argument('--colors', action='store_true', help='Print CSS variables from tokens')281parser.add_argument('--fix', action='store_true', help='Auto-fix issues (experimental)')282283args = parser.parse_args()284285# Show colors mode286if args.colors:287variables = load_css_variables()288print("\nDesign Tokens (from design-tokens.css):")289print("-" * 40)290for name, value in sorted(variables.items())[:30]:291print(f" {name}: {value}")292if len(variables) > 30:293print(f" ... and {len(variables) - 30} more")294return295296all_results: Dict[str, List[ValidationResult]] = {}297298# Validate specific files299if args.files:300results = []301for file_path in args.files:302path = Path(file_path)303if path.exists():304results.append(validate_file(path, args.verbose))305else:306result = ValidationResult(path)307result.add_error("File not found")308results.append(result)309all_results['specified'] = results310else:311# Validate by type312types_to_check = ASSET_DIRS.keys() if args.type == 'all' else [args.type]313314for asset_type in types_to_check:315if asset_type in ASSET_DIRS:316results = validate_directory(ASSET_DIRS[asset_type], args.verbose)317all_results[asset_type] = results318319# Print results320success = print_summary(all_results)321322if not success:323sys.exit(1)324325326if __name__ == '__main__':327main()328