Compare commits

...
2 Commits
8 changed files with 181 additions and 21 deletions
+16 -13
View File
@@ -11,15 +11,23 @@ class GeminiFixer
public function __construct(private readonly ?string $apiKey) public function __construct(private readonly ?string $apiKey)
{ {
if ($this->apiKey && class_exists(Gemini::class)) { if ($this->apiKey) {
$this->client = Gemini::client($this->apiKey); try {
$this->client = \Gemini::client($this->apiKey);
} catch (\Throwable) {
$this->client = null;
}
} }
} }
public function suggestFix(Violation $violation): ?string public function suggestFix(Violation $violation): ?string
{ {
if (!$this->apiKey) {
return "Hata: GEMINI_API_KEY bulunamadı.";
}
if (!$this->client) { if (!$this->client) {
return null; return "Hata: Gemini istemcisi başlatılamadı. Kütüphane eksik olabilir.";
} }
$prompt = <<<PROMPT $prompt = <<<PROMPT
@@ -30,20 +38,15 @@ Error: {$violation->message}
Severity: {$violation->severity->value} Severity: {$violation->severity->value}
Standard: {$violation->standard->value} Standard: {$violation->standard->value}
Please provide ONLY the fixed version of the HTML snippet. Please provide a short explanation of the fix (in Turkish) and the fixed version of the HTML snippet.
Do not explain, do not add markdown code blocks, just return the corrected HTML. Use the following EXACT format:
If the error is about missing 'alt' text, add a meaningful alt text based on context. EXPLANATION: [A short explanation of what you fixed and why]
If it's about ARIA roles, add the correct ones. FIX: [Only the corrected HTML snippet]
PROMPT; PROMPT;
try { try {
$result = $this->client->generativeModel('gemini-2.5-flash')->generateContent($prompt); $result = $this->client->generativeModel('gemini-2.5-flash')->generateContent($prompt);
$suggestion = trim($result->text()); return trim($result->text());
// Clean up if AI included markdown blocks anyway
$suggestion = preg_replace('/^```html\s*|\s*```$/i', '', $suggestion);
return $suggestion;
} catch (\Throwable $e) { } catch (\Throwable $e) {
return "AI Düzeltme Hatası: " . $e->getMessage(); return "AI Düzeltme Hatası: " . $e->getMessage();
} }
+11 -3
View File
@@ -79,9 +79,17 @@ class FixCommand extends Command
} }
// Get AI Suggestion // Get AI Suggestion
$suggestion = $fixer->suggestFix($violation); $rawSuggestion = $fixer->suggestFix($violation);
if (!$suggestion) { $suggestion = $rawSuggestion;
$io->writeln("<fg=red>Skipped (AI could not generate fix)</>"); if ($rawSuggestion && str_contains($rawSuggestion, 'FIX:')) {
preg_match('/FIX:(.*)/s', $rawSuggestion, $matches);
$suggestion = trim($matches[1] ?? $rawSuggestion);
// Clean up potential markdown
$suggestion = preg_replace('/^```html\s*|\s*```$/i', '', $suggestion);
}
if (!$suggestion || ($suggestion === $rawSuggestion && str_contains($suggestion, 'EXPLANATION:'))) {
$io->writeln("<fg=red>Skipped (AI could not generate a clean fix)</>");
continue; continue;
} }
+1
View File
@@ -83,6 +83,7 @@ class ScanCommand extends Command
$io->write(" <fg=gray>AI Fix Suggestion:</> "); $io->write(" <fg=gray>AI Fix Suggestion:</> ");
$suggestion = $fixer->suggestFix($violation); $suggestion = $fixer->suggestFix($violation);
if ($suggestion) { if ($suggestion) {
$violation->fixSuggestion = $suggestion;
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>"); $io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
} else { } else {
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>"); $io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
+24 -4
View File
@@ -4,10 +4,10 @@ namespace YakNet\AccessibilityConsole\Core;
class RuleEngine class RuleEngine
{ {
/** @var AbstractRule[] */ /** @var array */
private array $rules = []; private array $rules = [];
public function addRule(AbstractRule $rule): void public function addRule(mixed $rule): void
{ {
$this->rules[] = $rule; $this->rules[] = $rule;
} }
@@ -19,8 +19,28 @@ class RuleEngine
{ {
$allViolations = []; $allViolations = [];
foreach ($this->rules as $rule) { foreach ($this->rules as $rule) {
$violations = $rule->check($doc); if (method_exists($rule, 'check')) {
$allViolations = array_merge($allViolations, $violations); // Determine if it's a document-level rule or element-level rule
$reflection = new \ReflectionMethod($rule, 'check');
$params = $reflection->getParameters();
if (isset($params[0]) && $params[0]->getType() && $params[0]->getType()->getName() === 'DOMDocument') {
// Document-level rule (Core\AbstractRule style)
$violations = $rule->check($doc);
$allViolations = array_merge($allViolations, $violations);
} else {
// Element-level rule (Rules\RuleInterface style)
// We need to iterate over all elements
$xpath = new \DOMXPath($doc);
$elements = $xpath->query('//*');
foreach ($elements as $element) {
$violation = $rule->check($element);
if ($violation) {
$allViolations[] = $violation;
}
}
}
}
} }
return $allViolations; return $allViolations;
} }
+5
View File
@@ -11,6 +11,11 @@ class Scanner
} }
} }
public function addRule(mixed $rule): void
{
$this->engine->addRule($rule);
}
/** /**
* Scan HTML content for accessibility violations. * Scan HTML content for accessibility violations.
* *
+1 -1
View File
@@ -2,7 +2,7 @@
namespace YakNet\AccessibilityConsole\Core; namespace YakNet\AccessibilityConsole\Core;
readonly class Violation class Violation
{ {
public function __construct( public function __construct(
public string $ruleId, public string $ruleId,
+22
View File
@@ -17,11 +17,29 @@ class HtmlDashboardReporter
$rows = ''; $rows = '';
foreach ($violations as $v) { foreach ($violations as $v) {
$severityClass = strtolower($v->severity->value); $severityClass = strtolower($v->severity->value);
$aiSuggestion = $v->fixSuggestion;
$explanation = '';
$fixedCode = '';
if ($aiSuggestion && str_contains($aiSuggestion, 'FIX:')) {
preg_match('/EXPLANATION:(.*)FIX:(.*)/s', $aiSuggestion, $matches);
$explanation = trim($matches[1] ?? '');
$fixedCode = trim($matches[2] ?? '');
// Clean up potential markdown
$fixedCode = preg_replace('/^```html\s*|\s*```$/i', '', $fixedCode);
} else {
$fixedCode = $aiSuggestion;
}
$rows .= " $rows .= "
<tr class='severity-{$severityClass}'> <tr class='severity-{$severityClass}'>
<td><span class='badge'>{$v->ruleId}</span></td> <td><span class='badge'>{$v->ruleId}</span></td>
<td>{$v->message}</td> <td>{$v->message}</td>
<td><pre><code>" . htmlspecialchars($v->htmlSnippet) . "</code></pre></td> <td><pre><code>" . htmlspecialchars($v->htmlSnippet) . "</code></pre></td>
<td>
" . ($explanation ? "<div class='ai-explanation'><strong>Öneri:</strong> {$explanation}</div>" : "") . "
" . ($fixedCode ? "<pre class='ai-fix'><code>" . htmlspecialchars($fixedCode) . "</code></pre>" : "<span class='text-muted'>Manual fix required</span>") . "
</td>
<td>" . ($v->location ? "{$v->location['file']}:{$v->location['line']}" : 'N/A') . "</td> <td>" . ($v->location ? "{$v->location['file']}:{$v->location['line']}" : 'N/A') . "</td>
</tr>"; </tr>";
} }
@@ -43,8 +61,11 @@ class HtmlDashboardReporter
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; } th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; } th { background: #f8f9fa; }
pre { background: #272822; color: #f8f8f2; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; } pre { background: #272822; color: #f8f8f2; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
.ai-explanation { margin-bottom: 8px; font-size: 13px; color: #27ae60; line-height: 1.4; }
.ai-fix { border-left: 4px solid #2ecc71; background: #1e1e1e; margin-top: 5px; }
.badge { background: #34495e; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; } .badge { background: #34495e; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; }
.severity-error { background: #fff5f5; } .severity-error { background: #fff5f5; }
.text-muted { color: #95a5a6; font-size: 0.9em; font-style: italic; }
</style> </style>
</head> </head>
<body> <body>
@@ -69,6 +90,7 @@ class HtmlDashboardReporter
<th>Rule ID</th> <th>Rule ID</th>
<th>Issue</th> <th>Issue</th>
<th>Code Snippet</th> <th>Code Snippet</th>
<th>AI Recommendation</th>
<th>Source Location</th> <th>Source Location</th>
</tr> </tr>
</thead> </thead>
+101
View File
@@ -0,0 +1,101 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>A11y Report - demo/broken.html</title>
<style>
body { font-family: 'Inter', sans-serif; background: #f4f7f6; color: #333; margin: 0; padding: 20px; }
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
h1 { color: #2c3e50; margin-top: 0; }
.summary { display: flex; gap: 20px; margin-bottom: 30px; }
.card { background: #3498db; color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center; }
.card.error { background: #e74c3c; }
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
th { background: #f8f9fa; }
pre { background: #272822; color: #f8f8f2; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
.ai-fix { border-left: 4px solid #2ecc71; background: #1e1e1e; }
.badge { background: #34495e; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; }
.severity-error { background: #fff5f5; }
.text-muted { color: #95a5a6; font-size: 0.9em; font-style: italic; }
</style>
</head>
<body>
<div class="container">
<h1>Accessibility Report</h1>
<p>Target: <strong>demo/broken.html</strong> | Generated: 2026-05-13 09:46:04</p>
<div class="summary">
<div class="card error">
<h2>4</h2>
<p>Violations Found</p>
</div>
<div class="card">
<h2>WCAG 2.1</h2>
<p>Standard Level</p>
</div>
</div>
<table>
<thead>
<tr>
<th>Rule ID</th>
<th>Issue</th>
<th>Code Snippet</th>
<th>AI Recommendation</th>
<th>Source Location</th>
</tr>
</thead>
<tbody>
<tr class='severity-error'>
<td><span class='badge'>WCAG_3_1_1_LANG</span></td>
<td>The html element must have a lang attribute.</td>
<td><pre><code>&lt;html&gt;
&lt;head&gt;
&lt;title&gt;Broken Page&lt;/title&gt;
&lt;/head&gt;
&lt;body&gt;
&lt;h1&gt;Welcome to a broken page&lt;/h1&gt;
&lt;!-- Missing alt attribute --&gt;
&lt;img src=&quot;logo.png&quot;&gt;
&lt;!-- Empty link --&gt;
&lt;a href=&quot;https://google.com&quot;&gt;&lt;/a&gt;
&lt;!-- Empty button --&gt;
&lt;button type=&quot;submit&quot;&gt;&lt;/button&gt;
&lt;p&gt;This page has multiple accessibility issues.&lt;/p&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre></td>
<td><pre class='ai-fix'><code>Hata: GEMINI_API_KEY bulunamadı.</code></pre></td>
<td>N/A</td>
</tr>
<tr class='severity-error'>
<td><span class='badge'>WCAG_1_1_1_ALT</span></td>
<td>Images must have an alt attribute.</td>
<td><pre><code>&lt;img src=&quot;logo.png&quot;&gt;</code></pre></td>
<td><pre class='ai-fix'><code>Hata: GEMINI_API_KEY bulunamadı.</code></pre></td>
<td>N/A</td>
</tr>
<tr class='severity-error'>
<td><span class='badge'>WCAG_2_4_4_LINK_TEXT</span></td>
<td>Links must have discernible text.</td>
<td><pre><code>&lt;a href=&quot;https://google.com&quot;&gt;&lt;/a&gt;</code></pre></td>
<td><pre class='ai-fix'><code>Hata: GEMINI_API_KEY bulunamadı.</code></pre></td>
<td>N/A</td>
</tr>
<tr class='severity-error'>
<td><span class='badge'>WCAG_4_1_2_BUTTON</span></td>
<td>Buttons must have discernible text.</td>
<td><pre><code>&lt;button type=&quot;submit&quot;&gt;&lt;/button&gt;</code></pre></td>
<td><pre class='ai-fix'><code>Hata: GEMINI_API_KEY bulunamadı.</code></pre></td>
<td>N/A</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>