171 lines
5.8 KiB
Python
Executable File
171 lines
5.8 KiB
Python
Executable File
#!/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'(?<!background-)color:\s*([^;]+);', properties)
|
|
if color_match:
|
|
text_color = color_match.group(1).strip()
|
|
|
|
if bg_color and text_color:
|
|
# Resolve variables
|
|
if bg_color.startswith('var('):
|
|
var_match = re.search(r'var\(([^)]+)\)', bg_color)
|
|
if var_match:
|
|
var_name = var_match.group(1)
|
|
if var_name in root_vars:
|
|
bg_color = root_vars[var_name]
|
|
|
|
if text_color.startswith('var('):
|
|
var_match = re.search(r'var\(([^)]+)\)', text_color)
|
|
if var_match:
|
|
var_name = var_match.group(1)
|
|
if var_name in root_vars:
|
|
text_color = root_vars[var_name]
|
|
|
|
# Only add hex colors for now
|
|
if bg_color.startswith('#') and text_color.startswith('#'):
|
|
color_pairs.append((selector, bg_color, text_color))
|
|
|
|
return color_pairs, root_vars
|
|
|
|
def check_contrast_ratio(color_pairs):
|
|
"""Check contrast ratios for color pairs"""
|
|
results = []
|
|
for selector, bg_color, text_color in color_pairs:
|
|
try:
|
|
bg_rgb = hex_to_rgb(bg_color)
|
|
text_rgb = hex_to_rgb(text_color)
|
|
|
|
bg_luminance = luminance(*bg_rgb)
|
|
text_luminance = luminance(*text_rgb)
|
|
|
|
ratio = contrast_ratio(bg_luminance, text_luminance)
|
|
|
|
status = "PASS" if ratio >= 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() |