diff --git a/src/Core/AbstractRule.php b/src/Core/AbstractRule.php new file mode 100644 index 0000000..990cbf7 --- /dev/null +++ b/src/Core/AbstractRule.php @@ -0,0 +1,31 @@ +ownerDocument->saveHTML($element), + severity: $this->getSeverity(), + standard: $this->getStandardId() + ); + } +} diff --git a/src/Core/RuleEngine.php b/src/Core/RuleEngine.php index 13b41d0..4d8c234 100644 --- a/src/Core/RuleEngine.php +++ b/src/Core/RuleEngine.php @@ -2,23 +2,26 @@ namespace YakNet\AccessibilityConsole\Core; -use YakNet\AccessibilityConsole\Rules\RuleInterface; - class RuleEngine { - /** @var RuleInterface[] */ + /** @var AbstractRule[] */ private array $rules = []; - public function addRule(RuleInterface $rule): void + public function addRule(AbstractRule $rule): void { $this->rules[] = $rule; } /** - * @return RuleInterface[] + * @return Violation[] */ - public function getRules(): array + public function run(\DOMDocument $doc): array { - return $this->rules; + $allViolations = []; + foreach ($this->rules as $rule) { + $violations = $rule->check($doc); + $allViolations = array_merge($allViolations, $violations); + } + return $allViolations; } } diff --git a/src/Core/Scanner.php b/src/Core/Scanner.php index f0d1eb3..ed71214 100644 --- a/src/Core/Scanner.php +++ b/src/Core/Scanner.php @@ -2,62 +2,31 @@ namespace YakNet\AccessibilityConsole\Core; -use DOMDocument; -use YakNet\AccessibilityConsole\Rules\RuleInterface; - class Scanner { - private array $rules = []; - - public function __construct( - private readonly ?RuleEngine $engine = null - ) { - if ($this->engine) { - $this->rules = $this->engine->getRules(); + public function __construct(private ?RuleEngine $engine = null) + { + if ($this->engine === null) { + $this->engine = new RuleEngine(); } } - public function addRule(RuleInterface $rule): void - { - $this->rules[] = $rule; - } - /** + * Scan HTML content for accessibility violations. + * + * @param string $html * @return Violation[] */ public function scan(string $html): array { - if (trim($html) === '') { + if (empty(trim($html))) { return []; } - $dom = new DOMDocument(); - $previousState = libxml_use_internal_errors(true); - $loaded = $dom->loadHTML('' . $html, LIBXML_NOERROR | LIBXML_NOWARNING); - libxml_clear_errors(); - libxml_use_internal_errors($previousState); + $doc = new \DOMDocument(); + // Suppress warnings for malformed HTML + @$doc->loadHTML('' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); - if (!$loaded) { - return []; - } - - $violations = []; - $elements = $dom->getElementsByTagName('*'); - - foreach ($elements as $element) { - if (!$element instanceof \DOMElement) continue; - foreach ($this->rules as $rule) { - try { - $violation = $rule->check($element); - if ($violation) { - $violations[] = $violation; - } - } catch (\Throwable) { - continue; - } - } - } - - return $violations; + return $this->engine->run($doc); } } diff --git a/src/Rules/EmptyLinkRule.php b/src/Rules/EmptyLinkRule.php new file mode 100644 index 0000000..78adaca --- /dev/null +++ b/src/Rules/EmptyLinkRule.php @@ -0,0 +1,27 @@ +getElementsByTagName('a'); + + foreach ($links as $link) { + if (trim($link->textContent) === '' && !$link->hasAttribute('aria-label')) { + $violations[] = $this->createViolation( + "Bağlantı (a) etiketi metin içermiyor ve 'aria-label' tanımlanmamış. Ekran okuyucular bu linkin nereye gittiğini anlayamaz.", + $link + ); + } + } + return $violations; + } + + public function getStandardId(): string { return 'WCAG 2.1 2.4.4'; } + public function getSeverity(): string { return 'Critical'; } +} diff --git a/src/Rules/HeadingOrderRule.php b/src/Rules/HeadingOrderRule.php new file mode 100644 index 0000000..678755c --- /dev/null +++ b/src/Rules/HeadingOrderRule.php @@ -0,0 +1,31 @@ +query('//h1|//h2|//h3|//h4|//h5|//h6'); + + $lastLevel = 0; + foreach ($headings as $h) { + $level = (int)substr($h->tagName, 1); + if ($lastLevel > 0 && $level > $lastLevel + 1) { + $violations[] = $this->createViolation( + "Başlık hiyerarşisi bozuk. H{$lastLevel} etiketinden doğrudan H{$level} etiketine geçiş yapılmış. Sıralı olmalıdır.", + $h + ); + } + $lastLevel = $level; + } + return $violations; + } + + public function getStandardId(): string { return 'WCAG 2.1 1.3.1'; } + public function getSeverity(): string { return 'Major'; } +} diff --git a/src/Rules/ImageAltRule.php b/src/Rules/ImageAltRule.php new file mode 100644 index 0000000..aa4ee37 --- /dev/null +++ b/src/Rules/ImageAltRule.php @@ -0,0 +1,27 @@ +getElementsByTagName('img'); + + foreach ($images as $img) { + if (!$img->hasAttribute('alt') || trim($img->getAttribute('alt')) === '') { + $violations[] = $this->createViolation( + "Görüntü (img) etiketi 'alt' özniteliğine sahip değil veya boş. WCAG 1.1.1 uyarınca her görüntünün bir açıklaması olmalıdır.", + $img + ); + } + } + return $violations; + } + + public function getStandardId(): string { return 'WCAG 2.1 1.1.1'; } + public function getSeverity(): string { return 'Critical'; } +} diff --git a/src/Rules/StrictRuleSet.php b/src/Rules/StrictRuleSet.php new file mode 100644 index 0000000..0f87550 --- /dev/null +++ b/src/Rules/StrictRuleSet.php @@ -0,0 +1,16 @@ +addRule(new ImageAltRule()); + $this->addRule(new HeadingOrderRule()); + $this->addRule(new EmptyLinkRule()); + // Daha fazla kural buraya eklenebilir + } +}