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
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6df5e2bdc |
@@ -30,6 +30,25 @@
|
||||
<!-- Missing Iframe Title -->
|
||||
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315"></iframe>
|
||||
|
||||
<!-- New WCAG Rule Violations for testing -->
|
||||
|
||||
<!-- LinkTextGeneric: Generic text without context -->
|
||||
<a href="/read-more">Click here</a>
|
||||
<a href="/devam-oku">devamını oku</a>
|
||||
|
||||
<!-- PlaceholderAsLabel: Input relying only on placeholder -->
|
||||
<input type="text" placeholder="Search website...">
|
||||
|
||||
<!-- ImageAltRedundant: Redundant words in alt text -->
|
||||
<img src="logo2.png" alt="logo of our company">
|
||||
<img src="profile.jpg" alt="profile photo of john doe">
|
||||
|
||||
<!-- AriaHiddenFocusable: Focusable element hidden with aria-hidden="true" -->
|
||||
<div aria-hidden="true">
|
||||
<button type="button">Hidden click</button>
|
||||
</div>
|
||||
<a href="#" aria-hidden="true">Hidden link</a>
|
||||
|
||||
<p>This page has multiple accessibility issues.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class AriaHiddenFocusable extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_ARIA_HIDDEN_FOCUS'; }
|
||||
public function getDescription(): string { return 'Focusable elements must not be hidden with aria-hidden="true".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if ($element->getAttribute('aria-hidden') !== 'true') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($this->isFocusable($element) || $this->hasFocusableDescendant($element)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove aria-hidden="true" from the container or make its focusable elements non-focusable (e.g. by setting tabindex="-1" or disabled).'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function isFocusable(DOMElement $element): bool
|
||||
{
|
||||
$tagName = strtolower($element->tagName);
|
||||
|
||||
if ($element->hasAttribute('tabindex')) {
|
||||
$tabindex = (int)$element->getAttribute('tabindex');
|
||||
if ($tabindex < 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($tagName === 'a' && $element->hasAttribute('href')) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (in_array($tagName, ['button', 'select', 'textarea'], true)) {
|
||||
return !$element->hasAttribute('disabled');
|
||||
}
|
||||
|
||||
if ($tagName === 'input') {
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
return $type !== 'hidden' && !$element->hasAttribute('disabled');
|
||||
}
|
||||
|
||||
if ($tagName === 'iframe') {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function hasFocusableDescendant(DOMElement $element): bool
|
||||
{
|
||||
$doc = $element->ownerDocument;
|
||||
if ($doc === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$xpath = new \DOMXPath($doc);
|
||||
$descendants = $xpath->query('.//*', $element);
|
||||
if ($descendants === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach ($descendants as $descendant) {
|
||||
if ($descendant instanceof DOMElement && $this->isFocusable($descendant)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ImageAltRedundant extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_1_1_ALT_REDUNDANT'; }
|
||||
public function getDescription(): string { return 'Image alt text should not contain redundant words like "image" or "photo".'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
|
||||
/** @var array<int, string> */
|
||||
private array $redundantWords = [
|
||||
'resmi', 'görüntüsü', 'fotoğrafı', 'grafiği', 'logosu', 'ikonu',
|
||||
'image of', 'photo of', 'picture of', 'graphic of', 'logo of', 'icon of'
|
||||
];
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'img') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('alt')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$alt = trim($element->getAttribute('alt'));
|
||||
$cleanAlt = strtolower(preg_replace('/\s+/', ' ', $alt) ?? '');
|
||||
|
||||
if ($cleanAlt === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach ($this->redundantWords as $word) {
|
||||
if (str_starts_with($cleanAlt, $word) || str_contains($cleanAlt, ' ' . $word)) {
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Remove redundant phrases like "' . $word . '" from the alt text. Screen readers already announce elements as images.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class LinkTextGeneric extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_2_4_4_LINK_GENERIC'; }
|
||||
public function getDescription(): string { return 'Links must have descriptive text. Generic text like "click here" or "read more" is not accessible.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
|
||||
/** @var array<int, string> */
|
||||
private array $genericWords = [
|
||||
'click here', 'click', 'read more', 'more', 'here', 'download', 'go', 'details', 'link', 'website', 'page', 'info',
|
||||
'tıkla', 'tıklayın', 'devamı', 'devamını oku', 'indir', 'git', 'detay', 'detaylar', 'link', 'site', 'sayfa', 'bilgi'
|
||||
];
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'a') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim($element->textContent);
|
||||
$cleanText = strtolower(preg_replace('/\s+/', ' ', $text) ?? '');
|
||||
|
||||
if (in_array($cleanText, $this->genericWords, true)) {
|
||||
// Check for descriptive attributes
|
||||
if ($element->hasAttribute('aria-label') || $element->hasAttribute('aria-labelledby') || $element->hasAttribute('title')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createViolation(
|
||||
$element,
|
||||
$this->getDescription(),
|
||||
'Change the link text to describe its target, or add an aria-label attribute to provide context.'
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class PlaceholderAsLabel extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_3_3_2_PLACEHOLDER_LABEL'; }
|
||||
public function getDescription(): string { return 'Inputs should not use placeholder text as their only label.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tags = ['input', 'select', 'textarea'];
|
||||
if (!in_array(strtolower($element->tagName), $tags, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip hidden inputs, buttons, etc.
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
if ($type === 'hidden' || $type === 'submit' || $type === 'button' || $type === 'reset') {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If it doesn't have a placeholder, this rule doesn't apply (FormLabel handles general missing labels)
|
||||
if (!$element->hasAttribute('placeholder')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if there is an aria-label or aria-labelledby
|
||||
if ($element->hasAttribute('aria-label') || $element->hasAttribute('aria-labelledby')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for associated label element
|
||||
if ($element->hasAttribute('id')) {
|
||||
$id = $element->getAttribute('id');
|
||||
$doc = $element->ownerDocument;
|
||||
if ($doc !== null) {
|
||||
$xpath = new \DOMXPath($doc);
|
||||
$labels = $xpath->query("//label[@for='$id']");
|
||||
if ($labels !== false && $labels->length > 0) {
|
||||
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. Placeholder text is not a reliable label replacement.'
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,10 @@ class StandardRuleSet
|
||||
new DuplicateId(),
|
||||
new AriaRole(),
|
||||
new IframeTitle(),
|
||||
new LinkTextGeneric(),
|
||||
new PlaceholderAsLabel(),
|
||||
new ImageAltRedundant(),
|
||||
new AriaHiddenFocusable(),
|
||||
];
|
||||
return $rules;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,17 @@ class SourceLocator
|
||||
|
||||
try {
|
||||
$directory = new RecursiveDirectoryIterator($this->searchPath, \FilesystemIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
|
||||
$filter = new \RecursiveCallbackFilterIterator($directory, function ($current) {
|
||||
/** @var SplFileInfo $current */
|
||||
if ($current->isDir()) {
|
||||
$filename = $current->getFilename();
|
||||
if ($filename === 'vendor' || $filename === '.git' || $filename === 'node_modules') {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
});
|
||||
$iterator = new RecursiveIteratorIterator($filter, RecursiveIteratorIterator::SELF_FIRST);
|
||||
} catch (\UnexpectedValueException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user