feat: add guzzle-powered recursive website crawling and masterminds/html5 integration

This commit is contained in:
Enes Yakıştır
2026-06-06 13:22:35 +03:00
parent b805741ef2
commit c42ab714ff
5 changed files with 368 additions and 180 deletions
+3 -1
View File
@@ -30,7 +30,9 @@
"symfony/yaml": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0",
"monolog/monolog": "^3.0", "monolog/monolog": "^3.0",
"vlucas/phpdotenv": "^5.6", "vlucas/phpdotenv": "^5.6",
"google-gemini-php/client": "^1.0" "google-gemini-php/client": "^1.0",
"guzzlehttp/guzzle": "^7.8",
"masterminds/html5": "^2.8"
}, },
"require-dev": { "require-dev": {
"phpunit/phpunit": "^10.5", "phpunit/phpunit": "^10.5",
+10 -19
View File
@@ -1,19 +1,11 @@
<?php <?php
// 1. Setup Autoloading (Mocking Composer) // 1. Setup Autoloading
require_once __DIR__ . '/../src/Scanner.php'; require_once __DIR__ . '/../vendor/autoload.php';
require_once __DIR__ . '/../src/Violation.php';
require_once __DIR__ . '/../src/SourceLocator.php';
require_once __DIR__ . '/../src/Reporter/HtmlReporter.php';
require_once __DIR__ . '/../src/Rule/RuleInterface.php';
require_once __DIR__ . '/../src/Rules/ImgAltText.php';
require_once __DIR__ . '/../src/Rules/FormLabel.php';
require_once __DIR__ . '/../src/Rules/HtmlHasLang.php';
require_once __DIR__ . '/../src/Rules/EmptyLink.php';
use YakNet\AccessibilityConsole\Scanner; use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\SourceLocator; use YakNet\AccessibilityConsole\Source\SourceLocator;
use YakNet\AccessibilityConsole\Reporter\HtmlReporter; use YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter;
use YakNet\AccessibilityConsole\Rules\ImgAltText; use YakNet\AccessibilityConsole\Rules\ImgAltText;
use YakNet\AccessibilityConsole\Rules\FormLabel; use YakNet\AccessibilityConsole\Rules\FormLabel;
@@ -48,19 +40,18 @@ $violations = $scanner->scan($html);
if (!empty($violations)) { if (!empty($violations)) {
// Locate Sources // Locate Sources
// We point it to the 'views' directory so it knows where to look for the error source
$locator = new SourceLocator(__DIR__ . '/views'); $locator = new SourceLocator(__DIR__ . '/views');
foreach ($violations as $violation) { foreach ($violations as $violation) {
$location = $locator->locate($violation->snippet); $location = $locator->locate($violation->htmlSnippet);
if ($location) { if ($location) {
$violation->setSourceLocation($location['file'], $location['line']); $violation->location = $location;
} }
} }
// Render Report // Render Report (Format as results mapped by page URL)
$reporter = new HtmlReporter(); $reporter = new HtmlDashboardReporter();
$reportHtml = $reporter->render($violations); $reportHtml = $reporter->render(['http://localhost:8080/index.php' => $violations], 'demo/index.php');
// Inject into body // Inject into body
$html = str_replace('</body>', $reportHtml . '</body>', $html); $html = str_replace('</body>', $reportHtml . '</body>', $html);
+144 -19
View File
@@ -26,7 +26,10 @@ class ScanCommand extends Command
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan') ->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
->addOption('ai', null, InputOption::VALUE_NONE, 'Enable AI-powered fix suggestions') ->addOption('ai', null, InputOption::VALUE_NONE, 'Enable AI-powered fix suggestions')
->addOption('report', null, InputOption::VALUE_REQUIRED, 'Save report to file (html or json)') ->addOption('report', null, InputOption::VALUE_REQUIRED, 'Save report to file (html or json)')
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping'); ->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping')
->addOption('crawl', null, InputOption::VALUE_NONE, 'Crawl internal links recursively')
->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Maximum crawling depth', '3')
->addOption('max-pages', null, InputOption::VALUE_REQUIRED, 'Maximum number of pages to crawl', '20');
} }
protected function execute(InputInterface $input, OutputInterface $output): int protected function execute(InputInterface $input, OutputInterface $output): int
@@ -46,34 +49,152 @@ class ScanCommand extends Command
$io->title("Accessibility Console - Scanning: $target"); $io->title("Accessibility Console - Scanning: $target");
// 1. Fetch HTML // 1. Setup Scanner
$html = @file_get_contents($target);
if ($html === false || $html === '') {
$io->error("Could not read target: $target");
return Command::FAILURE;
}
// 2. Setup Scanner
$scanner = new Scanner(); $scanner = new Scanner();
foreach (StandardRuleSet::all() as $rule) { foreach (StandardRuleSet::all() as $rule) {
$scanner->addRule($rule); $scanner->addRule($rule);
} }
// 3. Scan // 2. Fetch and Scan
$violations = $scanner->scan($html); $isUrl = str_starts_with($target, 'http://') || str_starts_with($target, 'https://');
$crawl = (bool)$input->getOption('crawl') && $isUrl;
if (empty($violations)) { /** @var array<string, \YakNet\AccessibilityConsole\Core\Violation[]> $results */
$results = [];
if ($crawl) {
$depthOption = $input->getOption('depth');
$depthLimit = is_numeric($depthOption) ? (int)$depthOption : 3;
$maxPagesOption = $input->getOption('max-pages');
$maxPages = is_numeric($maxPagesOption) ? (int)$maxPagesOption : 20;
$io->comment("Starting recursive crawl from: $target (Max Depth: $depthLimit, Max Pages: $maxPages)");
$parsedBase = parse_url($target);
$baseHost = $parsedBase['host'] ?? '';
$queue = [
['url' => $target, 'depth' => 1]
];
$visited = [];
$client = new \GuzzleHttp\Client([
'timeout' => 10,
'allow_redirects' => true,
'headers' => [
'User-Agent' => 'YakNet Accessibility Console Crawler/1.0',
]
]);
while (!empty($queue) && count($visited) < $maxPages) {
$current = array_shift($queue);
if (!is_array($current)) {
continue;
}
$url = $current['url'];
$depth = (int)$current['depth'];
// Remove fragment
$cleanUrl = strtok($url, '#');
if ($cleanUrl === false || isset($visited[$cleanUrl])) {
continue;
}
$visited[$cleanUrl] = true;
$io->write(" <fg=gray>Crawling [Depth $depth]:</> <fg=cyan>$cleanUrl</>... ");
try {
$response = $client->request('GET', $cleanUrl);
$html = (string)$response->getBody();
$io->writeln("<fg=green>OK</>");
// Scan the page HTML
$pageViolations = $scanner->scan($html);
$results[$cleanUrl] = $pageViolations;
// Extract links if depth is less than limit
if ($depth < $depthLimit) {
$html5 = new \Masterminds\HTML5(['disable_html_ns' => true]);
$doc = $html5->loadHTML($html);
$links = $doc->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:')) {
continue;
}
// Resolve relative URI
try {
$baseUri = new \GuzzleHttp\Psr7\Uri($cleanUrl);
$relativeUri = new \GuzzleHttp\Psr7\Uri($href);
$absoluteUri = \GuzzleHttp\Psr7\UriResolver::resolve($baseUri, $relativeUri);
$absoluteUrl = (string)$absoluteUri;
} catch (\Throwable) {
continue;
}
// Check host compatibility
$parsedAbsolute = parse_url($absoluteUrl);
$absoluteHost = $parsedAbsolute['host'] ?? '';
if ($absoluteHost === $baseHost) {
// Add to queue if not already visited or in queue
$cleanAbsolute = strtok($absoluteUrl, '#');
if ($cleanAbsolute !== false && !isset($visited[$cleanAbsolute])) {
$inQueue = false;
foreach ($queue as $q) {
if ($q['url'] === $cleanAbsolute) {
$inQueue = true;
break;
}
}
if (!$inQueue) {
$queue[] = ['url' => $cleanAbsolute, 'depth' => $depth + 1];
}
}
}
}
}
} catch (\Throwable $e) {
$io->writeln("<fg=red>FAIL (" . $e->getMessage() . ")</>");
}
}
} else {
// Standard single page/file scan
$html = @file_get_contents($target);
if ($html === false || $html === '') {
$io->error("Could not read target: $target");
return Command::FAILURE;
}
$violations = $scanner->scan($html);
$results[$target] = $violations;
}
$totalViolations = 0;
foreach ($results as $url => $violations) {
$totalViolations += count($violations);
}
if ($totalViolations === 0) {
$io->success("No violations found!"); $io->success("No violations found!");
return Command::SUCCESS; return Command::SUCCESS;
} }
$io->section("Found " . count($violations) . " violations"); $io->section("Found a total of $totalViolations violations across " . count($results) . " pages");
// 4. Setup Helpers // 3. Setup Helpers
$locator = new SourceLocator($projectPath); $locator = new SourceLocator($projectPath);
$config = new Config(); $config = new Config();
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null; $fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
foreach ($results as $url => $violations) {
if (empty($violations)) {
continue;
}
$io->section("Page: $url (" . count($violations) . " violations)");
foreach ($violations as $violation) { foreach ($violations as $violation) {
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>"); $io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
@@ -100,19 +221,23 @@ class ScanCommand extends Command
$io->writeln(""); $io->writeln("");
} }
}
$io->warning("Scanned " . count($violations) . " errors. Use --ai to get fix suggestions."); $io->warning("Scanned " . $totalViolations . " errors. Use --ai to get fix suggestions.");
// Export Report // 4. Export Report
$reportPath = $input->getOption('report'); $reportPath = $input->getOption('report');
if (is_string($reportPath) && $reportPath !== '') { if (is_string($reportPath) && $reportPath !== '') {
$extension = pathinfo($reportPath, PATHINFO_EXTENSION); $extension = pathinfo($reportPath, PATHINFO_EXTENSION);
if ($extension === 'json') { if ($extension === 'json') {
$data = array_map(fn($v) => $v->toArray(), $violations); $serialized = [];
file_put_contents($reportPath, json_encode($data, JSON_PRETTY_PRINT)); foreach ($results as $url => $violations) {
$serialized[$url] = array_map(fn($v) => $v->toArray(), $violations);
}
file_put_contents($reportPath, json_encode($serialized, JSON_PRETTY_PRINT));
} else { } else {
$reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter(); $reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter();
file_put_contents($reportPath, $reporter->render($violations, $target)); file_put_contents($reportPath, $reporter->render($results, $target));
} }
$io->success("Report saved to: $reportPath"); $io->success("Report saved to: $reportPath");
} }
+4 -3
View File
@@ -28,9 +28,10 @@ class Scanner
return []; return [];
} }
$doc = new \DOMDocument(); $html5 = new \Masterminds\HTML5([
// Suppress warnings for malformed HTML 'disable_html_ns' => true,
@$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); ]);
$doc = $html5->loadHTML($html);
return $this->engine->run($doc); return $this->engine->run($doc);
} }
+101 -32
View File
@@ -7,14 +7,14 @@ use YakNet\AccessibilityConsole\Core\Violation;
class HtmlDashboardReporter class HtmlDashboardReporter
{ {
/** /**
* @param Violation[] $violations * @param array<string, Violation[]> $results
*/ */
public function render(array $violations, string $target): string public function render(array $results, string $target): string
{ {
$count = count($violations); $totalPages = count($results);
$date = date('Y-m-d H:i:s'); $date = date('Y-m-d H:i:s');
// Calculate severity and standard counts // Calculate severity, standard counts and overall average score
$criticalCount = 0; $criticalCount = 0;
$errorCount = 0; $errorCount = 0;
$warningCount = 0; $warningCount = 0;
@@ -24,19 +24,33 @@ class HtmlDashboardReporter
$standardAA = 0; $standardAA = 0;
$standardAAA = 0; $standardAAA = 0;
$pageScores = [];
$totalViolations = 0;
foreach ($results as $url => $violations) {
$totalViolations += count($violations);
$pageCrit = 0;
$pageErr = 0;
$pageWarn = 0;
$pageInfo = 0;
foreach ($violations as $v) { foreach ($violations as $v) {
switch ($v->severity) { switch ($v->severity) {
case \YakNet\AccessibilityConsole\Core\Severity::CRITICAL: case \YakNet\AccessibilityConsole\Core\Severity::CRITICAL:
$criticalCount++; $criticalCount++;
$pageCrit++;
break; break;
case \YakNet\AccessibilityConsole\Core\Severity::ERROR: case \YakNet\AccessibilityConsole\Core\Severity::ERROR:
$errorCount++; $errorCount++;
$pageErr++;
break; break;
case \YakNet\AccessibilityConsole\Core\Severity::WARNING: case \YakNet\AccessibilityConsole\Core\Severity::WARNING:
$warningCount++; $warningCount++;
$pageWarn++;
break; break;
case \YakNet\AccessibilityConsole\Core\Severity::INFO: case \YakNet\AccessibilityConsole\Core\Severity::INFO:
$infoCount++; $infoCount++;
$pageInfo++;
break; break;
} }
@@ -53,12 +67,25 @@ class HtmlDashboardReporter
} }
} }
// Calculate health score (0-100) $deductions = ($pageCrit * 15) + ($pageErr * 10) + ($pageWarn * 4) + ($pageInfo * 1);
$deductions = ($criticalCount * 15) + ($errorCount * 10) + ($warningCount * 4) + ($infoCount * 1); $pageScores[$url] = max(0, min(100, 100 - $deductions));
$score = max(0, min(100, 100 - $deductions)); }
$averageScore = $totalPages > 0 ? (int)round(array_sum($pageScores) / $totalPages) : 100;
// Generate Page Selector options
$pageOptions = "<option value=\"all\">Tüm Sayfalar ({$totalPages} sayfa, {$totalViolations} ihlal)</option>";
foreach ($results as $url => $violations) {
$escapedUrl = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
$vCount = count($violations);
$pageOptions .= "<option value=\"{$escapedUrl}\">{$escapedUrl} ({$vCount} ihlal)</option>";
}
$cards = ''; $cards = '';
foreach ($violations as $index => $v) { $cardIndex = 0;
foreach ($results as $url => $violations) {
$escapedUrl = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
foreach ($violations as $v) {
$severityClass = strtolower($v->severity->value); $severityClass = strtolower($v->severity->value);
$standardVal = $v->standard->value; $standardVal = $v->standard->value;
$aiSuggestion = $v->fixSuggestion; $aiSuggestion = $v->fixSuggestion;
@@ -82,14 +109,14 @@ class HtmlDashboardReporter
$escapedMessage = htmlspecialchars($v->message, ENT_QUOTES, 'UTF-8'); $escapedMessage = htmlspecialchars($v->message, ENT_QUOTES, 'UTF-8');
$hasAiFix = !empty($fixedCode); $hasAiFix = !empty($fixedCode);
$aiTabButton = $hasAiFix ? "<button class=\"tab-btn\" id=\"tab-ai-{$index}\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-ai-{$index}\" tabindex=\"-1\">Yapay Zeka Önerisi</button>" : ""; $aiTabButton = $hasAiFix ? "<button class=\"tab-btn\" id=\"tab-ai-{$cardIndex}\" role=\"tab\" aria-selected=\"false\" aria-controls=\"panel-ai-{$cardIndex}\" tabindex=\"-1\">Yapay Zeka Önerisi</button>" : "";
$aiTabPanel = $hasAiFix ? " $aiTabPanel = $hasAiFix ? "
<div class=\"tab-panel\" id=\"panel-ai-{$index}\" role=\"tabpanel\" aria-labelledby=\"tab-ai-{$index}\" hidden> <div class=\"tab-panel\" id=\"panel-ai-{$cardIndex}\" role=\"tabpanel\" aria-labelledby=\"tab-ai-{$cardIndex}\" hidden>
<div class=\"ai-recommendation\"> <div class=\"ai-recommendation\">
" . ($escapedExplanation ? "<div class=\"ai-explanation\"><strong>Öneri Açıklaması:</strong> {$escapedExplanation}</div>" : "") . " " . ($escapedExplanation ? "<div class=\"ai-explanation\"><strong>Öneri Açıklaması:</strong> {$escapedExplanation}</div>" : "") . "
<div class=\"code-wrapper\"> <div class=\"code-wrapper\">
<pre><code id=\"code-ai-{$index}\">{$escapedFixed}</code></pre> <pre><code id=\"code-ai-{$cardIndex}\">{$escapedFixed}</code></pre>
<button class=\"copy-btn\" aria-label=\"{$escapedRuleId} kuralı için önerilen düzeltmeyi kopyala\" onclick=\"copyCode('#code-ai-{$index}', this)\"> <button class=\"copy-btn\" aria-label=\"{$escapedRuleId} kuralı için önerilen düzeltmeyi kopyala\" onclick=\"copyCode('#code-ai-{$cardIndex}', this)\">
<svg class=\"icon-copy\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg> <svg class=\"icon-copy\" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\"><rect x=\"9\" y=\"9\" width=\"13\" height=\"13\" rx=\"2\" ry=\"2\"></rect><path d=\"M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1\"></path></svg>
<span>Kopyala</span> <span>Kopyala</span>
</button> </button>
@@ -98,7 +125,7 @@ class HtmlDashboardReporter
</div>" : ""; </div>" : "";
$cards .= " $cards .= "
<div class=\"violation-card\" data-severity=\"{$severityClass}\" data-standard=\"{$standardVal}\" data-rule=\"{$escapedRuleId}\" data-message=\"{$escapedMessage}\"> <div class=\"violation-card\" data-severity=\"{$severityClass}\" data-standard=\"{$standardVal}\" data-rule=\"{$escapedRuleId}\" data-message=\"{$escapedMessage}\" data-page=\"{$escapedUrl}\">
<div class=\"card-header\"> <div class=\"card-header\">
<div class=\"badge-row\"> <div class=\"badge-row\">
<span class=\"badge rule-badge\">{$escapedRuleId}</span> <span class=\"badge rule-badge\">{$escapedRuleId}</span>
@@ -118,16 +145,24 @@ class HtmlDashboardReporter
<div class=\"code-viewer\"> <div class=\"code-viewer\">
<div class=\"tab-list\" role=\"tablist\"> <div class=\"tab-list\" role=\"tablist\">
<button class=\"tab-btn active\" id=\"tab-orig-{$index}\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-orig-{$index}\">Mevcut Kod</button> <button class=\"tab-btn active\" id=\"tab-orig-{$cardIndex}\" role=\"tab\" aria-selected=\"true\" aria-controls=\"panel-orig-{$cardIndex}\">Mevcut Kod</button>
{$aiTabButton} {$aiTabButton}
</div> </div>
<div class=\"tab-panel active\" id=\"panel-orig-{$index}\" role=\"tabpanel\" aria-labelledby=\"tab-orig-{$index}\"> <div class=\"tab-panel active\" id=\"panel-orig-{$cardIndex}\" role=\"tabpanel\" aria-labelledby=\"tab-orig-{$cardIndex}\">
<pre><code>{$escapedSnippet}</code></pre> <pre><code>{$escapedSnippet}</code></pre>
</div> </div>
{$aiTabPanel} {$aiTabPanel}
</div> </div>
<div class=\"page-meta-row\" style=\"margin-top: 12px; font-size: 0.8rem; color: var(--text-secondary); display: flex; align-items: center; gap: 6px;\">
<svg viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" style=\"width:14px;height:14px;color:var(--text-muted);\"><path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"></path><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"></path></svg>
<span>Kaynak Sayfa: <strong style=\"word-break: break-all;\">{$escapedUrl}</strong></span>
</div>
</div> </div>
</div>"; </div>";
$cardIndex++;
}
} }
return <<<HTML return <<<HTML
@@ -378,7 +413,7 @@ class HtmlDashboardReporter
stroke-width: 8; stroke-width: 8;
stroke-linecap: round; stroke-linecap: round;
stroke-dasharray: 251.2; stroke-dasharray: 251.2;
stroke-dashoffset: calc(251.2 - (251.2 * {$score}) / 100); stroke-dashoffset: calc(251.2 - (251.2 * {$averageScore}) / 100);
transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1); transition: stroke-dashoffset 1.5s cubic-bezier(0.4, 0, 0.2, 1);
} }
@@ -466,7 +501,7 @@ class HtmlDashboardReporter
.search-group { .search-group {
position: relative; position: relative;
flex: 1; flex: 2;
min-width: 250px; min-width: 250px;
} }
@@ -506,6 +541,35 @@ class HtmlDashboardReporter
background: #fff; background: #fff;
} }
.page-select-group {
flex: 1;
min-width: 200px;
}
.select-input {
width: 100%;
background: rgba(255, 255, 255, 0.03);
border: 1px solid var(--border-card);
border-radius: 10px;
padding: 10px 16px;
color: var(--text-primary);
font-family: var(--font-secondary);
font-size: 0.9rem;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
}
body.light-theme .select-input {
background: rgba(0, 0, 0, 0.02);
}
body.light-theme .select-input option {
background-color: white;
color: #111827;
}
body.light-theme .select-input:focus {
background-color: white;
}
.filter-group { .filter-group {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@@ -820,7 +884,7 @@ class HtmlDashboardReporter
</div> </div>
</div> </div>
<div class="header-meta"> <div class="header-meta">
<div class="meta-tag" title="{$target}">Hedef: {$target}</div> <div class="meta-tag" title="{$target}">Hedef Site: {$target}</div>
<div class="meta-tag">Tarih: {$date}</div> <div class="meta-tag">Tarih: {$date}</div>
<button id="theme-toggle" class="theme-btn" aria-label="Açık/Karanlık tema değiştir" onclick="toggleTheme()"> <button id="theme-toggle" class="theme-btn" aria-label="Açık/Karanlık tema değiştir" onclick="toggleTheme()">
<!-- Moon icon --> <!-- Moon icon -->
@@ -838,13 +902,13 @@ class HtmlDashboardReporter
<div class="gauge-container"> <div class="gauge-container">
<svg viewBox="0 0 100 100" class="gauge"> <svg viewBox="0 0 100 100" class="gauge">
<circle cx="50" cy="50" r="40" class="gauge-bg"></circle> <circle cx="50" cy="50" r="40" class="gauge-bg"></circle>
<circle cx="50" cy="50" r="40" class="gauge-val " id="score-gauge" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle> <circle cx="50" cy="50" r="40" class="gauge-val" id="score-gauge" stroke-dasharray="251.2" stroke-dashoffset="251.2"></circle>
</svg> </svg>
<div class="gauge-label" id="score-val">0</div> <div class="gauge-label" id="score-val">0</div>
</div> </div>
<div class="metric-desc"> <div class="metric-desc">
<h2>Erişilebilirlik Skoru</h2> <h2>Ortalama Sağlık Skoru</h2>
<p>Sayfanın genel standartlara uygunluk derecesi.</p> <p>Tarama yapılan sayfaların ortalama puanı.</p>
</div> </div>
</div> </div>
@@ -882,20 +946,16 @@ class HtmlDashboardReporter
<div class="glass-panel"> <div class="glass-panel">
<div class="summary-stats"> <div class="summary-stats">
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">WCAG A</span> <span class="stat-label">Taranan Sayfa</span>
<span class="stat-val">{$standardA}</span> <span class="stat-val">{$totalPages}</span>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">WCAG AA</span> <span class="stat-label">WCAG A / AA / AAA</span>
<span class="stat-val">{$standardAA}</span> <span class="stat-val">{$standardA} / {$standardAA} / {$standardAAA}</span>
</div>
<div class="stat-item">
<span class="stat-label">WCAG AAA</span>
<span class="stat-val">{$standardAAA}</span>
</div> </div>
<div class="stat-item"> <div class="stat-item">
<span class="stat-label">Toplam İhlal</span> <span class="stat-label">Toplam İhlal</span>
<span class="stat-val">{$count}</span> <span class="stat-val">{$totalViolations}</span>
</div> </div>
</div> </div>
</div> </div>
@@ -910,6 +970,12 @@ class HtmlDashboardReporter
<input type="search" id="search-box" class="search-input" placeholder="Kural veya hata açıklaması ara..." aria-label="Kural veya hata açıklaması ara" oninput="applyFilters()"> <input type="search" id="search-box" class="search-input" placeholder="Kural veya hata açıklaması ara..." aria-label="Kural veya hata açıklaması ara" oninput="applyFilters()">
</div> </div>
<div class="page-select-group">
<select id="page-select" class="select-input" aria-label="Taranan sayfaya göre filtrele" onchange="applyFilters()">
{$pageOptions}
</select>
</div>
<div class="filter-group"> <div class="filter-group">
<button class="filter-btn active" id="btn-all" onclick="setSeverityFilter('all')" aria-label="Tüm ihlalleri göster" aria-pressed="true">Hepsi</button> <button class="filter-btn active" id="btn-all" onclick="setSeverityFilter('all')" aria-label="Tüm ihlalleri göster" aria-pressed="true">Hepsi</button>
<button class="filter-btn" id="btn-critical" onclick="setSeverityFilter('critical')" aria-label="Sadece Critical seviye ihlalleri göster" aria-pressed="false"> <button class="filter-btn" id="btn-critical" onclick="setSeverityFilter('critical')" aria-label="Sadece Critical seviye ihlalleri göster" aria-pressed="false">
@@ -946,7 +1012,7 @@ class HtmlDashboardReporter
<!-- JavaScript Logic --> <!-- JavaScript Logic -->
<script> <script>
// Apply Circular Score Gauge Color and Animation // Apply Circular Score Gauge Color and Animation
const score = {$score}; const score = {$averageScore};
const gauge = document.getElementById('score-gauge'); const gauge = document.getElementById('score-gauge');
const scoreText = document.getElementById('score-val'); const scoreText = document.getElementById('score-val');
@@ -1143,6 +1209,7 @@ class HtmlDashboardReporter
function applyFilters() { function applyFilters() {
const searchVal = document.getElementById('search-box').value.toLowerCase(); const searchVal = document.getElementById('search-box').value.toLowerCase();
const pageVal = document.getElementById('page-select').value;
const cards = document.querySelectorAll('.violation-card'); const cards = document.querySelectorAll('.violation-card');
let visibleCount = 0; let visibleCount = 0;
@@ -1150,11 +1217,13 @@ class HtmlDashboardReporter
const severity = card.getAttribute('data-severity'); const severity = card.getAttribute('data-severity');
const rule = card.getAttribute('data-rule').toLowerCase(); const rule = card.getAttribute('data-rule').toLowerCase();
const message = card.getAttribute('data-message').toLowerCase(); const message = card.getAttribute('data-message').toLowerCase();
const page = card.getAttribute('data-page');
const matchesSeverity = (activeSeverity === 'all' || severity === activeSeverity); const matchesSeverity = (activeSeverity === 'all' || severity === activeSeverity);
const matchesSearch = (!searchVal || rule.includes(searchVal) || message.includes(searchVal)); const matchesSearch = (!searchVal || rule.includes(searchVal) || message.includes(searchVal));
const matchesPage = (pageVal === 'all' || page === pageVal);
if (matchesSeverity && matchesSearch) { if (matchesSeverity && matchesSearch && matchesPage) {
card.style.display = ''; card.style.display = '';
visibleCount++; visibleCount++;
} else { } else {