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('