Major upgrade v2.0.0

This commit is contained in:
Enes Yakıştır
2026-05-13 09:10:43 +03:00
commit e348025579
41 changed files with 2139 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
abstract class AbstractRule implements RuleInterface
{
abstract public function getId(): string;
abstract public function getDescription(): string;
abstract public function getStandard(): WCAGStandard;
abstract public function getSeverity(): Severity;
protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation
{
return new Violation(
ruleId: $this->getId(),
message: $message,
severity: $this->getSeverity(),
standard: $this->getStandard(),
htmlSnippet: $element->ownerDocument->saveHTML($element),
fixSuggestion: $suggestion
);
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class ButtonName extends AbstractRule
{
public function getId(): string { return 'WCAG_4_1_2_BUTTON'; }
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 check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'button') {
return null;
}
$text = trim($element->textContent);
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
return $this->createViolation($element, $this->getDescription(), 'Add text or an aria-label to the button.');
}
return null;
}
}
+38
View File
@@ -0,0 +1,38 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class EmptyLink extends AbstractRule
{
public function getId(): string { return 'WCAG_2_4_4_LINK_TEXT'; }
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 check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'a') {
return null;
}
$text = trim($element->textContent);
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
// Check for images inside
$imgs = $element->getElementsByTagName('img');
foreach ($imgs as $img) {
if ($img->hasAttribute('alt') && trim($img->getAttribute('alt')) !== '') {
return null;
}
}
return $this->createViolation($element, $this->getDescription(), 'Add text content or an aria-label to the link.');
}
return null;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class FieldsetLegend extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_FIELDSET'; }
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 check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'fieldset') {
return null;
}
$legends = $element->getElementsByTagName('legend');
if ($legends->length === 0) {
return $this->createViolation($element, $this->getDescription(), 'Add a <legend> inside the fieldset.');
}
return null;
}
}
+55
View File
@@ -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 FormLabel extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_LABEL'; }
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 check(DOMElement $element): ?Violation
{
$tags = ['input', 'select', 'textarea'];
if (!in_array(strtolower($element->tagName), $tags)) {
return null;
}
// Skip hidden inputs and buttons
$type = strtolower($element->getAttribute('type'));
if ($type === 'hidden' || $type === 'submit' || $type === 'button' || $type === 'reset') {
return null;
}
if ($element->hasAttribute('id')) {
$id = $element->getAttribute('id');
$xpath = new \DOMXPath($element->ownerDocument);
$labels = $xpath->query("//label[@for='$id']");
if ($labels->length > 0) {
return null;
}
}
// Check for aria-label or aria-labelledby
if ($element->hasAttribute('aria-label') || $element->hasAttribute('aria-labelledby')) {
return null;
}
// Check if wrapped in label
$parent = $element->parentNode;
while ($parent) {
if ($parent instanceof DOMElement && strtolower($parent->tagName) === 'label') {
return null;
}
$parent = $parent->parentNode;
}
return $this->createViolation($element, $this->getDescription(), 'Add a <label for="..."> or an aria-label attribute.');
}
}
+40
View File
@@ -0,0 +1,40 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class HeadingOrder extends AbstractRule
{
private static int $lastLevel = 0;
public function getId(): string { return 'WCAG_1_3_1_HEADING'; }
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 check(DOMElement $element): ?Violation
{
if (!preg_match('/^h([1-6])$/i', $element->tagName, $matches)) {
return null;
}
$currentLevel = (int)$matches[1];
// This rule is a bit complex in a per-element check without global state,
// but we can at least check if an h2 comes before an h1 in some cases or jumps.
// For simplicity in this engine, we warn if it's not starting with H1 or jumps too many levels.
if ($currentLevel > self::$lastLevel + 1 && self::$lastLevel !== 0) {
$violation = $this->createViolation($element, "Heading level h$currentLevel jumps from h" . self::$lastLevel, "Ensure headings follow a logical order (h1, then h2, etc.)");
self::$lastLevel = $currentLevel;
return $violation;
}
self::$lastLevel = $currentLevel;
return null;
}
}
+29
View File
@@ -0,0 +1,29 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class HtmlHasLang extends AbstractRule
{
public function getId(): string { return 'WCAG_3_1_1_LANG'; }
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 check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'html') {
return null;
}
if (!$element->hasAttribute('lang') || trim($element->getAttribute('lang')) === '') {
return $this->createViolation($element, $this->getDescription(), 'Add lang="en" (or your language) to the html tag.');
}
return null;
}
}
+35
View File
@@ -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 ImgAltText extends AbstractRule
{
public function getId(): string { return 'WCAG_1_1_1_ALT'; }
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 check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'img') {
return null;
}
if (!$element->hasAttribute('alt')) {
// Check for decorative roles
$role = $element->getAttribute('role');
if ($role === 'presentation' || $role === 'none') {
return null;
}
return $this->createViolation($element, $this->getDescription(), 'Add an alt attribute. Use alt="" for decorative images.');
}
return null;
}
}
+30
View File
@@ -0,0 +1,30 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class MetaViewport extends AbstractRule
{
public function getId(): string { return 'A11Y_VIEWPORT'; }
public function getDescription(): string { return 'Viewport meta tag should not disable zooming.'; }
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
public function getSeverity(): Severity { return Severity::ERROR; }
public function check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'meta' || $element->getAttribute('name') !== 'viewport') {
return null;
}
$content = $element->getAttribute('content');
if (str_contains($content, 'user-scalable=no') || str_contains($content, 'maximum-scale=1')) {
return $this->createViolation($element, $this->getDescription(), 'Remove user-scalable=no or maximum-scale=1 to allow users to zoom.');
}
return null;
}
}
+12
View File
@@ -0,0 +1,12 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Violation;
interface RuleInterface
{
public function getId(): string;
public function check(DOMElement $element): ?Violation;
}
+23
View File
@@ -0,0 +1,23 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
class StandardRuleSet
{
/**
* @return RuleInterface[]
*/
public static function all(): array
{
return [
new HtmlHasLang(),
new ImgAltText(),
new EmptyLink(),
new ButtonName(),
new FormLabel(),
new FieldsetLegend(),
new HeadingOrder(),
new MetaViewport(),
];
}
}