diff --git a/contrast_detection_enhancement.md b/contrast_detection_enhancement.md new file mode 100644 index 0000000..11309df --- /dev/null +++ b/contrast_detection_enhancement.md @@ -0,0 +1,528 @@ +# Feature Request: Enhanced Background Color Detection for Contrast Analysis + +**Date:** December 8, 2025 +**Requested By:** Shortcut Solutions (ADA Audit Team) +**Priority:** Medium +**Estimated ROI:** High (saves 1-2 hours per audit) + +--- + +## Problem Statement + +The current `web_contrast_check_cremotemcp_cremotemcp` tool frequently fails to determine background colors when elements overlap or have transparent backgrounds. This results in: + +1. **False "failures"** that require manual verification +2. **Increased audit time** (1-2 hours per site for manual verification) +3. **Reduced confidence** in automated results +4. **Poor client experience** (reports show "requires manual verification" instead of definitive results) + +### Real-World Example + +During the visionleadership.org audit (December 8, 2025): +- **78 elements flagged** for contrast issues (27% of 289 elements) +- **Actual issues:** Likely 0-5 (most are false positives) +- **Root cause:** Tool couldn't determine background color due to: + - Overlapping header elements + - Transparent navigation backgrounds + - Gradient backgrounds on sliders + - Complex z-index stacking contexts + +### Current Behavior + +```javascript +// Current approach (simplified) +const bgColor = window.getComputedStyle(element).backgroundColor; + +// Problem: Returns 'transparent' or 'rgba(0, 0, 0, 0)' for many elements +// Result: Cannot calculate contrast ratio → flagged as "requires verification" +``` + +--- + +## Proposed Solution + +Implement a **three-tier background detection algorithm** with progressive fallback: + +### Tier 1: Enhanced DOM Tree Walking (Priority 1) +**Effort:** Low (2-4 hours) +**Impact:** Solves 80% of cases + +Walk up the DOM tree to find the first non-transparent background: + +```javascript +function getEffectiveBackgroundColor(element) { + let current = element; + let bgColor = window.getComputedStyle(current).backgroundColor; + + // Walk up DOM tree until we find a non-transparent background + while (current && (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)')) { + current = current.parentElement; + if (current) { + bgColor = window.getComputedStyle(current).backgroundColor; + } + } + + // If still transparent, default to white (common body background) + if (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)') { + bgColor = 'rgb(255, 255, 255)'; + } + + return bgColor; +} +``` + +### Tier 2: Element Stacking Context Analysis (Priority 2) +**Effort:** Medium (4-8 hours) +**Impact:** Solves 95% of cases + +Use `document.elementsFromPoint()` to find what's actually behind the text: + +```javascript +function getBackgroundFromStackingContext(element) { + const rect = element.getBoundingClientRect(); + + // 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 + ]; + + const backgrounds = []; + + for (const point of samplePoints) { + // 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 (bgColor !== 'transparent' && bgColor !== 'rgba(0, 0, 0, 0)') { + backgrounds.push(bgColor); + break; + } + } + } + + // Return the most common background color found + return getMostCommonColor(backgrounds) || 'rgb(255, 255, 255)'; +} +``` + +### Tier 3: Gradient and Complex Background Handling (Priority 3) +**Effort:** Medium-High (8-12 hours) +**Impact:** Solves 99% of cases + +For gradient backgrounds, calculate worst-case contrast: + +```javascript +function analyzeGradientContrast(element) { + const bgImage = window.getComputedStyle(element).backgroundImage; + + // Check if background is a gradient + if (bgImage.includes('gradient')) { + // Extract gradient colors using regex + const colors = extractGradientColors(bgImage); + + // Calculate contrast against each color in the gradient + const textColor = window.getComputedStyle(element).color; + const contrastRatios = colors.map(bgColor => + calculateContrastRatio(textColor, bgColor) + ); + + // Return worst-case (minimum) contrast ratio + return { + worstCase: Math.min(...contrastRatios), + bestCase: Math.max(...contrastRatios), + isGradient: true, + colors: colors + }; + } + + return null; // Not a gradient +} + +function extractGradientColors(gradientString) { + // Parse gradient string to extract color stops + // Example: "linear-gradient(90deg, rgb(255,0,0) 0%, rgb(0,0,255) 100%)" + const colorRegex = /rgba?\([^)]+\)|#[0-9a-f]{3,6}/gi; + return gradientString.match(colorRegex) || []; +} +``` + +--- + +## Implementation Plan + +### Phase 1: Core Enhancement (Week 1) +**Effort:** 2-4 hours + +1. Implement Tier 1 (DOM tree walking) +2. Update `web_contrast_check_cremotemcp_cremotemcp` to use new function +3. Add fallback to existing method if new method fails +4. Test on 5-10 real-world sites + +**Deliverables:** +- Updated contrast checking function +- Unit tests for DOM tree walking +- Documentation of new behavior + +### Phase 2: Stacking Context Analysis (Week 2) +**Effort:** 4-8 hours + +1. Implement Tier 2 (elementsFromPoint) +2. Add multi-point sampling for accuracy +3. Handle edge cases (fixed positioning, transforms) +4. Test on complex layouts + +**Deliverables:** +- Stacking context analysis function +- Integration tests with overlapping elements +- Performance benchmarks + +### Phase 3: Gradient Support (Week 3-4) +**Effort:** 8-12 hours + +1. Implement Tier 3 (gradient parsing) +2. Add worst-case contrast calculation +3. Handle CSS gradients, background images +4. Test on sites with complex backgrounds + +**Deliverables:** +- Gradient analysis function +- Comprehensive test suite +- Documentation with examples + +### Phase 4: Integration and Testing (Week 4) +**Effort:** 4-6 hours + +1. Integrate all three tiers with progressive fallback +2. Add configuration options (enable/disable tiers) +3. Performance optimization +4. Documentation and examples + +**Deliverables:** +- Complete integrated solution +- Performance benchmarks +- User documentation +- Migration guide + +--- + +## Technical Specifications + +### New Function Signature + +```javascript +/** + * Enhanced contrast checking with improved background detection + * @param {string} selector - CSS selector for elements to check + * @param {Object} options - Configuration options + * @param {boolean} options.useStackingContext - Enable Tier 2 (default: true) + * @param {boolean} options.analyzeGradients - Enable Tier 3 (default: true) + * @param {number} options.samplePoints - Number of points to sample (default: 3) + * @param {string} options.fallbackBg - Fallback background color (default: 'rgb(255, 255, 255)') + * @returns {Object} Enhanced contrast analysis results + */ +async function enhancedContrastCheck(selector, options = {}) { + const elements = document.querySelectorAll(selector); + const results = []; + + for (const element of elements) { + const textColor = window.getComputedStyle(element).color; + let bgColor = null; + let method = null; + + // Tier 1: DOM tree walking + bgColor = getEffectiveBackgroundColor(element); + method = 'dom-tree'; + + // Tier 2: Stacking context (if enabled and Tier 1 returned transparent) + if (options.useStackingContext && (bgColor === 'transparent' || bgColor === 'rgba(0, 0, 0, 0)')) { + bgColor = getBackgroundFromStackingContext(element); + method = 'stacking-context'; + } + + // Tier 3: Gradient analysis (if enabled) + let gradientInfo = null; + if (options.analyzeGradients) { + gradientInfo = analyzeGradientContrast(element); + if (gradientInfo) { + method = 'gradient-analysis'; + } + } + + // Calculate contrast ratio + const contrastRatio = gradientInfo + ? gradientInfo.worstCase + : calculateContrastRatio(textColor, bgColor); + + results.push({ + element: element, + selector: getElementSelector(element), + textColor: textColor, + backgroundColor: bgColor, + contrastRatio: contrastRatio, + detectionMethod: method, + gradientInfo: gradientInfo, + passes: { + AA: contrastRatio >= 4.5, + AAA: contrastRatio >= 7.0, + AALarge: contrastRatio >= 3.0, + AAALarge: contrastRatio >= 4.5 + } + }); + } + + return results; +} +``` + +### Return Value Structure + +```javascript +{ + element: HTMLElement, + selector: "nav > ul > li > a", + textColor: "rgb(51, 51, 51)", + backgroundColor: "rgb(255, 255, 255)", + contrastRatio: 12.63, + detectionMethod: "stacking-context", // or "dom-tree", "gradient-analysis" + gradientInfo: null, // or { worstCase, bestCase, isGradient, colors } + passes: { + AA: true, + AAA: true, + AALarge: true, + AAALarge: true + } +} +``` + +--- + +## Expected Benefits + +### Quantitative Benefits + +1. **Reduced False Positives:** 80-95% reduction in "requires manual verification" flags +2. **Time Savings:** 1-2 hours per audit (manual verification eliminated) +3. **Accuracy Improvement:** 95%+ accuracy vs current ~70% +4. **Client Satisfaction:** Fewer ambiguous results in reports + +### Qualitative Benefits + +1. **Increased Confidence:** Clients trust automated results more +2. **Better Reports:** Definitive pass/fail instead of "requires verification" +3. **Competitive Advantage:** More accurate than other automated tools +4. **Reduced Support:** Fewer questions about flagged items + +### Cost-Benefit Analysis + +**Development Cost:** 18-30 hours ($1,800-$3,000 at $100/hour) +**Time Savings:** 1.5 hours per audit × 50 audits/year = 75 hours/year ($7,500/year) +**ROI:** 150-250% in first year +**Payback Period:** 3-4 months + +--- + +## Testing Strategy + +### Unit Tests + +```javascript +describe('Enhanced Background Detection', () => { + test('should find background from parent element', () => { + // Test DOM tree walking + }); + + test('should detect background from overlapping element', () => { + // Test stacking context analysis + }); + + test('should calculate worst-case contrast for gradients', () => { + // Test gradient analysis + }); + + test('should fallback gracefully when detection fails', () => { + // Test fallback behavior + }); +}); +``` + +### Integration Tests + +1. **Simple Layouts:** Text on solid backgrounds +2. **Overlapping Elements:** Navigation menus, headers +3. **Transparent Backgrounds:** Inherited backgrounds +4. **Gradient Backgrounds:** Linear and radial gradients +5. **Complex Stacking:** Multiple z-index layers +6. **Fixed/Absolute Positioning:** Overlays, modals + +### Real-World Testing + +Test on 10-20 production websites including: +- E-commerce sites (complex headers) +- News sites (overlapping content) +- Corporate sites (gradient backgrounds) +- Educational sites (accessibility plugins) + +--- + +## Backwards Compatibility + +### Migration Strategy + +1. **Default Behavior:** New algorithm enabled by default +2. **Opt-Out Option:** Add `legacy_mode: true` to use old algorithm +3. **Gradual Rollout:** Test on internal audits first, then production +4. **Comparison Mode:** Run both algorithms and compare results + +### Configuration Options + +```javascript +// Enable all enhancements (recommended) +web_contrast_check_cremotemcp_cremotemcp({ + selector: 'body *', + enhanced_detection: true, + use_stacking_context: true, + analyze_gradients: true +}); + +// Legacy mode (old behavior) +web_contrast_check_cremotemcp_cremotemcp({ + selector: 'body *', + legacy_mode: true +}); + +// Custom configuration +web_contrast_check_cremotemcp_cremotemcp({ + selector: 'body *', + enhanced_detection: true, + use_stacking_context: true, + analyze_gradients: false, // Skip gradient analysis for speed + sample_points: 5, + fallback_bg: 'rgb(255, 255, 255)' +}); +``` + +--- + +## Performance Considerations + +### Current Performance +- **Time per element:** ~5-10ms +- **289 elements:** ~1.5-3 seconds + +### Expected Performance with Enhancements +- **Tier 1 (DOM tree):** +1-2ms per element +- **Tier 2 (stacking context):** +3-5ms per element +- **Tier 3 (gradient):** +5-10ms per element (only when gradients detected) + +**Total:** ~2-5 seconds for 289 elements (acceptable) + +### Optimization Strategies + +1. **Lazy Evaluation:** Only use Tier 2/3 when Tier 1 fails +2. **Caching:** Cache computed styles for repeated elements +3. **Parallel Processing:** Process elements in batches +4. **Early Exit:** Stop at first successful detection method + +--- + +## Documentation Requirements + +### User Documentation + +1. **Feature Overview:** What's new and why it matters +2. **Configuration Guide:** How to enable/disable features +3. **Examples:** Before/after comparisons +4. **Troubleshooting:** Common issues and solutions + +### Developer Documentation + +1. **Architecture:** How the three tiers work +2. **API Reference:** Function signatures and parameters +3. **Testing Guide:** How to test the new features +4. **Performance Guide:** Optimization tips + +--- + +## Success Metrics + +### Acceptance Criteria + +- [ ] Reduces false positives by 80%+ on test suite +- [ ] Maintains or improves performance (< 5 seconds for 300 elements) +- [ ] Passes all unit and integration tests +- [ ] Successfully tested on 10+ real-world sites +- [ ] Documentation complete and reviewed +- [ ] Backwards compatible with legacy mode + +### Post-Launch Monitoring + +1. **Accuracy Tracking:** Compare automated vs manual verification results +2. **Performance Monitoring:** Track execution time per audit +3. **User Feedback:** Collect feedback from audit team +4. **Error Rates:** Monitor detection failures and edge cases + +--- + +## Risks and Mitigations + +### Risk 1: Performance Degradation +**Likelihood:** Low +**Impact:** Medium +**Mitigation:** Implement lazy evaluation, caching, and performance benchmarks + +### Risk 2: New False Positives +**Likelihood:** Low +**Impact:** Medium +**Mitigation:** Extensive testing, gradual rollout, legacy mode fallback + +### Risk 3: Browser Compatibility +**Likelihood:** Low +**Impact:** Low +**Mitigation:** Test on Chrome, Firefox, Safari; use polyfills if needed + +### Risk 4: Complex Edge Cases +**Likelihood:** Medium +**Impact:** Low +**Mitigation:** Progressive fallback, comprehensive test suite, manual verification option + +--- + +## Future Enhancements + +### Phase 5: Machine Learning (Future) +- Train ML model on manually verified results +- Predict contrast issues with higher accuracy +- Learn from false positives/negatives + +### Phase 6: Screenshot-Based Analysis (Future) +- Capture actual rendered pixels +- Use image processing for perfect accuracy +- Handle all edge cases (animations, transforms, etc.) + +### Phase 7: Real-Time Validation (Future) +- Integrate with browser DevTools +- Provide live feedback during development +- Suggest color adjustments for compliance + +--- + +## Conclusion + +This enhancement will significantly improve the accuracy and reliability of automated contrast checking, reducing manual verification time and increasing client confidence in audit results. The three-tier approach provides a good balance between accuracy, performance, and complexity. + +**Recommendation:** Proceed with implementation starting with Phase 1 (Tier 1 DOM tree walking) as a quick win, then evaluate results before proceeding to Phases 2-3. + +--- + +**Feature Request Prepared By:** Shortcut Solutions +**Date:** December 8, 2025 +**Status:** Pending Review +**Priority:** Medium +**Estimated ROI:** High (150-250% first year) + diff --git a/daemon/daemon.go b/daemon/daemon.go index cd591a9..88663fd 100644 --- a/daemon/daemon.go +++ b/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) + ')',