Git Komut Rehberi & Terminal Asistanı
Sıfırdan Yeni Bir Depo Başlatın:
Mevcut Bir Git Deposu veya Klasörü Gönderin:
Hataları Geri Alma, İptal & Temizlik:
Son commit'i geri al (yapılan değişiklikleri korur):
git reset --soft HEAD~1
Yapılan yerel değişiklikleri iptal et (çalışma ağacını temizler):
git restore .
Takip edilmeyen gereksiz dosyaları sil:
git clean -fd
Değişiklikleri geçici sakla ve geri getir (stash):
git stash git stash pop
Dal (Branch) ve Sürüm Etiketleri:
Yeni dal aç ve geçiş yap:
git checkout -b yeni-ozellik git push -u origin yeni-ozellik
Sürüm etiketi (tag) oluştur ve gönder:
git tag -a v1.0.0 -m "Sürüm 1.0.0" git push origin --tags
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84c6521195 | ||
|
|
0134482c54 | ||
|
|
c9ffbdba08 | ||
|
|
2a97138fec | ||
|
|
c42ab714ff | ||
|
|
b805741ef2 |
+4
-1
@@ -12,6 +12,7 @@
|
||||
"yaknet"
|
||||
],
|
||||
"homepage": "https://forum.yak.net.tr",
|
||||
"version": "2.9.2",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
@@ -30,7 +31,9 @@
|
||||
"symfony/yaml": "^6.4|^7.0",
|
||||
"monolog/monolog": "^3.0",
|
||||
"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": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
|
||||
+13
-1
@@ -28,7 +28,7 @@
|
||||
<div role="cool-widget" aria-controls="non-existent-popup">WAI-ARIA elements</div>
|
||||
|
||||
<!-- Missing Iframe Title -->
|
||||
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315"></iframe>
|
||||
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" width="560" height="315"></iframe>
|
||||
|
||||
<!-- New WCAG Rule Violations for testing -->
|
||||
|
||||
@@ -49,6 +49,18 @@
|
||||
</div>
|
||||
<a href="#" aria-hidden="true">Hidden link</a>
|
||||
|
||||
<!-- Empty heading violation -->
|
||||
<h2></h2>
|
||||
|
||||
<!-- Video missing track violation -->
|
||||
<video src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" width="320" height="240" controls></video>
|
||||
|
||||
<!-- Empty label violation -->
|
||||
<label for="search-input"></label>
|
||||
|
||||
<!-- AriaFocusable violation -->
|
||||
<div role="button">Clickable div button without tabindex</div>
|
||||
|
||||
<p>This page has multiple accessibility issues.</p>
|
||||
</body>
|
||||
</html>
|
||||
+10
-19
@@ -1,19 +1,11 @@
|
||||
<?php
|
||||
|
||||
// 1. Setup Autoloading (Mocking Composer)
|
||||
require_once __DIR__ . '/../src/Scanner.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';
|
||||
// 1. Setup Autoloading
|
||||
require_once __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use YakNet\AccessibilityConsole\Scanner;
|
||||
use YakNet\AccessibilityConsole\SourceLocator;
|
||||
use YakNet\AccessibilityConsole\Reporter\HtmlReporter;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Source\SourceLocator;
|
||||
use YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter;
|
||||
use YakNet\AccessibilityConsole\Rules\ImgAltText;
|
||||
use YakNet\AccessibilityConsole\Rules\FormLabel;
|
||||
|
||||
@@ -48,19 +40,18 @@ $violations = $scanner->scan($html);
|
||||
|
||||
if (!empty($violations)) {
|
||||
// Locate Sources
|
||||
// We point it to the 'views' directory so it knows where to look for the error source
|
||||
$locator = new SourceLocator(__DIR__ . '/views');
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$location = $locator->locate($violation->snippet);
|
||||
$location = $locator->locate($violation->htmlSnippet);
|
||||
if ($location) {
|
||||
$violation->setSourceLocation($location['file'], $location['line']);
|
||||
$violation->location = $location;
|
||||
}
|
||||
}
|
||||
|
||||
// Render Report
|
||||
$reporter = new HtmlReporter();
|
||||
$reportHtml = $reporter->render($violations);
|
||||
// Render Report (Format as results mapped by page URL)
|
||||
$reporter = new HtmlDashboardReporter();
|
||||
$reportHtml = $reporter->render(['http://localhost:8080/index.php' => $violations], 'demo/index.php');
|
||||
|
||||
// Inject into body
|
||||
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
||||
|
||||
@@ -45,7 +45,7 @@ FIX: [Only the corrected HTML snippet]
|
||||
PROMPT;
|
||||
|
||||
try {
|
||||
$result = $this->client->generativeModel('gemini-2.5-flash')->generateContent($prompt);
|
||||
$result = $this->client->generativeModel('gemini-3.1-flash-lite')->generateContent($prompt);
|
||||
return trim($result->text());
|
||||
} catch (\Throwable $e) {
|
||||
return "AI Düzeltme Hatası: " . $e->getMessage();
|
||||
|
||||
@@ -12,6 +12,7 @@ use YakNet\AccessibilityConsole\AI\GeminiFixer;
|
||||
use YakNet\AccessibilityConsole\Core\Config;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\StandardRuleSet;
|
||||
use YakNet\AccessibilityConsole\Rules\RuleLevels;
|
||||
use YakNet\AccessibilityConsole\Source\SourceLocator;
|
||||
|
||||
class ScanCommand extends Command
|
||||
@@ -26,7 +27,11 @@ class ScanCommand extends Command
|
||||
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
|
||||
->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('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')
|
||||
->addOption('level', 'l', InputOption::VALUE_REQUIRED, 'Accessibility scan level (1-9)', '4');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
@@ -44,75 +49,205 @@ class ScanCommand extends Command
|
||||
$projectPath = '.';
|
||||
}
|
||||
|
||||
$io->title("Accessibility Console - Scanning: $target");
|
||||
|
||||
// 1. Fetch HTML
|
||||
$html = @file_get_contents($target);
|
||||
if ($html === false || $html === '') {
|
||||
$io->error("Could not read target: $target");
|
||||
$levelOption = $input->getOption('level');
|
||||
$level = is_numeric($levelOption) ? (int)$levelOption : 4;
|
||||
if ($level < 1 || $level > 9) {
|
||||
$io->error("Scan level must be between 1 and 9.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 2. Setup Scanner
|
||||
$io->title("Accessibility Console - Scanning: $target (Level $level)");
|
||||
|
||||
// 1. Setup Scanner
|
||||
$scanner = new Scanner();
|
||||
foreach (StandardRuleSet::all() as $rule) {
|
||||
$rules = RuleLevels::getRulesForLevel($level);
|
||||
foreach ($rules as $rule) {
|
||||
$scanner->addRule($rule);
|
||||
}
|
||||
|
||||
// 3. Scan
|
||||
$violations = $scanner->scan($html);
|
||||
// 2. Fetch and Scan
|
||||
$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!");
|
||||
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);
|
||||
$config = new Config();
|
||||
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
|
||||
|
||||
// Show Snippet
|
||||
$io->writeln(" <fg=gray>Snippet:</> <fg=yellow>" . htmlspecialchars($violation->htmlSnippet) . "</>");
|
||||
|
||||
// Source Mapping
|
||||
$location = $locator->locate($violation->htmlSnippet);
|
||||
if ($location) {
|
||||
$io->writeln(" <fg=gray>Source:</> <fg=cyan>{$location['file']}:{$location['line']}</>");
|
||||
foreach ($results as $url => $violations) {
|
||||
if (empty($violations)) {
|
||||
continue;
|
||||
}
|
||||
$io->section("Page: $url (" . count($violations) . " violations)");
|
||||
|
||||
// AI Fixer
|
||||
if ($enableAi && $fixer) {
|
||||
$io->write(" <fg=gray>AI Fix Suggestion:</> ");
|
||||
$suggestion = $fixer->suggestFix($violation);
|
||||
if ($suggestion) {
|
||||
$violation->fixSuggestion = $suggestion;
|
||||
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
|
||||
} else {
|
||||
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
|
||||
foreach ($violations as $violation) {
|
||||
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
|
||||
|
||||
// Show Snippet
|
||||
$io->writeln(" <fg=gray>Snippet:</> <fg=yellow>" . htmlspecialchars($violation->htmlSnippet) . "</>");
|
||||
|
||||
// Source Mapping
|
||||
$location = $locator->locate($violation->htmlSnippet);
|
||||
if ($location) {
|
||||
$io->writeln(" <fg=gray>Source:</> <fg=cyan>{$location['file']}:{$location['line']}</>");
|
||||
}
|
||||
}
|
||||
|
||||
$io->writeln("");
|
||||
// AI Fixer
|
||||
if ($enableAi && $fixer) {
|
||||
$io->write(" <fg=gray>AI Fix Suggestion:</> ");
|
||||
$suggestion = $fixer->suggestFix($violation);
|
||||
if ($suggestion) {
|
||||
$violation->fixSuggestion = $suggestion;
|
||||
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
|
||||
} else {
|
||||
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
|
||||
}
|
||||
}
|
||||
|
||||
$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');
|
||||
if (is_string($reportPath) && $reportPath !== '') {
|
||||
$extension = pathinfo($reportPath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'json') {
|
||||
$data = array_map(fn($v) => $v->toArray(), $violations);
|
||||
file_put_contents($reportPath, json_encode($data, JSON_PRETTY_PRINT));
|
||||
$serialized = [];
|
||||
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 {
|
||||
$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");
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ abstract class AbstractRule
|
||||
*/
|
||||
abstract public function getSeverity(): Severity;
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
protected function createViolation(string $message, \DOMElement $element): Violation
|
||||
{
|
||||
$doc = $element->ownerDocument;
|
||||
|
||||
@@ -28,9 +28,10 @@ class Scanner
|
||||
return [];
|
||||
}
|
||||
|
||||
$doc = new \DOMDocument();
|
||||
// Suppress warnings for malformed HTML
|
||||
@$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
|
||||
$html5 = new \Masterminds\HTML5([
|
||||
'disable_html_ns' => true,
|
||||
]);
|
||||
$doc = $html5->loadHTML($html);
|
||||
|
||||
return $this->engine->run($doc);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -14,6 +14,11 @@ abstract class AbstractRule implements RuleInterface
|
||||
abstract public function getStandard(): WCAGStandard;
|
||||
abstract public function getSeverity(): Severity;
|
||||
|
||||
public function getLevel(): int
|
||||
{
|
||||
return 4;
|
||||
}
|
||||
|
||||
protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation
|
||||
{
|
||||
$doc = $element->ownerDocument;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaFocusable extends AbstractRule
|
||||
{
|
||||
/** @var array<int, string> */
|
||||
private static array $interactiveRoles = [
|
||||
'button', 'link', 'checkbox', 'menuitem', 'radio', 'textbox',
|
||||
'searchbox', 'slider', 'spinbutton', 'switch', 'tab'
|
||||
];
|
||||
|
||||
/** @var array<int, string> */
|
||||
private static array $nativeFocusable = [
|
||||
'a', 'button', 'input', 'select', 'textarea', 'iframe'
|
||||
];
|
||||
|
||||
public function getId(): string { return 'WCAG_2_1_1_ARIA_FOCUSABLE'; }
|
||||
public function getDescription(): string { return 'Elements with interactive ARIA roles must be focusable (e.g. have a tabindex attribute).'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('role')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$role = strtolower(trim($element->getAttribute('role')));
|
||||
if (!in_array($role, self::$interactiveRoles, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tagName = strtolower($element->tagName);
|
||||
if (in_array($tagName, self::$nativeFocusable, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('tabindex')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add tabindex="0" to make the element focusable via keyboard navigation.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class AriaHiddenFocusable extends AbstractRule
|
||||
public function getDescription(): string { return 'Focusable elements must not be hidden with aria-hidden="true".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 5; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -78,4 +78,5 @@ class AriaRole extends AbstractRule
|
||||
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 3; }
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class ButtonName extends AbstractRule
|
||||
public function getDescription(): string { return 'Buttons must have discernible text.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class ColorContrast extends AbstractRule
|
||||
public function getDescription(): string { return 'Text must have sufficient contrast against the background (4.5:1 minimum).'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 5; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -48,4 +48,5 @@ class DuplicateId extends AbstractRule
|
||||
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 3; }
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class EmptyHeading extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_EMPTY_HEADING'; }
|
||||
public function getDescription(): string { return 'Heading elements (h1-h6) must have discernible text content.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!preg_match('/^h([1-6])$/i', $element->tagName)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim($element->textContent);
|
||||
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add descriptive text or an aria-label to the heading element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class EmptyLabel extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_EMPTY_LABEL'; }
|
||||
public function getDescription(): string { return 'Label elements must have discernible text content.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'label') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim($element->textContent);
|
||||
if ($text === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add text content to the label element to describe its associated input.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class EmptyLink extends AbstractRule
|
||||
public function getDescription(): string { return 'Links must have discernible text.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class FieldsetLegend extends AbstractRule
|
||||
public function getDescription(): string { return 'Fieldsets should have a legend.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class FormLabel extends AbstractRule
|
||||
public function getDescription(): string { return 'Form inputs must have an associated label.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ class HeadingOrder extends AbstractRule
|
||||
public function getDescription(): string { return 'Headings should follow a logical nesting order.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class HtmlHasLang extends AbstractRule
|
||||
public function getDescription(): string { return 'The html element must have a lang attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class IframeTitle extends AbstractRule
|
||||
public function getDescription(): string { return 'Iframe elements must have a non-empty title attribute to describe their content.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class ImgAltText extends AbstractRule
|
||||
public function getDescription(): string { return 'Images must have an alt attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -13,6 +13,7 @@ class PageTitle extends AbstractRule
|
||||
public function getDescription(): string { return 'Web pages must have a non-empty title element in the head.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 1; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
class RuleLevels
|
||||
{
|
||||
/**
|
||||
* Get rule instances for the specified level.
|
||||
* Levels are incremental: Level 5 includes all rules from Levels 1, 2, 3, and 4.
|
||||
*
|
||||
* @param int $level
|
||||
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
|
||||
*/
|
||||
public static function getRulesForLevel(int $level): array
|
||||
{
|
||||
$rules = [];
|
||||
$files = glob(__DIR__ . '/*.php');
|
||||
if ($files === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = pathinfo($file, PATHINFO_FILENAME);
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $className;
|
||||
|
||||
if (class_exists($fqcn)) {
|
||||
$reflection = new \ReflectionClass($fqcn);
|
||||
|
||||
if ($reflection->isAbstract() || $reflection->isInterface() || $reflection->isTrait()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if it is a concrete rule
|
||||
$isRule = $reflection->isSubclassOf(\YakNet\AccessibilityConsole\Core\AbstractRule::class) ||
|
||||
$reflection->implementsInterface(\YakNet\AccessibilityConsole\Rules\RuleInterface::class);
|
||||
|
||||
if ($isRule) {
|
||||
// Exclude Turkish custom rules to match standard behavior
|
||||
if (in_array($className, ['EmptyLinkRule', 'HeadingOrderRule', 'ImageAltRule'], true)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$instance = $reflection->newInstance();
|
||||
|
||||
// Check level
|
||||
$ruleLevel = 4; // default
|
||||
if (method_exists($instance, 'getLevel')) {
|
||||
/** @var mixed $levelVal */
|
||||
$levelVal = $instance->getLevel();
|
||||
if (is_int($levelVal)) {
|
||||
$ruleLevel = $levelVal;
|
||||
}
|
||||
}
|
||||
|
||||
if ($ruleLevel <= $level) {
|
||||
/** @var \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule $instance */
|
||||
$rules[] = $instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sort rules by class name to ensure deterministic order
|
||||
usort($rules, function ($a, $b) {
|
||||
return strcmp(get_class($a), get_class($b));
|
||||
});
|
||||
|
||||
return $rules;
|
||||
}
|
||||
}
|
||||
@@ -5,31 +5,10 @@ namespace YakNet\AccessibilityConsole\Rules;
|
||||
class StandardRuleSet
|
||||
{
|
||||
/**
|
||||
* @return RuleInterface[]
|
||||
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
/** @var RuleInterface[] $rules */
|
||||
$rules = [
|
||||
new HtmlHasLang(),
|
||||
new ImgAltText(),
|
||||
new EmptyLink(),
|
||||
new ButtonName(),
|
||||
new FormLabel(),
|
||||
new FieldsetLegend(),
|
||||
new HeadingOrder(),
|
||||
new MetaViewport(),
|
||||
new TabindexOrder(),
|
||||
new ColorContrast(),
|
||||
new DuplicateId(),
|
||||
new AriaRole(),
|
||||
new IframeTitle(),
|
||||
new LinkTextGeneric(),
|
||||
new PlaceholderAsLabel(),
|
||||
new ImageAltRedundant(),
|
||||
new AriaHiddenFocusable(),
|
||||
new PageTitle(),
|
||||
];
|
||||
return $rules;
|
||||
return RuleLevels::getRulesForLevel(5);
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,7 @@ class TabindexOrder extends AbstractRule
|
||||
public function getDescription(): string { return 'Avoid positive tabindex values to maintain natural keyboard focus order.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class VideoTrack extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_2_2_VIDEO_TRACK'; }
|
||||
public function getDescription(): string { return 'Video elements must contain at least one track element for captions or subtitles.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 2; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'video') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there is a track element inside
|
||||
$tracks = $element->getElementsByTagName('track');
|
||||
if ($tracks->length === 0) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a <track kind="captions"> or <track kind="subtitles"> element inside the video element.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+1866
-75
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user