#!/usr/bin/env python3 """ HTML Design Token Validator Ensures all HTML assets (slides, infographics, etc.) use design tokens. Source of truth: assets/design-tokens.css Usage: python html-token-validator.py # Validate all HTML assets python html-token-validator.py --type slides # Validate only slides python html-token-validator.py --type infographics # Validate only infographics python html-token-validator.py path/to/file.html # Validate specific file python html-token-validator.py --fix # Auto-fix issues (WIP) """ import re import json import sys from pathlib import Path from typing import Dict, List, Tuple, Optional # Project root relative to this script PROJECT_ROOT = Path(__file__).parent.parent.parent.parent.parent TOKENS_JSON_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.json' TOKENS_CSS_PATH = PROJECT_ROOT / 'assets' / 'design-tokens.css' # Asset directories to validate ASSET_DIRS = { 'slides': PROJECT_ROOT / 'assets' / 'designs' / 'slides', 'infographics': PROJECT_ROOT / 'assets' / 'infographics', } # Patterns that indicate hardcoded values (should use tokens) FORBIDDEN_PATTERNS = [ (r'#[0-9A-Fa-f]{3,8}\b', 'hex color'), (r'rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)', 'rgb color'), (r'rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*[\d.]+\s*\)', 'rgba color'), (r'hsl\([^)]+\)', 'hsl color'), (r"font-family:\s*'[^v][^a][^r][^']*',", 'hardcoded font'), # Exclude var() (r'font-family:\s*"[^v][^a][^r][^"]*",', 'hardcoded font'), ] # Allowed rgba patterns (brand colors with transparency - CSS limitation) # These are derived from brand tokens but need rgba for transparency ALLOWED_RGBA_PATTERNS = [ r'rgba\(\s*59\s*,\s*130\s*,\s*246', # --color-primary (#3B82F6) r'rgba\(\s*245\s*,\s*158\s*,\s*11', # --color-secondary (#F59E0B) r'rgba\(\s*16\s*,\s*185\s*,\s*129', # --color-accent (#10B981) r'rgba\(\s*20\s*,\s*184\s*,\s*166', # --color-accent alt (#14B8A6) r'rgba\(\s*0\s*,\s*0\s*,\s*0', # black transparency (common) r'rgba\(\s*255\s*,\s*255\s*,\s*255', # white transparency (common) r'rgba\(\s*15\s*,\s*23\s*,\s*42', # --color-surface (#0F172A) r'rgba\(\s*7\s*,\s*11\s*,\s*20', # --color-background (#070B14) ] # Allowed exceptions (external images, etc.) ALLOWED_EXCEPTIONS = [ 'pexels.com', 'unsplash.com', 'youtube.com', 'ytimg.com', 'googlefonts', 'fonts.googleapis.com', 'fonts.gstatic.com', ] class ValidationResult: """Validation result for a single file.""" def __init__(self, file_path: Path): self.file_path = file_path self.errors: List[str] = [] self.warnings: List[str] = [] self.passed = True def add_error(self, msg: str): self.errors.append(msg) self.passed = False def add_warning(self, msg: str): self.warnings.append(msg) def load_css_variables() -> Dict[str, str]: """Load CSS variables from design-tokens.css.""" variables = {} if TOKENS_CSS_PATH.exists(): content = TOKENS_CSS_PATH.read_text() # Extract --var-name: value patterns for match in re.finditer(r'(--[\w-]+):\s*([^;]+);', content): variables[match.group(1)] = match.group(2).strip() return variables def is_inside_block(content: str, match_pos: int, open_tag: str, close_tag: str) -> bool: """Check if position is inside a specific HTML block.""" pre = content[:match_pos] tag_open = pre.rfind(open_tag) tag_close = pre.rfind(close_tag) return tag_open > tag_close def is_allowed_exception(context: str) -> bool: """Check if the hardcoded value is in an allowed exception context.""" context_lower = context.lower() return any(exc in context_lower for exc in ALLOWED_EXCEPTIONS) def is_allowed_rgba(match_text: str) -> bool: """Check if rgba pattern uses brand colors (allowed for transparency).""" return any(re.match(pattern, match_text) for pattern in ALLOWED_RGBA_PATTERNS) def get_context(content: str, pos: int, chars: int = 100) -> str: """Get surrounding context for a match position.""" start = max(0, pos - chars) end = min(len(content), pos + chars) return content[start:end] def validate_html(content: str, file_path: Path, verbose: bool = False) -> ValidationResult: """ Validate HTML content for design token compliance. Checks: 1. design-tokens.css import present 2. No hardcoded colors in CSS (except in '): if verbose: result.add_warning(f"Allowed in