/** * TokenValidator - Validates CSS and HTML for design token compliance * * Ensures all color, spacing, typography, and other design values use * valid CSS custom properties instead of hardcoded values. * * Usage: * const validator = new TokenValidator(); * const report = validator.validateCSS(cssText); * console.log(report.violations); // Array of violations found */ class TokenValidator { constructor() { // Define all valid tokens from the design system this.validTokens = { colors: [ // Semantic colors '--primary', '--primary-foreground', '--secondary', '--secondary-foreground', '--accent', '--accent-foreground', '--destructive', '--destructive-foreground', '--success', '--success-foreground', '--warning', '--warning-foreground', '--info', '--info-foreground', // Functional colors '--background', '--foreground', '--card', '--card-foreground', '--popover', '--popover-foreground', '--muted', '--muted-foreground', '--border', '--ring', '--input' ], spacing: [ '--space-0', '--space-1', '--space-2', '--space-3', '--space-4', '--space-5', '--space-6', '--space-8', '--space-10', '--space-12', '--space-16', '--space-20', '--space-24', '--space-32' ], typography: [ // Font families '--font-sans', '--font-mono', // Font sizes '--text-xs', '--text-sm', '--text-base', '--text-lg', '--text-xl', '--text-2xl', '--text-3xl', '--text-4xl', // Font weights '--font-normal', '--font-medium', '--font-semibold', '--font-bold', // Line heights '--leading-tight', '--leading-normal', '--leading-relaxed', // Letter spacing '--tracking-tight', '--tracking-normal', '--tracking-wide' ], radius: [ '--radius-none', '--radius-sm', '--radius', '--radius-md', '--radius-lg', '--radius-xl', '--radius-full' ], shadows: [ '--shadow-sm', '--shadow', '--shadow-md', '--shadow-lg', '--shadow-xl' ], timing: [ '--duration-fast', '--duration-normal', '--duration-slow', '--ease-default', '--ease-in', '--ease-out' ], zIndex: [ '--z-base', '--z-dropdown', '--z-sticky', '--z-fixed', '--z-modal-background', '--z-modal', '--z-popover', '--z-tooltip', '--z-notification', '--z-toast' ] }; // Flatten all valid tokens for quick lookup this.allValidTokens = new Set(); Object.values(this.validTokens).forEach(group => { group.forEach(token => this.allValidTokens.add(token)); }); // Patterns for detecting CSS variable usage this.varPattern = /var\(\s*([a-z0-9\-_]+)/gi; // Patterns for detecting hardcoded values this.colorPattern = /#[0-9a-f]{3,6}|rgb\(|hsl\(|oklch\(/gi; this.spacingPattern = /\b\d+(?:px|rem|em|ch|vw|vh)\b/g; // Track violations this.violations = []; this.warnings = []; this.stats = { totalTokenReferences: 0, validTokenReferences: 0, invalidTokenReferences: 0, hardcodedValues: 0, files: {} }; } /** * Validate CSS text for token compliance * @param {string} cssText - CSS content to validate * @param {string} [fileName] - Optional filename for reporting * @returns {object} Report with violations, warnings, and stats */ validateCSS(cssText, fileName = 'inline') { this.violations = []; this.warnings = []; if (!cssText || typeof cssText !== 'string') { return { violations: [], warnings: [], stats: this.stats }; } // Parse CSS and check for token compliance this._validateTokenReferences(cssText, fileName); this._validateHardcodedColors(cssText, fileName); this._validateSpacingValues(cssText, fileName); // Store stats for this file this.stats.files[fileName] = { violations: this.violations.length, warnings: this.warnings.length }; return { violations: this.violations, warnings: this.warnings, stats: this.stats, isCompliant: this.violations.length === 0 }; } /** * Validate all CSS variable references * @private */ _validateTokenReferences(cssText, fileName) { let match; const varPattern = /var\(\s*([a-z0-9\-_]+)/gi; while ((match = varPattern.exec(cssText)) !== null) { const tokenName = match[1]; this.stats.totalTokenReferences++; if (this.allValidTokens.has(tokenName)) { this.stats.validTokenReferences++; } else { this.stats.invalidTokenReferences++; this.violations.push({ type: 'INVALID_TOKEN', token: tokenName, message: `Invalid token: '${tokenName}'`, file: fileName, suggestion: this._suggestToken(tokenName), code: match[0] }); } } } /** * Detect hardcoded colors (anti-pattern) * @private */ _validateHardcodedColors(cssText, fileName) { // Skip comments const cleanCSS = cssText.replace(/\/\*[\s\S]*?\*\//g, ''); // Find hardcoded hex colors const hexPattern = /#[0-9a-f]{3,6}(?![a-z0-9\-])/gi; let match; while ((match = hexPattern.exec(cleanCSS)) !== null) { // Check if this is a valid CSS variable fallback (e.g., var(--primary, #fff)) const context = cleanCSS.substring(Math.max(0, match.index - 20), match.index + 10); if (!context.includes('var(')) { this.stats.hardcodedValues++; this.violations.push({ type: 'HARDCODED_COLOR', value: match[0], message: `Hardcoded color detected: '${match[0]}' - Use CSS tokens instead`, file: fileName, suggestion: `Use var(--primary) or similar color token`, code: match[0], severity: 'HIGH' }); } } // Find hardcoded rgb/hsl colors (but not as fallback) const rgbPattern = /rgb\([^)]+\)|hsl\([^)]+\)|oklch\([^)]+\)(?![a-z])/gi; while ((match = rgbPattern.exec(cleanCSS)) !== null) { const context = cleanCSS.substring(Math.max(0, match.index - 20), match.index + 10); if (!context.includes('var(')) { this.stats.hardcodedValues++; this.violations.push({ type: 'HARDCODED_COLOR_FUNCTION', value: match[0], message: `Hardcoded color function detected - Use CSS tokens instead`, file: fileName, suggestion: 'Use var(--color-name) instead of hardcoded rgb/hsl/oklch', code: match[0], severity: 'HIGH' }); } } } /** * Detect hardcoded spacing values * @private */ _validateSpacingValues(cssText, fileName) { // Look for padding/margin with px values that could be tokens const spacingProps = ['padding', 'margin', 'gap', 'width', 'height']; const spacingPattern = /(?:padding|margin|gap|width|height):\s*(\d+)px\b/gi; let match; while ((match = spacingPattern.exec(cssText)) !== null) { const value = parseInt(match[1]); // Only warn if it's a multiple of 4 (our spacing base unit) if (value % 4 === 0 && value <= 128) { this.warnings.push({ type: 'HARDCODED_SPACING', value: match[0], message: `Hardcoded spacing value: '${match[1]}px' - Could use --space-* token`, file: fileName, suggestion: `Use var(--space-${Math.log2(value / 4)}) or appropriate token`, code: match[0], severity: 'MEDIUM' }); } } } /** * Suggest the closest valid token based on fuzzy matching * @private */ _suggestToken(invalidToken) { const lower = invalidToken.toLowerCase(); let closest = null; let closestDistance = Infinity; this.allValidTokens.forEach(validToken => { const distance = this._levenshteinDistance(lower, validToken.toLowerCase()); if (distance < closestDistance && distance < 5) { closestDistance = distance; closest = validToken; } }); return closest ? `Did you mean '${closest}'?` : 'Check valid tokens in tokens.css'; } /** * Calculate Levenshtein distance for fuzzy matching * @private */ _levenshteinDistance(str1, str2) { const matrix = []; for (let i = 0; i <= str2.length; i++) { matrix[i] = [i]; } for (let j = 0; j <= str1.length; j++) { matrix[0][j] = j; } for (let i = 1; i <= str2.length; i++) { for (let j = 1; j <= str1.length; j++) { if (str2.charAt(i - 1) === str1.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1 ); } } } return matrix[str2.length][str1.length]; } /** * Validate HTML for inline style violations * @param {HTMLElement} element - Element to validate * @param {string} [fileName] - Optional filename for reporting * @returns {object} Report with violations */ validateHTML(element, fileName = 'html') { this.violations = []; this.warnings = []; if (!element) return { violations: [], warnings: [] }; // Check style attributes const allElements = element.querySelectorAll('[style]'); allElements.forEach(el => { const styleAttr = el.getAttribute('style'); if (styleAttr) { this.validateCSS(styleAttr, `${fileName}:${el.tagName}`); } }); // Check shadow DOM styles const walkElements = (node) => { if (node.shadowRoot) { const styleElements = node.shadowRoot.querySelectorAll('style'); styleElements.forEach(style => { this.validateCSS(style.textContent, `${fileName}:${node.tagName}(shadow)`); }); } for (let child of node.children) { walkElements(child); } }; walkElements(element); return { violations: this.violations, warnings: this.warnings, stats: this.stats }; } /** * Generate a compliance report * @param {boolean} [includeStats=true] - Include detailed statistics * @returns {string} Formatted report */ generateReport(includeStats = true) { let report = ` ╔══════════════════════════════════════════════════════════════╗ ║ Design Token Compliance Report ║ ╚══════════════════════════════════════════════════════════════╝ `; if (this.violations.length === 0) { report += `✅ COMPLIANT - No token violations found\n`; } else { report += `❌ ${this.violations.length} VIOLATIONS FOUND:\n\n`; this.violations.forEach((v, i) => { report += `${i + 1}. ${v.type}: ${v.message}\n`; report += ` File: ${v.file}\n`; report += ` Code: ${v.code}\n`; if (v.suggestion) report += ` Suggestion: ${v.suggestion}\n`; report += `\n`; }); } if (this.warnings.length > 0) { report += `⚠️ ${this.warnings.length} WARNINGS:\n\n`; this.warnings.forEach((w, i) => { report += `${i + 1}. ${w.type}: ${w.message}\n`; if (w.suggestion) report += ` Suggestion: ${w.suggestion}\n`; report += `\n`; }); } if (includeStats && this.stats.totalTokenReferences > 0) { report += `\n📊 STATISTICS:\n`; report += ` Total Token References: ${this.stats.totalTokenReferences}\n`; report += ` Valid References: ${this.stats.validTokenReferences}\n`; report += ` Invalid References: ${this.stats.invalidTokenReferences}\n`; report += ` Hardcoded Values: ${this.stats.hardcodedValues}\n`; report += ` Compliance Rate: ${((this.stats.validTokenReferences / this.stats.totalTokenReferences) * 100).toFixed(1)}%\n`; } return report; } /** * Get list of all valid tokens * @returns {object} Tokens organized by category */ getValidTokens() { return this.validTokens; } /** * Export validator instance for global access */ static getInstance() { if (!window.__dssTokenValidator) { window.__dssTokenValidator = new TokenValidator(); } return window.__dssTokenValidator; } } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = TokenValidator; } // Make available globally for console debugging if (typeof window !== 'undefined') { window.TokenValidator = TokenValidator; window.__dssTokenValidator = new TokenValidator(); // Add to debug interface if (window.__DSS_DEBUG) { window.__DSS_DEBUG.validateTokens = () => { const report = TokenValidator.getInstance().generateReport(); console.log(report); return TokenValidator.getInstance(); }; } }