enhance contrast issue detection
This commit is contained in:
243
daemon/daemon.go
243
daemon/daemon.go
@@ -8997,19 +8997,21 @@ type ContrastCheckResult struct {
|
||||
|
||||
// ContrastCheckElement represents a single element's contrast check
|
||||
type ContrastCheckElement struct {
|
||||
Selector string `json:"selector"`
|
||||
Text string `json:"text"`
|
||||
ForegroundColor string `json:"foreground_color"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
ContrastRatio float64 `json:"contrast_ratio"`
|
||||
FontSize string `json:"font_size"`
|
||||
FontWeight string `json:"font_weight"`
|
||||
IsLargeText bool `json:"is_large_text"`
|
||||
PassesAA bool `json:"passes_aa"`
|
||||
PassesAAA bool `json:"passes_aaa"`
|
||||
RequiredAA float64 `json:"required_aa"`
|
||||
RequiredAAA float64 `json:"required_aaa"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Selector string `json:"selector"`
|
||||
Text string `json:"text"`
|
||||
ForegroundColor string `json:"foreground_color"`
|
||||
BackgroundColor string `json:"background_color"`
|
||||
ContrastRatio float64 `json:"contrast_ratio"`
|
||||
FontSize string `json:"font_size"`
|
||||
FontWeight string `json:"font_weight"`
|
||||
IsLargeText bool `json:"is_large_text"`
|
||||
PassesAA bool `json:"passes_aa"`
|
||||
PassesAAA bool `json:"passes_aaa"`
|
||||
RequiredAA float64 `json:"required_aa"`
|
||||
RequiredAAA float64 `json:"required_aaa"`
|
||||
DetectionMethod string `json:"detection_method,omitempty"` // "dom-tree", "stacking-context", "gradient-analysis"
|
||||
GradientInfo map[string]interface{} `json:"gradient_info,omitempty"` // Additional info for gradient backgrounds
|
||||
Error string `json:"error,omitempty"`
|
||||
}
|
||||
|
||||
// checkContrast checks color contrast for text elements on the page
|
||||
@@ -9062,24 +9064,153 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
||||
return (lighter + 0.05) / (darker + 0.05);
|
||||
}
|
||||
|
||||
// Helper function to get effective background color
|
||||
function getEffectiveBackground(element) {
|
||||
// Helper function to check if color is transparent
|
||||
function isTransparent(colorStr) {
|
||||
if (!colorStr || colorStr === 'transparent' || colorStr === 'rgba(0, 0, 0, 0)') {
|
||||
return true;
|
||||
}
|
||||
const parsed = parseColor(colorStr);
|
||||
return !parsed || parsed.a === 0;
|
||||
}
|
||||
|
||||
// Tier 1: Enhanced DOM tree walking
|
||||
function getEffectiveBackgroundDOMTree(element) {
|
||||
let current = element;
|
||||
while (current && current !== document.body.parentElement) {
|
||||
while (current && current !== document.documentElement.parentElement) {
|
||||
const style = window.getComputedStyle(current);
|
||||
const bgColor = style.backgroundColor;
|
||||
const parsed = parseColor(bgColor);
|
||||
|
||||
if (parsed && parsed.a > 0) {
|
||||
// Check if it's not transparent
|
||||
if (!(parsed.r === 0 && parsed.g === 0 && parsed.b === 0 && parsed.a === 0)) {
|
||||
return bgColor;
|
||||
}
|
||||
if (!isTransparent(bgColor)) {
|
||||
return bgColor;
|
||||
}
|
||||
|
||||
current = current.parentElement;
|
||||
}
|
||||
return 'rgb(255, 255, 255)'; // Default to white
|
||||
return null; // Return null if no background found
|
||||
}
|
||||
|
||||
// Tier 2: Stacking context analysis with multi-point sampling
|
||||
function getBackgroundFromStackingContext(element) {
|
||||
try {
|
||||
const rect = element.getBoundingClientRect();
|
||||
|
||||
// Skip if element is not visible
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Sample multiple points (center, corners) for accuracy
|
||||
const samplePoints = [
|
||||
{ x: rect.left + rect.width / 2, y: rect.top + rect.height / 2 }, // center
|
||||
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.25 }, // top-left
|
||||
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.75 }, // bottom-right
|
||||
{ x: rect.left + rect.width * 0.75, y: rect.top + rect.height * 0.25 }, // top-right
|
||||
{ x: rect.left + rect.width * 0.25, y: rect.top + rect.height * 0.75 } // bottom-left
|
||||
];
|
||||
|
||||
const backgrounds = [];
|
||||
|
||||
for (const point of samplePoints) {
|
||||
// Make sure point is within viewport
|
||||
if (point.x < 0 || point.y < 0 || point.x > window.innerWidth || point.y > window.innerHeight) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get all elements at this point (in z-index order)
|
||||
const elementsAtPoint = document.elementsFromPoint(point.x, point.y);
|
||||
|
||||
// Find the first element with a non-transparent background
|
||||
for (const el of elementsAtPoint) {
|
||||
if (el === element) continue; // Skip the text element itself
|
||||
|
||||
const bgColor = window.getComputedStyle(el).backgroundColor;
|
||||
if (!isTransparent(bgColor)) {
|
||||
backgrounds.push(bgColor);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return the most common background color found
|
||||
if (backgrounds.length > 0) {
|
||||
// Count occurrences
|
||||
const colorCounts = {};
|
||||
backgrounds.forEach(color => {
|
||||
colorCounts[color] = (colorCounts[color] || 0) + 1;
|
||||
});
|
||||
|
||||
// Find most common
|
||||
let maxCount = 0;
|
||||
let mostCommon = backgrounds[0];
|
||||
for (const color in colorCounts) {
|
||||
if (colorCounts[color] > maxCount) {
|
||||
maxCount = colorCounts[color];
|
||||
mostCommon = color;
|
||||
}
|
||||
}
|
||||
return mostCommon;
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: Gradient analysis
|
||||
function analyzeGradientContrast(element, textColor) {
|
||||
try {
|
||||
const style = window.getComputedStyle(element);
|
||||
const bgImage = style.backgroundImage;
|
||||
|
||||
// Check if background is a gradient
|
||||
if (!bgImage || !bgImage.includes('gradient')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Extract gradient colors using regex
|
||||
const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi;
|
||||
const matches = bgImage.match(colorRegex) || [];
|
||||
// Fix incomplete rgb/rgba matches by adding closing paren
|
||||
const colors = matches.map(c => c.includes('(') && !c.includes(')') ? c + ')' : c);
|
||||
|
||||
if (colors.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Calculate contrast against each color in the gradient
|
||||
const fg = parseColor(textColor);
|
||||
if (!fg) return null;
|
||||
|
||||
const contrastRatios = [];
|
||||
const parsedColors = [];
|
||||
|
||||
for (const colorStr of colors) {
|
||||
const bg = parseColor(colorStr);
|
||||
if (bg) {
|
||||
const ratio = getContrastRatio(fg, bg);
|
||||
contrastRatios.push(ratio);
|
||||
parsedColors.push(colorStr);
|
||||
}
|
||||
}
|
||||
|
||||
if (contrastRatios.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return worst-case (minimum) contrast ratio
|
||||
const worstCase = Math.min(...contrastRatios);
|
||||
const bestCase = Math.max(...contrastRatios);
|
||||
|
||||
return {
|
||||
worstCase: worstCase,
|
||||
bestCase: bestCase,
|
||||
colors: parsedColors,
|
||||
isGradient: true
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if text is large
|
||||
@@ -9104,10 +9235,42 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
||||
// Get computed styles
|
||||
const style = window.getComputedStyle(element);
|
||||
const fgColor = style.color;
|
||||
const bgColor = getEffectiveBackground(element);
|
||||
const fontSize = style.fontSize;
|
||||
const fontWeight = style.fontWeight;
|
||||
|
||||
// Three-tier background detection with progressive fallback
|
||||
let bgColor = null;
|
||||
let detectionMethod = null;
|
||||
let gradientInfo = null;
|
||||
|
||||
// Tier 1: DOM tree walking
|
||||
bgColor = getEffectiveBackgroundDOMTree(element);
|
||||
if (bgColor) {
|
||||
detectionMethod = 'dom-tree';
|
||||
}
|
||||
|
||||
// Tier 2: Stacking context analysis (if Tier 1 failed)
|
||||
if (!bgColor) {
|
||||
bgColor = getBackgroundFromStackingContext(element);
|
||||
if (bgColor) {
|
||||
detectionMethod = 'stacking-context';
|
||||
}
|
||||
}
|
||||
|
||||
// Tier 3: Check for gradient backgrounds
|
||||
if (bgColor) {
|
||||
gradientInfo = analyzeGradientContrast(element, fgColor);
|
||||
if (gradientInfo) {
|
||||
detectionMethod = 'gradient-analysis';
|
||||
}
|
||||
}
|
||||
|
||||
// Final fallback to white if no background detected
|
||||
if (!bgColor) {
|
||||
bgColor = 'rgb(255, 255, 255)';
|
||||
detectionMethod = 'fallback-white';
|
||||
}
|
||||
|
||||
// Parse colors
|
||||
const fg = parseColor(fgColor);
|
||||
const bg = parseColor(bgColor);
|
||||
@@ -9116,20 +9279,27 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
||||
results.push({
|
||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||
text: text.substring(0, 100),
|
||||
error: 'Unable to parse colors'
|
||||
error: 'Unable to parse colors',
|
||||
detection_method: detectionMethod
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Calculate contrast ratio
|
||||
const ratio = getContrastRatio(fg, bg);
|
||||
// Calculate contrast ratio (use worst case for gradients)
|
||||
let ratio;
|
||||
if (gradientInfo) {
|
||||
ratio = gradientInfo.worstCase;
|
||||
} else {
|
||||
ratio = getContrastRatio(fg, bg);
|
||||
}
|
||||
|
||||
const large = isLargeText(fontSize, fontWeight);
|
||||
|
||||
// WCAG requirements
|
||||
const requiredAA = large ? 3.0 : 4.5;
|
||||
const requiredAAA = large ? 4.5 : 7.0;
|
||||
|
||||
results.push({
|
||||
const result = {
|
||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||
text: text.substring(0, 100),
|
||||
foreground_color: fgColor,
|
||||
@@ -9141,8 +9311,21 @@ func (d *Daemon) checkContrast(tabID string, selector string, timeout int) (*Con
|
||||
passes_aa: ratio >= requiredAA,
|
||||
passes_aaa: ratio >= requiredAAA,
|
||||
required_aa: requiredAA,
|
||||
required_aaa: requiredAAA
|
||||
});
|
||||
required_aaa: requiredAAA,
|
||||
detection_method: detectionMethod
|
||||
};
|
||||
|
||||
// Add gradient info if present
|
||||
if (gradientInfo) {
|
||||
result.gradient_info = {
|
||||
worst_contrast: Math.round(gradientInfo.worstCase * 100) / 100,
|
||||
best_contrast: Math.round(gradientInfo.bestCase * 100) / 100,
|
||||
colors: gradientInfo.colors,
|
||||
is_gradient: true
|
||||
};
|
||||
}
|
||||
|
||||
results.push(result);
|
||||
} catch (e) {
|
||||
results.push({
|
||||
selector: '%s:nth-of-type(' + (index + 1) + ')',
|
||||
|
||||
Reference in New Issue
Block a user