diff --git a/.gitignore b/.gitignore
index 9805767..ebc698b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
/rapor.html
/.phpunit.result.cache
/phpstan-baseline.neon
+/.phpunit.cache/
+
diff --git a/bin/a11y b/bin/a11y
index 4ce6734..112780e 100644
--- a/bin/a11y
+++ b/bin/a11y
@@ -11,17 +11,19 @@ if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
}
use Symfony\Component\Console\Application;
+use YakNet\AccessibilityConsole\Core\Container\ContainerFactory;
use YakNet\AccessibilityConsole\Console\Command\ScanCommand;
use YakNet\AccessibilityConsole\Console\Command\FixCommand;
use YakNet\AccessibilityConsole\Console\Command\InitCommand;
use YakNet\AccessibilityConsole\Console\Command\RulesCommand;
-$application = new Application('YakNet Accessibility Console', '3.0.1');
+$container = ContainerFactory::create();
+$application = new Application('YakNet Accessibility Console', '3.1.0');
-// Add commands
-$application->addCommand(new ScanCommand());
-$application->addCommand(new FixCommand());
-$application->addCommand(new InitCommand());
-$application->addCommand(new RulesCommand());
+// Add commands resolved via DI Container
+$application->add($container->get(ScanCommand::class));
+$application->add($container->get(FixCommand::class));
+$application->add($container->get(InitCommand::class));
+$application->add($container->get(RulesCommand::class));
$application->run();
diff --git a/composer.json b/composer.json
index d5ae740..a5fb7e3 100644
--- a/composer.json
+++ b/composer.json
@@ -12,7 +12,7 @@
"yaknet"
],
"homepage": "https://forum.yak.net.tr",
- "version": "3.0.5",
+ "version": "3.1.0",
"type": "library",
"license": "MIT",
"authors": [
@@ -35,13 +35,20 @@
"guzzlehttp/guzzle": "^7.8",
"masterminds/html5": "^2.8",
"sabberworm/php-css-parser": "^9.4",
- "chrome-php/chrome": "^1.15"
+ "chrome-php/chrome": "^1.15",
+ "sebastian/diff": "^5.1",
+ "php-di/php-di": "^7.1",
+ "symfony/css-selector": "^8.1",
+ "symfony/dom-crawler": "^8.1"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
"phpstan/phpstan": "^1.10"
},
"autoload": {
+ "files": [
+ "src/accessibility-console.php"
+ ],
"psr-4": {
"YakNet\\AccessibilityConsole\\": "src/"
}
@@ -57,4 +64,3 @@
}
}
}
-
diff --git a/demo/test_local_fix.html b/demo/test_local_fix.html
new file mode 100644
index 0000000..a18986d
--- /dev/null
+++ b/demo/test_local_fix.html
@@ -0,0 +1,10 @@
+
+
+
+ Test Local Fix
+
+
+ Hello blink
+
+
+
diff --git a/phpunit.xml b/phpunit.xml
new file mode 100644
index 0000000..a0d4146
--- /dev/null
+++ b/phpunit.xml
@@ -0,0 +1,18 @@
+
+
+
+
+ tests
+
+
+
diff --git a/src/AI/AiFixerManager.php b/src/AI/AiFixerManager.php
new file mode 100644
index 0000000..9b86d1a
--- /dev/null
+++ b/src/AI/AiFixerManager.php
@@ -0,0 +1,73 @@
+ */
+ private array $providers = [];
+ private ?string $activeProviderName = null;
+
+ public function __construct(?string $geminiApiKey = null, ?string $openAiApiKey = null, ?string $claudeApiKey = null)
+ {
+ // Register default providers
+ if ($geminiApiKey) {
+ $this->registerProvider('gemini', new GeminiProvider($geminiApiKey));
+ $this->activeProviderName = 'gemini';
+ }
+ if ($openAiApiKey) {
+ $this->registerProvider('openai', new OpenAiProvider($openAiApiKey));
+ if (!$this->activeProviderName) {
+ $this->activeProviderName = 'openai';
+ }
+ }
+ if ($claudeApiKey) {
+ $this->registerProvider('claude', new ClaudeProvider($claudeApiKey));
+ if (!$this->activeProviderName) {
+ $this->activeProviderName = 'claude';
+ }
+ }
+
+ // Always register mock provider as fallback/testing option
+ $this->registerProvider('mock', new MockAiProvider());
+ if (!$this->activeProviderName) {
+ $this->activeProviderName = 'mock';
+ }
+ }
+
+ public function registerProvider(string $name, AiProviderInterface $provider): void
+ {
+ $this->providers[strtolower($name)] = $provider;
+ }
+
+ public function setActiveProvider(string $name): void
+ {
+ $key = strtolower($name);
+ if (isset($this->providers[$key])) {
+ $this->activeProviderName = $key;
+ }
+ }
+
+ public function getActiveProvider(): ?AiProviderInterface
+ {
+ if ($this->activeProviderName && isset($this->providers[$this->activeProviderName])) {
+ return $this->providers[$this->activeProviderName];
+ }
+ return null;
+ }
+
+ public function suggestFix(Violation $violation): ?string
+ {
+ $provider = $this->getActiveProvider();
+ if ($provider) {
+ return $provider->suggestFix($violation);
+ }
+ return "No active AI provider configured.";
+ }
+}
diff --git a/src/AI/AiProviderInterface.php b/src/AI/AiProviderInterface.php
new file mode 100644
index 0000000..aaf1d0c
--- /dev/null
+++ b/src/AI/AiProviderInterface.php
@@ -0,0 +1,23 @@
+htmlSnippet}
+Error Message: {$violation->message}
+Rule ID: {$violation->ruleId}
+Severity: {$violation->severity->value}
+Standard: {$violation->standard->value}
+
+Please provide a short explanation of the fix in Turkish and the fixed version of the HTML snippet.
+Use the following EXACT format:
+EXPLANATION: [A short explanation of what you fixed and why, in Turkish]
+FIX: [Only the corrected HTML snippet, no surrounding explanation]
+PROMPT;
+ }
+}
diff --git a/src/AI/Providers/ClaudeProvider.php b/src/AI/Providers/ClaudeProvider.php
new file mode 100644
index 0000000..b9a15e3
--- /dev/null
+++ b/src/AI/Providers/ClaudeProvider.php
@@ -0,0 +1,62 @@
+apiKey) {
+ $this->client = new GuzzleClient([
+ 'base_uri' => 'https://api.anthropic.com/v1/',
+ 'headers' => [
+ 'x-api-key' => $this->apiKey,
+ 'anthropic-version' => '2023-06-01',
+ 'Content-Type' => 'application/json',
+ ],
+ 'timeout' => 10.0,
+ ]);
+ }
+ }
+
+ public function getName(): string
+ {
+ return 'Anthropic Claude';
+ }
+
+ public function suggestFix(Violation $violation): ?string
+ {
+ if (!$this->client) {
+ return null;
+ }
+
+ $prompt = PromptBuilder::build($violation);
+
+ try {
+ $response = $this->client->post('messages', [
+ 'json' => [
+ 'model' => 'claude-3-5-haiku-20241022',
+ 'max_tokens' => 1000,
+ 'messages' => [
+ ['role' => 'user', 'content' => $prompt]
+ ],
+ ],
+ ]);
+
+ $data = json_decode($response->getBody()->getContents(), true);
+ if (is_array($data) && isset($data['content'][0]['text']) && is_string($data['content'][0]['text'])) {
+ return $data['content'][0]['text'];
+ }
+ return null;
+ } catch (\Throwable $e) {
+ return "Claude Error: " . $e->getMessage();
+ }
+ }
+}
diff --git a/src/AI/Providers/GeminiProvider.php b/src/AI/Providers/GeminiProvider.php
new file mode 100644
index 0000000..0520e5e
--- /dev/null
+++ b/src/AI/Providers/GeminiProvider.php
@@ -0,0 +1,45 @@
+apiKey) {
+ try {
+ $this->client = \Gemini::client($this->apiKey);
+ } catch (\Throwable) {
+ $this->client = null;
+ }
+ }
+ }
+
+ public function getName(): string
+ {
+ return 'Google Gemini';
+ }
+
+ public function suggestFix(Violation $violation): ?string
+ {
+ if (!$this->apiKey || !$this->client) {
+ return null;
+ }
+
+ $prompt = PromptBuilder::build($violation);
+
+ try {
+ $result = $this->client->generativeModel('gemini-3.1-flash-lite')->generateContent($prompt);
+ return trim($result->text());
+ } catch (\Throwable $e) {
+ return "Gemini Error: " . $e->getMessage();
+ }
+ }
+}
diff --git a/src/AI/Providers/MockAiProvider.php b/src/AI/Providers/MockAiProvider.php
new file mode 100644
index 0000000..700d956
--- /dev/null
+++ b/src/AI/Providers/MockAiProvider.php
@@ -0,0 +1,49 @@
+htmlSnippet;
+ $fixedSnippet = $snippet;
+
+ // Simple mock fixes based on rule ID
+ if ($violation->ruleId === 'WCAG_2_2_2_BLINK') {
+ $fixedSnippet = str_replace([''], ['', ''], $snippet);
+ } elseif ($violation->ruleId === 'WCAG_1_3_1_DUPLICATE_H1') {
+ $fixedSnippet = str_replace(['', '
'], ['', '
'], $snippet);
+ } elseif ($violation->ruleId === 'WCAG_1_3_5_AUTOCOMPLETE') {
+ $fixedSnippet = str_replace('ruleId === 'WCAG_2_1_1_SCROLLABLE_FOCUS') {
+ $fixedSnippet = str_replace('ruleId === 'WCAG_4_1_2_ARIA_LABELLEDBY') {
+ $fixedSnippet = str_replace('aria-labelledby="non_existent_label"', 'aria-labelledby="real_label"', $snippet);
+ } elseif ($violation->ruleId === 'WCAG_1_1_1_ALT_PLACEHOLDER') {
+ if (str_contains($snippet, 'alt="logo"')) {
+ $fixedSnippet = str_replace('alt="logo"', 'alt="YakNet Accessibility Console Company Logo"', $snippet);
+ } else {
+ $fixedSnippet = str_replace('alt="header_banner.jpg"', 'alt="YakNet Accessibility Console Marketing Banner"', $snippet);
+ }
+ } elseif ($violation->ruleId === 'WCAG_1_1_1_SVG') {
+ $fixedSnippet = str_replace('