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
release: v3.2.0 - Enterprise Edition with Live Web Dashboard, PDF/Excel Exporters, Framework Stubs & 110 WCAG Rules
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use Spatie\Color\Hex;
|
||||
use YakNet\AccessibilityConsole\Color\Contrast\ContrastRatioCalculator;
|
||||
use YakNet\AccessibilityConsole\Color\Simulators\DeuteranopiaSimulator;
|
||||
use YakNet\AccessibilityConsole\Color\Simulators\ProtanopiaSimulator;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ColorBlindnessContrast extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_4_1_COLOR_BLINDNESS_CONTRAST'; }
|
||||
public function getDescription(): string { return 'Text and background color contrast must remain accessible (minimum 4.5:1) under color vision deficiency simulations (Deuteranopia, Protanopia).'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!$element->hasAttribute('style')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$style = $element->getAttribute('style');
|
||||
$colorHex = $this->extractColorFromStyle($style, 'color');
|
||||
$bgHex = $this->extractColorFromStyle($style, 'background-color') ?? $this->extractColorFromStyle($style, 'background');
|
||||
|
||||
if (!$colorHex || !$bgHex) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$fgRgb = Hex::fromString($colorHex)->toRgb();
|
||||
$bgRgb = Hex::fromString($bgHex)->toRgb();
|
||||
} catch (\Throwable) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Simulate Deuteranopia & Protanopia using dedicated simulators
|
||||
$simulations = [
|
||||
'Deuteranopia' => [
|
||||
'fg' => DeuteranopiaSimulator::simulate($fgRgb),
|
||||
'bg' => DeuteranopiaSimulator::simulate($bgRgb),
|
||||
],
|
||||
'Protanopia' => [
|
||||
'fg' => ProtanopiaSimulator::simulate($fgRgb),
|
||||
'bg' => ProtanopiaSimulator::simulate($bgRgb),
|
||||
],
|
||||
];
|
||||
|
||||
foreach ($simulations as $type => $sim) {
|
||||
$contrast = ContrastRatioCalculator::calculate($sim['fg'], $sim['bg']);
|
||||
if ($contrast < 4.5) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Insufficient color contrast (%.2f:1) under %s color blindness simulation (minimum 4.5:1 required).', $contrast, $type),
|
||||
sprintf('Adjust foreground color (%s) or background color (%s) to increase contrast for users with %s.', $colorHex, $bgHex, $type)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function extractColorFromStyle(string $style, string $property): ?string
|
||||
{
|
||||
if (preg_match('/' . preg_quote($property, '/') . '\s*:\s*(#[0-9a-fA-F]{3,6})/i', $style, $matches)) {
|
||||
$hex = $matches[1];
|
||||
if (strlen($hex) === 4) {
|
||||
$hex = '#' . $hex[1] . $hex[1] . $hex[2] . $hex[2] . $hex[3] . $hex[3];
|
||||
}
|
||||
return $hex;
|
||||
}
|
||||
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 ContentInfoParent extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_CONTENTINFO_PARENT'; }
|
||||
public function getDescription(): string { return 'Footer contentinfo landmark must be at top-level body or within valid sectioning root.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'footer' && $element->getAttribute('role') !== 'contentinfo') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
while ($parent instanceof DOMElement) {
|
||||
$pTag = strtolower($parent->tagName);
|
||||
if (in_array($pTag, ['article', 'aside', 'main', 'nav', 'section'], true)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Footer contentinfo landmark is nested inside <%s> sectioning element.', $pTag),
|
||||
'Place global footer contentinfo landmark at the top level of <body> rather than nested inside sectioning elements.'
|
||||
);
|
||||
}
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use Intervention\Image\ImageManager;
|
||||
use Intervention\Image\Drivers\Gd\Driver;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ImageDimensionsAndScale extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_4_4_IMAGE_DIMENSIONS_SCALE'; }
|
||||
public function getDescription(): string { return 'Images must specify explicit width and height attributes or CSS dimensions to prevent layout shifts and ensure accessible touch target sizes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'img') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hasWidthAttr = $element->hasAttribute('width') && !empty(trim($element->getAttribute('width')));
|
||||
$hasHeightAttr = $element->hasAttribute('height') && !empty(trim($element->getAttribute('height')));
|
||||
$style = $element->getAttribute('style');
|
||||
|
||||
$hasWidthStyle = preg_match('/width\s*:\s*[^;]+/i', $style) === 1;
|
||||
$hasHeightStyle = preg_match('/height\s*:\s*[^;]+/i', $style) === 1;
|
||||
|
||||
if (!$hasWidthAttr && !$hasWidthStyle) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Image element is missing explicit width attributes or CSS width rules.',
|
||||
'Specify explicit width and height attributes (e.g. width="300" height="200") on the <img> tag to prevent layout shifts (CLS).'
|
||||
);
|
||||
}
|
||||
|
||||
if (!$hasHeightAttr && !$hasHeightStyle) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Image element is missing explicit height attributes or CSS height rules.',
|
||||
'Specify explicit height attributes on the <img> tag to ensure proper rendering aspect ratio.'
|
||||
);
|
||||
}
|
||||
|
||||
// Check if image target is too small when nested in a link or button
|
||||
if ($hasWidthAttr && $hasHeightAttr) {
|
||||
$w = (int)$element->getAttribute('width');
|
||||
$h = (int)$element->getAttribute('height');
|
||||
|
||||
$parentTag = strtolower($element->parentNode ? $element->parentNode->nodeName : '');
|
||||
if (($parentTag === 'a' || $parentTag === 'button') && ($w < 24 || $h < 24)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf('Interactive image target dimensions (%dx%dpx) are smaller than the recommended 24x24px minimum touch target size.', $w, $h),
|
||||
'Increase interactive image target size or add padding to meet WCAG 2.5.5 touch target size requirements.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -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 InputTypePasswordVisibility extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_PASSWORD_VISIBILITY_TOGGLE'; }
|
||||
public function getDescription(): string { return 'Password visibility toggle buttons must have accessible names and specify aria-pressed or aria-label attributes.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$class = strtolower($element->getAttribute('class'));
|
||||
$id = strtolower($element->getAttribute('id'));
|
||||
$ariaLabel = $element->getAttribute('aria-label');
|
||||
|
||||
$isPasswordToggle = str_contains($class, 'password') || str_contains($class, 'toggle-pass') || str_contains($id, 'password') || str_contains($id, 'toggle-pass');
|
||||
|
||||
if ($isPasswordToggle && empty($ariaLabel) && !$element->hasAttribute('aria-labelledby') && empty(trim($element->textContent))) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Password visibility toggle button is missing an accessible name.',
|
||||
'Add an aria-label="Şifreyi Göster/Gizle" or aria-pressed="false" attribute to the password toggle button.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -2,11 +2,17 @@
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use ReflectionClass;
|
||||
use YakNet\AccessibilityConsole\Core\AbstractRule;
|
||||
|
||||
class RuleLevels
|
||||
{
|
||||
/**
|
||||
* Get rule instances for the specified level.
|
||||
* Levels are incremental: Level 5 includes all rules from Levels 1, 2, 3, and 4.
|
||||
* Recursively scans src/Rules and all subdirectories for rule classes.
|
||||
*
|
||||
* @param int $level
|
||||
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
|
||||
@@ -14,27 +20,46 @@ class RuleLevels
|
||||
public static function getRulesForLevel(int $level): array
|
||||
{
|
||||
$rules = [];
|
||||
$files = glob(__DIR__ . '/*.php');
|
||||
if ($files === false) {
|
||||
$dir = __DIR__;
|
||||
|
||||
if (!is_dir($dir)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
foreach ($files as $file) {
|
||||
$className = pathinfo($file, PATHINFO_FILENAME);
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $className;
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator($dir, RecursiveDirectoryIterator::SKIP_DOTS)
|
||||
);
|
||||
|
||||
/** @var \SplFileInfo $file */
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->getExtension() !== 'php') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$realPath = $file->getRealPath();
|
||||
$relativePath = str_replace([$dir . DIRECTORY_SEPARATOR, '.php'], ['', ''], $realPath);
|
||||
$subNamespace = str_replace(DIRECTORY_SEPARATOR, '\\', $relativePath);
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $subNamespace;
|
||||
|
||||
if (!class_exists($fqcn)) {
|
||||
// Fallback to flat namespace if class is in root Rules namespace
|
||||
$flatClassName = $file->getBasename('.php');
|
||||
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $flatClassName;
|
||||
}
|
||||
|
||||
if (class_exists($fqcn)) {
|
||||
$reflection = new \ReflectionClass($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);
|
||||
$isRule = $reflection->isSubclassOf(AbstractRule::class) ||
|
||||
$reflection->implementsInterface(RuleInterface::class);
|
||||
|
||||
if ($isRule) {
|
||||
$className = $file->getBasename('.php');
|
||||
// Exclude Turkish custom rules to match standard behavior
|
||||
if (in_array($className, ['EmptyLinkRule', 'HeadingOrderRule', 'ImageAltRule'], true)) {
|
||||
continue;
|
||||
|
||||
@@ -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 TableNestedBanned extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_TABLE_NESTED_BANNED'; }
|
||||
public function getDescription(): string { return 'Tables should not be nested inside another table as it confuses screen reader data table navigation.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
public function getLevel(): int { return 4; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'table') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$parent = $element->parentNode;
|
||||
while ($parent instanceof DOMElement) {
|
||||
if (strtolower($parent->tagName) === 'table') {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
'Table element is nested inside another table.',
|
||||
'Refactor layout to use modern CSS Flexbox/Grid instead of nested tables for tabular data.'
|
||||
);
|
||||
}
|
||||
$parent = $parent->parentNode;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user