Compare commits

...
3 Commits
11 changed files with 169 additions and 23 deletions
+14 -6
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
@@ -37,7 +45,8 @@ If it's about ARIA roles, add the correct ones.
PROMPT; PROMPT;
try { try {
$result = $this->client->generativeModel('gemini-2.5-flash')->generateContent($prompt); // Stable model: gemini-1.5-flash
$result = $this->client->generativeModel('gemini-1.5-flash')->generateContent($prompt);
$suggestion = trim($result->text()); $suggestion = trim($result->text());
// Clean up if AI included markdown blocks anyway // Clean up if AI included markdown blocks anyway
@@ -45,8 +54,7 @@ PROMPT;
return $suggestion; return $suggestion;
} catch (\Throwable $e) { } catch (\Throwable $e) {
// Log error if needed return "AI Düzeltme Hatası: " . $e->getMessage();
return null;
} }
} }
} }
+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)</>");
+7 -6
View File
@@ -10,22 +10,23 @@ abstract class AbstractRule
abstract public function check(\DOMDocument $doc): array; abstract public function check(\DOMDocument $doc): array;
/** /**
* Get the WCAG standard ID (e.g., WCAG 2.1 1.1.1) * Get the WCAG standard Enum
*/ */
abstract public function getStandardId(): string; abstract public function getStandard(): WCAGStandard;
/** /**
* Get the severity level (Critical, Major, Minor) * Get the severity level Enum
*/ */
abstract public function getSeverity(): string; abstract public function getSeverity(): Severity;
protected function createViolation(string $message, \DOMElement $element): Violation protected function createViolation(string $message, \DOMElement $element): Violation
{ {
return new Violation( return new Violation(
ruleId: (new \ReflectionClass($this))->getShortName(),
message: $message, message: $message,
htmlSnippet: $element->ownerDocument->saveHTML($element),
severity: $this->getSeverity(), severity: $this->getSeverity(),
standard: $this->getStandardId() standard: $this->getStandard(),
htmlSnippet: $element->ownerDocument->saveHTML($element)
); );
} }
} }
+22 -2
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) {
if (method_exists($rule, 'check')) {
// 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); $violations = $rule->check($doc);
$allViolations = array_merge($allViolations, $violations); $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,
+4
View File
@@ -22,6 +22,7 @@ class HtmlDashboardReporter
<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>" . ($v->fixSuggestion ? "<pre class='ai-fix'><code>" . htmlspecialchars($v->fixSuggestion) . "</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 +44,10 @@ 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-fix { border-left: 4px solid #2ecc71; background: #1e1e1e; }
.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 +72,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>
+4 -2
View File
@@ -3,6 +3,8 @@
namespace YakNet\AccessibilityConsole\Rules; namespace YakNet\AccessibilityConsole\Rules;
use YakNet\AccessibilityConsole\Core\AbstractRule; use YakNet\AccessibilityConsole\Core\AbstractRule;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class EmptyLinkRule extends AbstractRule class EmptyLinkRule extends AbstractRule
{ {
@@ -22,6 +24,6 @@ class EmptyLinkRule extends AbstractRule
return $violations; return $violations;
} }
public function getStandardId(): string { return 'WCAG 2.1 2.4.4'; } public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): string { return 'Critical'; } public function getSeverity(): Severity { return Severity::CRITICAL; }
} }
+4 -2
View File
@@ -3,6 +3,8 @@
namespace YakNet\AccessibilityConsole\Rules; namespace YakNet\AccessibilityConsole\Rules;
use YakNet\AccessibilityConsole\Core\AbstractRule; use YakNet\AccessibilityConsole\Core\AbstractRule;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class HeadingOrderRule extends AbstractRule class HeadingOrderRule extends AbstractRule
{ {
@@ -26,6 +28,6 @@ class HeadingOrderRule extends AbstractRule
return $violations; return $violations;
} }
public function getStandardId(): string { return 'WCAG 2.1 1.3.1'; } public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): string { return 'Major'; } public function getSeverity(): Severity { return Severity::WARNING; }
} }
+4 -2
View File
@@ -3,6 +3,8 @@
namespace YakNet\AccessibilityConsole\Rules; namespace YakNet\AccessibilityConsole\Rules;
use YakNet\AccessibilityConsole\Core\AbstractRule; use YakNet\AccessibilityConsole\Core\AbstractRule;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class ImageAltRule extends AbstractRule class ImageAltRule extends AbstractRule
{ {
@@ -22,6 +24,6 @@ class ImageAltRule extends AbstractRule
return $violations; return $violations;
} }
public function getStandardId(): string { return 'WCAG 2.1 1.1.1'; } public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): string { return 'Critical'; } public function getSeverity(): Severity { return Severity::CRITICAL; }
} }
+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>