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
feat: Release v3.0.0 - PHPStan-style analysis, configuration, baseline, and advanced WCAG rules
This commit is contained in:
@@ -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 AccesskeyBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_2_1_1_ACCESSKEY'; }
|
||||
public function getDescription(): string { return 'The accesskey attribute should not be used as it conflicts with browser and screen reader shortcuts.'; }
|
||||
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('accesskey')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the accesskey attribute. Use natural focus order and standard keyboard event listeners instead.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
+221
-34
@@ -9,6 +9,9 @@ use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ColorContrast extends AbstractRule
|
||||
{
|
||||
/** @var array<string, array<string, array{color?: string, background-color?: string}>> */
|
||||
private array $documentStylesCache = [];
|
||||
|
||||
public function getId(): string { return 'WCAG_1_4_3_CONTRAST'; }
|
||||
public function getDescription(): string { return 'Text must have sufficient contrast against the background (4.5:1 minimum).'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
@@ -17,14 +20,27 @@ class ColorContrast extends AbstractRule
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('style')) {
|
||||
return null;
|
||||
}
|
||||
// 1. Parse inline styles (highest priority)
|
||||
$inlineStyle = $element->hasAttribute('style') ? $element->getAttribute('style') : '';
|
||||
$color = $this->parseColorValue($inlineStyle, 'color');
|
||||
$bg = $this->parseColorValue($inlineStyle, 'background-color') ?: $this->parseColorValue($inlineStyle, 'background');
|
||||
|
||||
$style = $element->getAttribute('style');
|
||||
|
||||
$color = $this->parseColorValue($style, 'color');
|
||||
$bg = $this->parseColorValue($style, 'background-color') ?: $this->parseColorValue($style, 'background');
|
||||
// 2. Fallback: Parse stylesheet styles
|
||||
if (($color === null || $bg === null) && $element->ownerDocument !== null) {
|
||||
$docStyles = $this->parseDocumentStyles($element->ownerDocument);
|
||||
|
||||
// Loop through all parsed styles. Later matching rules override earlier ones
|
||||
foreach ($docStyles as $selector => $styles) {
|
||||
if ($this->elementMatchesSelector($element, $selector)) {
|
||||
if ($color === null && isset($styles['color'])) {
|
||||
$color = $styles['color'];
|
||||
}
|
||||
if ($bg === null && isset($styles['background-color'])) {
|
||||
$bg = $styles['background-color'];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($color === null || $bg === null) {
|
||||
return null;
|
||||
@@ -42,22 +58,11 @@ class ColorContrast extends AbstractRule
|
||||
|
||||
$ratio = ($cLum > $bgLum) ? ($cLum + 0.05) / ($bgLum + 0.05) : ($bgLum + 0.05) / ($cLum + 0.05);
|
||||
|
||||
// Check font size & weight for large text rules
|
||||
$isLarge = false;
|
||||
if (preg_match('/font-size\s*:\s*([0-9.]+)(px|em|rem|pt)/i', $style, $matches)) {
|
||||
$val = (float)$matches[1];
|
||||
$unit = strtolower($matches[2]);
|
||||
if (($unit === 'px' && $val >= 24) || ($unit === 'pt' && $val >= 18) || (($unit === 'em' || $unit === 'rem') && $val >= 1.5)) {
|
||||
$isLarge = true;
|
||||
}
|
||||
}
|
||||
if (preg_match('/font-weight\s*:\s*(bold|[7-9]00)/i', $style)) {
|
||||
if (preg_match('/font-size\s*:\s*([0-9.]+)(px|em|rem|pt)/i', $style, $matches)) {
|
||||
$val = (float)$matches[1];
|
||||
$unit = strtolower($matches[2]);
|
||||
if (($unit === 'px' && $val >= 18) || ($unit === 'pt' && $val >= 14) || (($unit === 'em' || $unit === 'rem') && $val >= 1.2)) {
|
||||
$isLarge = true;
|
||||
}
|
||||
}
|
||||
$fontSizeStyle = $this->parseFontSize($inlineStyle, $element);
|
||||
if ($fontSizeStyle !== null) {
|
||||
$isLarge = $this->isLargeText($fontSizeStyle, $this->parseFontWeight($inlineStyle, $element));
|
||||
}
|
||||
|
||||
$minRatio = $isLarge ? 3.0 : 4.5;
|
||||
@@ -74,21 +79,203 @@ class ColorContrast extends AbstractRule
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseFontSize(string $style, DOMElement $element): ?string
|
||||
{
|
||||
if (preg_match('/font-size\s*:\s*([^;]+)/i', $style, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseFontWeight(string $style, DOMElement $element): ?string
|
||||
{
|
||||
if (preg_match('/font-weight\s*:\s*([^;]+)/i', $style, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isLargeText(string $size, ?string $weight): bool
|
||||
{
|
||||
if (preg_match('/([0-9.]+)(px|em|rem|pt)/i', $size, $matches)) {
|
||||
$val = (float)$matches[1];
|
||||
$unit = strtolower($matches[2]);
|
||||
$isBold = $weight !== null && preg_match('/^(bold|[7-9]00)$/i', $weight);
|
||||
|
||||
if ($isBold) {
|
||||
return ($unit === 'px' && $val >= 18) || ($unit === 'pt' && $val >= 14) || (($unit === 'em' || $unit === 'rem') && $val >= 1.2);
|
||||
} else {
|
||||
return ($unit === 'px' && $val >= 24) || ($unit === 'pt' && $val >= 18) || (($unit === 'em' || $unit === 'rem') && $val >= 1.5);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, array{color?: string, background-color?: string}>
|
||||
*/
|
||||
private function parseDocumentStyles(\DOMDocument $doc): array
|
||||
{
|
||||
$hash = spl_object_hash($doc);
|
||||
if (isset($this->documentStylesCache[$hash])) {
|
||||
return $this->documentStylesCache[$hash];
|
||||
}
|
||||
|
||||
$mergedStyles = [];
|
||||
$styleTags = $doc->getElementsByTagName('style');
|
||||
|
||||
foreach ($styleTags as $tag) {
|
||||
$cssText = $tag->textContent;
|
||||
if (trim($cssText) === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$parser = new \Sabberworm\CSS\Parser($cssText);
|
||||
$cssDoc = $parser->parse();
|
||||
|
||||
foreach ($cssDoc->getAllDeclarationBlocks() as $block) {
|
||||
$rules = [];
|
||||
foreach ($block->getDeclarations() as $decl) {
|
||||
$prop = strtolower($decl->getPropertyName());
|
||||
if ($prop === 'color' || $prop === 'background-color' || $prop === 'background') {
|
||||
// Extract string value
|
||||
$valObj = $decl->getValue();
|
||||
$val = is_object($valObj) && method_exists($valObj, '__toString')
|
||||
? (string)$valObj
|
||||
: (is_string($valObj) ? $valObj : '');
|
||||
|
||||
if ($prop === 'color') {
|
||||
$normalized = $this->normalizeColorValue($val);
|
||||
if ($normalized !== null) {
|
||||
$rules['color'] = $normalized;
|
||||
}
|
||||
} elseif ($prop === 'background-color') {
|
||||
$normalized = $this->normalizeColorValue($val);
|
||||
if ($normalized !== null) {
|
||||
$rules['background-color'] = $normalized;
|
||||
}
|
||||
} elseif ($prop === 'background') {
|
||||
$normalized = $this->extractColorFromBackground($val);
|
||||
if ($normalized !== null) {
|
||||
$rules['background-color'] = $normalized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($rules)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($block->getSelectors() as $selector) {
|
||||
$selectorString = trim($selector->getSelector());
|
||||
if ($selectorString !== '') {
|
||||
if (!isset($mergedStyles[$selectorString])) {
|
||||
$mergedStyles[$selectorString] = [];
|
||||
}
|
||||
$mergedStyles[$selectorString] = array_merge($mergedStyles[$selectorString], $rules);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
// Ignore CSS parser failures
|
||||
}
|
||||
}
|
||||
|
||||
$this->documentStylesCache[$hash] = $mergedStyles;
|
||||
return $mergedStyles;
|
||||
}
|
||||
|
||||
private function elementMatchesSelector(DOMElement $element, string $selector): bool
|
||||
{
|
||||
$parts = array_map('trim', explode(',', $selector));
|
||||
foreach ($parts as $part) {
|
||||
if ($part === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($part, '#')) {
|
||||
$id = substr($part, 1);
|
||||
if ($element->getAttribute('id') === $id) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_starts_with($part, '.')) {
|
||||
$class = substr($part, 1);
|
||||
$classes = array_filter(array_map('trim', explode(' ', $element->getAttribute('class'))));
|
||||
if (in_array($class, $classes, true)) {
|
||||
return true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strtolower($element->tagName) === strtolower($part)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (preg_match('/^([a-zA-Z0-9_-]+)\.([a-zA-Z0-9_-]+)$/', $part, $matches)) {
|
||||
$tag = $matches[1];
|
||||
$class = $matches[2];
|
||||
$classes = array_filter(array_map('trim', explode(' ', $element->getAttribute('class'))));
|
||||
if (strtolower($element->tagName) === strtolower($tag) && in_array($class, $classes, true)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/^([a-zA-Z0-9_-]+)#([a-zA-Z0-9_-]+)$/', $part, $matches)) {
|
||||
$tag = $matches[1];
|
||||
$id = $matches[2];
|
||||
if (strtolower($element->tagName) === strtolower($tag) && $element->getAttribute('id') === $id) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function normalizeColorValue(string $val): ?string
|
||||
{
|
||||
$val = trim($val);
|
||||
if (preg_match('/^#([0-9a-fA-F]{3,6})$/', $val)) {
|
||||
return $val;
|
||||
}
|
||||
|
||||
if (preg_match('/rgba?\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)/i', $val, $matches)) {
|
||||
return sprintf("#%02x%02x%02x", (int)$matches[1], (int)$matches[2], (int)$matches[3]);
|
||||
}
|
||||
|
||||
$colors = [
|
||||
'black' => '#000000', 'white' => '#ffffff', 'red' => '#ff0000',
|
||||
'green' => '#008000', 'blue' => '#0000ff', 'yellow' => '#ffff00',
|
||||
'gray' => '#808080', 'grey' => '#808080', 'silver' => '#c0c0c0',
|
||||
];
|
||||
if (isset($colors[strtolower($val)])) {
|
||||
return $colors[strtolower($val)];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractColorFromBackground(string $val): ?string
|
||||
{
|
||||
$parts = explode(' ', $val);
|
||||
foreach ($parts as $part) {
|
||||
$color = $this->normalizeColorValue($part);
|
||||
if ($color !== null) {
|
||||
return $color;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private function parseColorValue(string $style, string $property): ?string
|
||||
{
|
||||
if (preg_match('/(?:^|;)\s*' . preg_quote($property, '/') . '\s*:\s*([^;]+)/i', $style, $matches)) {
|
||||
$value = trim($matches[1]);
|
||||
if (preg_match('/^#([0-9a-f]{3,6})$/i', $value)) {
|
||||
return $value;
|
||||
}
|
||||
$colors = [
|
||||
'black' => '#000000', 'white' => '#ffffff', 'red' => '#ff0000',
|
||||
'green' => '#008000', 'blue' => '#0000ff', 'yellow' => '#ffff00',
|
||||
'gray' => '#808080', 'grey' => '#808080', 'silver' => '#c0c0c0',
|
||||
];
|
||||
if (isset($colors[strtolower($value)])) {
|
||||
return $colors[strtolower($value)];
|
||||
}
|
||||
return $this->normalizeColorValue(trim($matches[1]));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -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 FontTagBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_FONT_TAG_BANNED'; }
|
||||
public function getDescription(): string { return 'The font tag is obsolete and should not be used. Use CSS for styling instead.'; }
|
||||
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) === 'font') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the <font> tag and style the text using CSS instead.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class InputTypeButtonDiscernible extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_INPUT_BUTTON'; }
|
||||
public function getDescription(): string { return 'Input buttons (button, submit, reset) must have discernible text.'; }
|
||||
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) !== 'input') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
if (in_array($type, ['button', 'submit', 'reset'], true)) {
|
||||
$value = trim($element->getAttribute('value'));
|
||||
$ariaLabel = trim($element->getAttribute('aria-label'));
|
||||
$title = trim($element->getAttribute('title'));
|
||||
|
||||
if ($value === '' && $ariaLabel === '' && $title === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add a non-empty value, aria-label, or title attribute to the input button.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class LanguageSubtag extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_3_1_2_LANG'; }
|
||||
public function getDescription(): string { return 'Language attributes (lang) must use a valid language code format.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 3; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('lang')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$lang = trim($element->getAttribute('lang'));
|
||||
|
||||
// Simple IETF language tag format check (e.g. "en", "tr", "en-US", "zh-Hant")
|
||||
$isValid = preg_match('/^[a-zA-Z]{2,3}(-[a-zA-Z0-9]{2,8})*$/', $lang);
|
||||
|
||||
if (!$isValid || $lang === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Use a valid IETF language code format for the lang attribute (e.g., "tr", "en", "en-US").'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class LinkTargetBlank extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_LINK_TARGET_BLANK'; }
|
||||
public function getDescription(): string { return 'Links with target="_blank" should have rel="noopener" or rel="noreferrer".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'a') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (strtolower($element->getAttribute('target')) === '_blank') {
|
||||
$rel = strtolower($element->getAttribute('rel'));
|
||||
if (!str_contains($rel, 'noopener') && !str_contains($rel, 'noreferrer')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add rel="noopener" or rel="noreferrer" to the link to improve security, performance, and screen reader notification.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 MarqueeBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_2_2_2_MARQUEE'; }
|
||||
public function getDescription(): string { return 'The marquee element is obsolete and presents significant accessibility barriers.'; }
|
||||
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
|
||||
{
|
||||
if (strtolower($element->tagName) === 'marquee') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the marquee tag and use CSS animations or transitions instead.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 MediaAutoplay extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_4_2_AUTOPLAY'; }
|
||||
public function getDescription(): string { return 'Audio and video elements must not autoplay with sound, as it interferes with screen readers and user control.'; }
|
||||
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
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
if ($tagName !== 'video' && $tagName !== 'audio') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('autoplay')) {
|
||||
// If autoplay is active, it must be muted to be acceptable
|
||||
if (!$element->hasAttribute('muted')) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove the autoplay attribute, or add the muted attribute to prevent the media from playing sound automatically.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class NoFocusOutline extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_2_4_7_FOCUS_OUTLINE'; }
|
||||
public function getDescription(): string { return 'Focus outlines should not be disabled in inline styles without providing custom focus styles.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 5; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('style')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$style = strtolower($element->getAttribute('style'));
|
||||
|
||||
// Match outline: none or outline: 0
|
||||
if (preg_match('/\boutline\s*:\s*(none|0)\b/', $style)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Avoid disabling focus outlines using outline: none or outline: 0 in inline styles. Ensure keyboard users can see where focus is.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ObjectAlt extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_1_1_OBJECT'; }
|
||||
public function getDescription(): string { return 'Object elements must have alternative text or descriptive 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) !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Object can have title attribute
|
||||
if ($element->hasAttribute('title') && trim($element->getAttribute('title')) !== '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Or object can contain fallback text content.
|
||||
// We clean up child tags like <param> or <embed> to find actual descriptive text.
|
||||
$text = '';
|
||||
foreach ($element->childNodes as $child) {
|
||||
if ($child instanceof \DOMText) {
|
||||
$text .= $child->textContent;
|
||||
} elseif ($child instanceof DOMElement) {
|
||||
$tagName = strtolower($child->tagName);
|
||||
if ($tagName !== 'param' && $tagName !== 'embed') {
|
||||
$text .= $child->textContent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (trim($text) === '') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add fallback text inside the <object> element or provide a descriptive title attribute.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class TableHeaders extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_TABLE'; }
|
||||
public function getDescription(): string { return 'Data tables must have headers (th) or captions for screen readers.'; }
|
||||
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) !== 'table') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip layout tables explicitly marked as presentation
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
if ($role === 'presentation' || $role === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there is at least one <th> or <caption>
|
||||
$thCount = $element->getElementsByTagName('th')->length;
|
||||
$captionCount = $element->getElementsByTagName('caption')->length;
|
||||
|
||||
if ($thCount === 0 && $captionCount === 0) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Add <th> header elements or a <caption> to the table, or set role="presentation" if it is only used for layout.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user