diff --git a/Justfile b/Justfile index 0f0d2b3..4dd74db 100644 --- a/Justfile +++ b/Justfile @@ -172,12 +172,35 @@ docs-api: # Build documentation site docs-build: @echo "Building documentation..." - @echo "TODO: Implement docs site build" + @mkdir -p docs/_site + @# Convert AsciiDoc files to HTML + @for file in *.adoc docs/*.adoc; do \ + if [ -f "$$file" ]; then \ + echo "Converting $$file..."; \ + asciidoctor -D docs/_site "$$file" 2>/dev/null || echo " (skipped - asciidoctor not installed)"; \ + fi; \ + done + @# Copy markdown files + @for file in docs/*.md; do \ + if [ -f "$$file" ]; then \ + cp "$$file" docs/_site/ 2>/dev/null || true; \ + fi; \ + done + @# Copy static assets + @cp -r docs/*.md docs/_site/ 2>/dev/null || true + @cp README.adoc docs/_site/index.adoc 2>/dev/null || true + @echo "✓ Documentation built in docs/_site/" # Serve documentation locally docs-serve: @echo "Serving documentation..." - @echo "TODO: Implement docs site serve" + @just docs-build + @echo "Starting server at http://localhost:8000" + @echo "Press Ctrl+C to stop" + @cd docs/_site && python3 -m http.server 8000 2>/dev/null || \ + (cd docs/_site && python -m SimpleHTTPServer 8000 2>/dev/null) || \ + (cd docs/_site && deno run --allow-net --allow-read https://deno.land/std/http/file_server.ts) || \ + echo "Error: No suitable HTTP server found (python3, python, or deno required)" # === Cleanup === @@ -236,8 +259,37 @@ check-build-system: # Check tests exist check-tests: @echo "Checking test infrastructure..." - @echo "⚠️ Tests need to be implemented" - @echo "TODO: Add comprehensive test suite" + @# Check for test directories + @test_dirs=0; \ + for dir in tests test __tests__ spec; do \ + if [ -d "$$dir" ] || [ -d "packages/*/$$dir" ] || [ -d "components/*/$$dir" ]; then \ + test_dirs=$$((test_dirs + 1)); \ + fi; \ + done; \ + if [ $$test_dirs -eq 0 ]; then \ + echo "⚠️ No test directories found (tests/, test/, __tests__, spec/)"; \ + else \ + echo "✅ Found $$test_dirs test directories"; \ + fi + @# Check for test files + @test_files=$$(find . -name "*.test.*" -o -name "*.spec.*" -o -name "test_*.py" -o -name "*_test.go" 2>/dev/null | grep -v node_modules | wc -l); \ + if [ "$$test_files" -eq 0 ]; then \ + echo "⚠️ No test files found"; \ + else \ + echo "✅ Found $$test_files test files"; \ + fi + @# Check for test configuration + @if [ -f "jest.config.js" ] || [ -f "jest.config.ts" ] || [ -f "vitest.config.ts" ] || [ -f "pytest.ini" ] || [ -f "phpunit.xml" ]; then \ + echo "✅ Test configuration found"; \ + else \ + echo "⚠️ No test configuration found"; \ + fi + @# Check for test scripts in package.json + @if grep -q '"test"' package.json 2>/dev/null; then \ + echo "✅ Test script configured in package.json"; \ + else \ + echo "⚠️ No test script in package.json"; \ + fi # === Utility === diff --git a/docs/CAMPAIGN_MATERIALS.md b/docs/CAMPAIGN_MATERIALS.md index 722acbf..9a7ee0f 100644 --- a/docs/CAMPAIGN_MATERIALS.md +++ b/docs/CAMPAIGN_MATERIALS.md @@ -246,9 +246,9 @@ THE CASE: - Machine-readable headers (proposed) 4. Proven Demand: - - [XXX,XXX] users of our tools (6 months) - - [XXX] companies actively monitoring - - [XXX] GitHub Action installs + - [INSERT_USER_COUNT] users of our tools (6 months) + - [INSERT_COMPANY_COUNT] companies actively monitoring + - [INSERT_INSTALL_COUNT] GitHub Action installs - Growing developer awareness PROPOSED IMPLEMENTATION: @@ -301,10 +301,10 @@ WHAT WE OFFER: DATA AVAILABLE: After [6/12/18] months of operation: -- [XXX,XXX] sites scanned -- [XXX] paying API customers -- [XXX] developers using tools -- [XX]% average score improvement +- [INSERT_SCAN_COUNT] sites scanned +- [INSERT_CUSTOMER_COUNT] paying API customers +- [INSERT_DEVELOPER_COUNT] developers using tools +- [INSERT_IMPROVEMENT_PERCENT]% average score improvement This demonstrates real demand for accessibility as a ranking factor. diff --git a/tools/wordpress-plugin/includes/class-a11y-admin.php b/tools/wordpress-plugin/includes/class-a11y-admin.php index c6767ef..53470b7 100644 --- a/tools/wordpress-plugin/includes/class-a11y-admin.php +++ b/tools/wordpress-plugin/includes/class-a11y-admin.php @@ -247,10 +247,113 @@ private function render_recent_scans() { } /** - * Render common violations + * Render common violations aggregated from all scanned posts. + * + * Displays a summary of the most frequent accessibility violations + * found across the site's content. */ private function render_common_violations() { - // This would aggregate violation data - echo '
' . __('Configure API key to track violations', 'accessibility-everywhere') . '
'; + global $wpdb; + + // Get all violation data from post meta + $violations_data = $wpdb->get_col( + "SELECT meta_value FROM $wpdb->postmeta WHERE meta_key = '_a11y_violations' AND meta_value != ''" + ); + + if (empty($violations_data)) { + echo '' . __('No violation data available yet. Scan some pages to see common issues.', 'accessibility-everywhere') . '
'; + echo '' . __('Tip: Enable auto-scan in settings or use the Quick Scan feature.', 'accessibility-everywhere') . '
'; + return; + } + + // Aggregate violations by type + $violation_counts = []; + $violation_details = []; + + foreach ($violations_data as $json_data) { + $violations = json_decode($json_data, true); + if (!is_array($violations)) { + continue; + } + + foreach ($violations as $violation) { + $id = $violation['id'] ?? 'unknown'; + + if (!isset($violation_counts[$id])) { + $violation_counts[$id] = 0; + $violation_details[$id] = [ + 'description' => $violation['description'] ?? $id, + 'impact' => $violation['impact'] ?? 'unknown', + 'help' => $violation['help'] ?? '', + 'helpUrl' => $violation['helpUrl'] ?? '', + ]; + } + + // Count nodes/instances + $node_count = isset($violation['nodes']) ? count($violation['nodes']) : 1; + $violation_counts[$id] += $node_count; + } + } + + if (empty($violation_counts)) { + echo '' . __('No violations found across scanned pages.', 'accessibility-everywhere') . '
'; + return; + } + + // Sort by count descending + arsort($violation_counts); + + // Display top 10 violations + $top_violations = array_slice($violation_counts, 0, 10, true); + + echo '| ' . esc_html__('Issue', 'accessibility-everywhere') . ' | '; + echo '' . esc_html__('Impact', 'accessibility-everywhere') . ' | '; + echo '' . esc_html__('Count', 'accessibility-everywhere') . ' | '; + echo '
|---|---|---|
| ';
+
+ if (!empty($details['helpUrl'])) {
+ echo '';
+ echo esc_html($details['description']);
+ echo ' ';
+ echo '' . esc_html__('(opens in new tab)', 'accessibility-everywhere') . '';
+ echo '';
+ } else {
+ echo esc_html($details['description']);
+ }
+
+ if (!empty($details['help'])) {
+ echo ' ' . esc_html($details['help']) . ' '; + } + + echo ' | ';
+ echo '' . esc_html(ucfirst($details['impact'])) . ' | '; + echo '' . esc_html(number_format_i18n($count)) . ' | '; + echo '
'; + printf( + /* translators: 1: total violation count, 2: unique issue count */ + esc_html__('Total: %1$s violations across %2$s unique issue types.', 'accessibility-everywhere'), + '' . esc_html(number_format_i18n($total_violations)) . '', + '' . esc_html(number_format_i18n($unique_issues)) . '' + ); + echo '
'; } } diff --git a/tools/wordpress-plugin/includes/class-a11y-scanner.php b/tools/wordpress-plugin/includes/class-a11y-scanner.php index a002a2d..e22c10c 100644 --- a/tools/wordpress-plugin/includes/class-a11y-scanner.php +++ b/tools/wordpress-plugin/includes/class-a11y-scanner.php @@ -58,30 +58,298 @@ private function scan_via_api($url, $options) { /** * Client-side scanning (requires JavaScript) + * + * Returns configuration for browser-based axe-core scanning. + * The actual scan is performed by JavaScript using the axe-core library. + * + * @param string $url The URL to scan. + * @param array $options Scan options. + * @return array Scan configuration for client-side execution. */ private function scan_client_side($url, $options) { - // Return placeholder - actual scanning happens in browser + // Generate a unique scan ID for tracking + $scan_id = wp_generate_uuid4(); + + // Store pending scan in transient for JS callback + set_transient('a11y_pending_scan_' . $scan_id, [ + 'url' => $url, + 'options' => $options, + 'started' => current_time('mysql'), + ], HOUR_IN_SECONDS); + + // Return configuration for client-side scanning return [ 'url' => $url, - 'score' => 0, - 'violations' => [], - 'passes' => [], - 'incomplete' => [], + 'scan_id' => $scan_id, 'client_side' => true, - 'message' => __('Configure API key for server-side scanning', 'accessibility-everywhere'), + 'config' => [ + 'wcag_level' => $options['wcag_level'], + 'rules' => $this->get_wcag_rules($options['wcag_level']), + 'callback_url' => rest_url('accessibility-everywhere/v1/scan-complete'), + 'nonce' => wp_create_nonce('a11y_scan_complete'), + ], + 'message' => __('Scan initiated. Results will be processed via axe-core in browser.', 'accessibility-everywhere'), ]; } /** - * Scan post content HTML + * Get WCAG rules for the specified level. + * + * @param string $level WCAG level (A, AA, or AAA). + * @return array Array of rule tags to check. + */ + private function get_wcag_rules($level) { + $rules = ['wcag2a', 'wcag21a', 'wcag22a', 'best-practice']; + + if ($level === 'AA' || $level === 'AAA') { + $rules = array_merge($rules, ['wcag2aa', 'wcag21aa', 'wcag22aa']); + } + + if ($level === 'AAA') { + $rules = array_merge($rules, ['wcag2aaa', 'wcag21aaa', 'wcag22aaa']); + } + + return $rules; + } + + /** + * Scan post content HTML for accessibility issues. + * + * Performs server-side HTML analysis for common accessibility violations. + * This is a lightweight check that catches obvious issues without requiring + * a full browser-based axe-core scan. + * + * @param string $html The HTML content to scan. + * @param array $options Scan options. + * @return array Scan results with violations and score. */ public function scan_html($html, $options = []) { - // This would integrate with axe-core via JavaScript - // For now, return placeholder + $violations = []; + $passes = []; + + // Load HTML into DOMDocument for analysis + $dom = new DOMDocument(); + libxml_use_internal_errors(true); + $dom->loadHTML('' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + libxml_clear_errors(); + + $xpath = new DOMXPath($dom); + + // Check for images without alt attributes + $images = $xpath->query('//img[not(@alt)]'); + if ($images->length > 0) { + $nodes = []; + foreach ($images as $img) { + $nodes[] = [ + 'html' => $dom->saveHTML($img), + 'target' => $this->get_selector($img), + ]; + } + $violations[] = [ + 'id' => 'image-alt', + 'impact' => 'critical', + 'description' => __('Images must have alternate text', 'accessibility-everywhere'), + 'help' => __('Ensures