#!/usr/bin/env python3 import re import math import os import sys def luminance(r, g, b): """ Calculate the relative luminance of an RGB color Formula from WCAG 2.0: https://www.w3.org/TR/WCAG20-TECHS/G17.html """ r = r / 255 g = g / 255 b = b / 255 r = r / 12.92 if r <= 0.03928 else ((r + 0.055) / 1.055) ** 2.4 g = g / 12.92 if g <= 0.03928 else ((g + 0.055) / 1.055) ** 2.4 b = b / 12.92 if b <= 0.03928 else ((b + 0.055) / 1.055) ** 2.4 return 0.2126 * r + 0.7152 * g + 0.0722 * b def contrast_ratio(lum1, lum2): """ Calculate contrast ratio between two luminance values Formula from WCAG 2.0: https://www.w3.org/TR/WCAG20-TECHS/G17.html """ lighter = max(lum1, lum2) darker = min(lum1, lum2) return (lighter + 0.05) / (darker + 0.05) def hex_to_rgb(hex_color): """Convert hex color to RGB tuple""" hex_color = hex_color.lstrip('#') if len(hex_color) == 3: hex_color = ''.join([c*2 for c in hex_color]) return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) def parse_css_colors(css_file): """Extract color definitions from CSS file""" with open(css_file, 'r') as f: css_content = f.read() # Find root variables root_vars = {} root_match = re.search(r':root\s*{([^}]+)}', css_content) if root_match: root_content = root_match.group(1) var_matches = re.findall(r'--([a-zA-Z0-9-]+):\s*([^;]+);', root_content) for name, value in var_matches: if '#' in value: root_vars[f'--{name}'] = value.strip() # Find color properties color_pairs = [] # Background-color and color pairs elements = re.findall(r'([.#]?[a-zA-Z0-9_-]+)\s*{([^}]+)}', css_content) for selector, properties in elements: bg_color = None text_color = None bg_match = re.search(r'background-color:\s*([^;]+);', properties) if bg_match: bg_color = bg_match.group(1).strip() color_match = re.search(r'(?= 4.5 else "FAIL" if 3.0 <= ratio < 4.5: status = "PASS (Large Text Only)" results.append({ "selector": selector, "background": bg_color, "text": text_color, "ratio": ratio, "status": status }) except Exception as e: print(f"Error processing {selector}: {e}") return results def main(): css_file = "docker/ploughshares/static/css/custom.css" if not os.path.exists(css_file): print(f"CSS file not found: {css_file}") return print(f"Analyzing contrast ratios in {css_file}...") color_pairs, root_vars = parse_css_colors(css_file) print("\nCSS Variables:") for name, value in root_vars.items(): print(f" {name}: {value}") print("\nContrast Ratio Analysis:") results = check_contrast_ratio(color_pairs) if not results: print("No color pairs found for contrast analysis.") return # Sort by ratio (ascending) results.sort(key=lambda x: x["ratio"]) print(f"\n{'Selector':<30} {'Background':<15} {'Text':<15} {'Ratio':<10} {'Status'}") print("-" * 80) for result in results: print(f"{result['selector']:<30} {result['background']:<15} {result['text']:<15} {result['ratio']:.2f} {result['status']}") # Summary fails = sum(1 for r in results if r['status'] == 'FAIL') large_only = sum(1 for r in results if r['status'] == 'PASS (Large Text Only)') passes = sum(1 for r in results if r['status'] == 'PASS') print("\nSummary:") print(f" Total color pairs analyzed: {len(results)}") print(f" Passed: {passes}") print(f" Passed (Large Text Only): {large_only}") print(f" Failed: {fails}") print("\nRecommendations:") if fails > 0: print(" - Increase contrast for failing elements to at least 4.5:1") if large_only > 0: print(" - Ensure text with lower contrast (3:1-4.5:1) is used only for large text (18pt+)") print(" - Use WebAIM Contrast Checker for verification: https://webaim.org/resources/contrastchecker/") print(" - Test with real users, including those with visual impairments") if __name__ == "__main__": main()