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(v3.3.0): enterprise presets, badges, notifications, rule controller, watch mode, offline healer, and WCAG 2.2 ruleset
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMXPath;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class LabelInName extends AbstractRule
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'WCAG_2_5_3_LABEL_IN_NAME';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Accessible name must contain the visible text label of interactive controls (WCAG 2.5.3).';
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
$tag = strtolower($element->tagName);
|
||||
$role = strtolower($element->getAttribute('role'));
|
||||
$isInteractive = in_array($tag, ['button', 'a'], true)
|
||||
|| ($tag === 'input' && in_array(strtolower($element->getAttribute('type')), ['button', 'submit', 'reset'], true))
|
||||
|| in_array($role, ['button', 'link'], true);
|
||||
|
||||
if (!$isInteractive) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Avoid template false positives
|
||||
$html = $element->ownerDocument ? $element->ownerDocument->saveHTML($element) : '';
|
||||
if ($html !== false && preg_match('/<\?php|\{\{|\{%/i', $html)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Determine visible text
|
||||
$visibleText = '';
|
||||
if ($tag === 'input') {
|
||||
$visibleText = trim($element->getAttribute('value'));
|
||||
} else {
|
||||
$visibleText = trim($element->textContent);
|
||||
}
|
||||
|
||||
// If there is no visible text (e.g. icon-only button), Label in Name criterion does not apply
|
||||
if ($visibleText === '' || mb_strlen($visibleText) < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get explicit accessible name
|
||||
$accessibleName = '';
|
||||
if ($element->hasAttribute('aria-label')) {
|
||||
$accessibleName = trim($element->getAttribute('aria-label'));
|
||||
} elseif ($element->hasAttribute('aria-labelledby')) {
|
||||
$id = trim($element->getAttribute('aria-labelledby'));
|
||||
$doc = $element->ownerDocument;
|
||||
if ($doc !== null && $id !== '') {
|
||||
$xpath = new DOMXPath($doc);
|
||||
$nodes = $xpath->query('//*[@id="' . $id . '"]');
|
||||
if ($nodes !== false && $nodes->length > 0) {
|
||||
$node = $nodes->item(0);
|
||||
if ($node instanceof DOMNode) {
|
||||
$accessibleName = trim($node->textContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If no explicit ARIA name is set, the visible text itself is the accessible name
|
||||
if ($accessibleName === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Normalization: clean extra whitespace and lower case
|
||||
$normVisible = mb_strtolower((string)preg_replace('/\s+/', ' ', $visibleText));
|
||||
$normAcc = mb_strtolower((string)preg_replace('/\s+/', ' ', $accessibleName));
|
||||
|
||||
// WCAG 2.5.3: Accessible name must contain the visible text
|
||||
if (!str_contains($normAcc, $normVisible)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
sprintf(
|
||||
"Accessible name \"%s\" does not contain the visible text \"%s\".",
|
||||
$accessibleName,
|
||||
$visibleText
|
||||
),
|
||||
sprintf(
|
||||
"Update aria-label to include \"%s\" (e.g. aria-label=\"%s %s\") so speech input users can activate the control.",
|
||||
$visibleText,
|
||||
$visibleText,
|
||||
$accessibleName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use DOMXPath;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class RedundantEntry extends AbstractRule
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'WCAG_3_3_7_REDUNDANT_ENTRY';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Forms with repeated information (such as billing and shipping) must provide auto-population or a selection mechanism (WCAG 2.2 SC 3.3.7).';
|
||||
}
|
||||
|
||||
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 (strtolower($element->tagName) !== 'form') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$doc = $element->ownerDocument;
|
||||
if ($doc === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$xpath = new DOMXPath($doc);
|
||||
|
||||
// Find all input and select elements in the form
|
||||
$inputs = $xpath->query('.//input | .//select | .//textarea', $element);
|
||||
if ($inputs === false || $inputs->length < 4) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$hasShipping = false;
|
||||
$hasBilling = false;
|
||||
$hasSameCheckbox = false;
|
||||
|
||||
foreach ($inputs as $input) {
|
||||
if (!$input instanceof DOMElement) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$name = strtolower($input->getAttribute('name'));
|
||||
$id = strtolower($input->getAttribute('id'));
|
||||
$autocomplete = strtolower($input->getAttribute('autocomplete'));
|
||||
$type = strtolower($input->getAttribute('type'));
|
||||
|
||||
if (str_contains($name, 'shipping') || str_contains($id, 'shipping') || str_contains($autocomplete, 'shipping')) {
|
||||
$hasShipping = true;
|
||||
}
|
||||
|
||||
if (str_contains($name, 'billing') || str_contains($id, 'billing') || str_contains($autocomplete, 'billing')) {
|
||||
$hasBilling = true;
|
||||
}
|
||||
|
||||
if ($type === 'checkbox') {
|
||||
if (str_contains($name, 'same') || str_contains($id, 'same') || str_contains($name, 'copy') || str_contains($id, 'copy')) {
|
||||
$hasSameCheckbox = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Also check labels for "same as"
|
||||
if (!$hasSameCheckbox) {
|
||||
$labels = $xpath->query('.//label', $element);
|
||||
if ($labels !== false) {
|
||||
foreach ($labels as $label) {
|
||||
if ($label instanceof DOMElement) {
|
||||
$labelText = strtolower(trim($label->textContent));
|
||||
if (str_contains($labelText, 'same as') || str_contains($labelText, 'aynı') || str_contains($labelText, 'kopyala')) {
|
||||
$hasSameCheckbox = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If form has both shipping and billing fields but no "same as" checkbox
|
||||
if ($hasShipping && $hasBilling && !$hasSameCheckbox) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
"Form requires re-entering shipping and billing information without providing a 'Same as shipping' checkbox or auto-fill option (WCAG 2.2 SC 3.3.7 Redundant Entry).",
|
||||
"Add a checkbox (e.g. 'Billing address is the same as shipping address') or populate with previously entered user data to reduce cognitive burden."
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\AbstractRule;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class RuleController
|
||||
{
|
||||
/**
|
||||
* @var array<string, RuleInterface|AbstractRule>
|
||||
*/
|
||||
private array $rulesRegistry = [];
|
||||
|
||||
/**
|
||||
* @param array<int, RuleInterface|AbstractRule>|null $rules
|
||||
*/
|
||||
public function __construct(?array $rules = null)
|
||||
{
|
||||
$allRules = $rules ?? RuleLevels::getRulesForLevel(9);
|
||||
foreach ($allRules as $rule) {
|
||||
$id = ($rule instanceof RuleInterface) ? $rule->getId() : (new \ReflectionClass($rule))->getShortName();
|
||||
$this->rulesRegistry[$id] = $rule;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rule instance by ID.
|
||||
*/
|
||||
public function getRule(string $ruleId): RuleInterface|AbstractRule|null
|
||||
{
|
||||
return $this->rulesRegistry[$ruleId] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a specific rule directly on a DOM element.
|
||||
*/
|
||||
public function executeRule(string $ruleId, DOMElement $element): ?Violation
|
||||
{
|
||||
$rule = $this->getRule($ruleId);
|
||||
if ($rule === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($rule instanceof RuleInterface) {
|
||||
return $rule->check($element);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter active rules by WCAG standard (A, AA, AAA).
|
||||
*
|
||||
* @return array<string, RuleInterface|AbstractRule>
|
||||
*/
|
||||
public function filterByStandard(WCAGStandard $standard): array
|
||||
{
|
||||
$filtered = [];
|
||||
foreach ($this->rulesRegistry as $id => $rule) {
|
||||
if (method_exists($rule, 'getStandard') && $rule->getStandard() === $standard) {
|
||||
$filtered[$id] = $rule;
|
||||
}
|
||||
}
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter active rules by severity.
|
||||
*
|
||||
* @return array<string, RuleInterface|AbstractRule>
|
||||
*/
|
||||
public function filterBySeverity(Severity $severity): array
|
||||
{
|
||||
$filtered = [];
|
||||
foreach ($this->rulesRegistry as $id => $rule) {
|
||||
if (method_exists($rule, 'getSeverity') && $rule->getSeverity() === $severity) {
|
||||
$filtered[$id] = $rule;
|
||||
}
|
||||
}
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Total number of registered rules in this controller.
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return count($this->rulesRegistry);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
class RuleExecutionStats
|
||||
{
|
||||
/**
|
||||
* @var array<string, array{invocations: int, total_time_ms: float, violations: int}>
|
||||
*/
|
||||
private array $stats = [];
|
||||
|
||||
/**
|
||||
* Record a rule execution metric.
|
||||
*/
|
||||
public function record(string $ruleId, float $durationMs, bool $hasViolation = false): void
|
||||
{
|
||||
if (!isset($this->stats[$ruleId])) {
|
||||
$this->stats[$ruleId] = [
|
||||
'invocations' => 0,
|
||||
'total_time_ms' => 0.0,
|
||||
'violations' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$this->stats[$ruleId]['invocations']++;
|
||||
$this->stats[$ruleId]['total_time_ms'] += $durationMs;
|
||||
if ($hasViolation) {
|
||||
$this->stats[$ruleId]['violations']++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top slowest rules by total execution time.
|
||||
*
|
||||
* @return array<string, array{invocations: int, total_time_ms: float, avg_time_ms: float, violations: int}>
|
||||
*/
|
||||
public function getSlowestRules(int $limit = 10): array
|
||||
{
|
||||
$calculated = [];
|
||||
foreach ($this->stats as $id => $data) {
|
||||
$avg = $data['invocations'] > 0 ? ($data['total_time_ms'] / $data['invocations']) : 0.0;
|
||||
$calculated[$id] = [
|
||||
'invocations' => $data['invocations'],
|
||||
'total_time_ms' => round($data['total_time_ms'], 3),
|
||||
'avg_time_ms' => round($avg, 4),
|
||||
'violations' => $data['violations'],
|
||||
];
|
||||
}
|
||||
|
||||
uasort($calculated, function ($a, $b) {
|
||||
return $b['total_time_ms'] <=> $a['total_time_ms'];
|
||||
});
|
||||
|
||||
return array_slice($calculated, 0, $limit, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all recorded statistics.
|
||||
*
|
||||
* @return array<string, array{invocations: int, total_time_ms: float, violations: int}>
|
||||
*/
|
||||
public function getAllStats(): array
|
||||
{
|
||||
return $this->stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all stats counters.
|
||||
*/
|
||||
public function reset(): void
|
||||
{
|
||||
$this->stats = [];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class TextSpacing extends AbstractRule
|
||||
{
|
||||
public function getId(): string
|
||||
{
|
||||
return 'WCAG_1_4_12_TEXT_SPACING';
|
||||
}
|
||||
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Ensure text containers do not restrict line-height or clip content when text spacing is adjusted (WCAG 1.4.12).';
|
||||
}
|
||||
|
||||
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 = strtolower($element->getAttribute('style'));
|
||||
|
||||
// Check line-height < 1.2 or <= 12px
|
||||
if (preg_match('/line-height\s*:\s*([^;]+)/i', $style, $matches)) {
|
||||
$lh = trim($matches[1]);
|
||||
// Unitless number check
|
||||
if (is_numeric($lh) && (float)$lh < 1.2 && (float)$lh > 0) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
"Inline line-height is set to {$lh}, which is below the recommended minimum of 1.2 to 1.5.",
|
||||
"Increase line-height to at least 1.5 to satisfy WCAG 1.4.12 Text Spacing."
|
||||
);
|
||||
}
|
||||
// Pixel check (e.g. 10px, 12px)
|
||||
if (preg_match('/^(\d+(?:\.\d+)?)px$/i', $lh, $pxMatches)) {
|
||||
$px = (float)$pxMatches[1];
|
||||
if ($px < 14) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
"Inline line-height is fixed to {$px}px, risking text overlapping when resized.",
|
||||
"Use relative line-height units (e.g. 1.5) instead of fixed small pixel values."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check fixed height combined with overflow: hidden on text containers
|
||||
$textTags = ['p', 'div', 'span', 'article', 'section', 'blockquote', 'li'];
|
||||
if (in_array(strtolower($element->tagName), $textTags, true)) {
|
||||
if (str_contains($style, 'overflow:hidden') || str_contains($style, 'overflow: hidden')) {
|
||||
if (preg_match('/(?:^|;)\s*height\s*:\s*(\d+)px/i', $style, $heightMatches)) {
|
||||
$height = (int)$heightMatches[1];
|
||||
$textLength = mb_strlen(trim($element->textContent));
|
||||
if ($height <= 60 && $textLength > 60) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
"Fixed height ({$height}px) with overflow:hidden risks clipping text when user overrides text spacing.",
|
||||
"Use min-height instead of fixed height or avoid overflow:hidden on text containers."
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user