Compare commits

...
5 Commits
Author SHA1 Message Date
Enes Yakıştır 7f3bf3cb7d feat: add 5 new WCAG accessibility rules and bump version to 3.0.5 2026-07-08 12:38:47 +03:00
Enes Yakıştır 8e6ea62ba7 chore: bump version to v3.0.4 and fix Symfony 8 compatibility 2026-07-07 13:00:44 +03:00
Enes Yakıştır c138cbca65 chore: remove BOM from composer.json 2026-07-07 12:59:20 +03:00
Enes Yakıştır 18aa4641d2 chore: bump version to v3.0.3 and update configurations 2026-07-07 12:47:34 +03:00
Enes Yakıştır c4745d6cb3 fix: resolve all WCAG 2.1 violations in demo/broken.html and bump version to 3.0.2
- Added lang attribute to html element (WCAG 3.1.1)
- Added descriptive page title (WCAG 2.4.2)
- Added alt attributes to all images (WCAG 1.1.1)
- Removed redundant alt text words (WCAG 1.1.1)
- Fixed empty link and button with discernible text (WCAG 2.4.4, 4.1.2)
- Removed positive tabindex value (WCAG 2.4.3)
- Eliminated duplicate id=main-content (WCAG 4.1.1)
- Fixed insufficient color contrast ratio (WCAG 1.4.3)
- Replaced invalid ARIA role cool-widget with region (WCAG 4.1.2)
- Added title attribute to iframe (WCAG 4.1.2)
- Replaced generic link texts with descriptive ones (WCAG 2.4.4)
- Added associated label for placeholder-only input (WCAG 3.3.2)
- Removed aria-hidden from focusable elements (WCAG 4.1.2)
- Added non-empty heading text (WCAG 2.4.6)
- Added video captions track element (WCAG 1.2.2)
- Added non-empty label text (WCAG 1.3.1)
- Added tabindex=0 to role=button div (WCAG 2.1.1)
2026-06-23 01:25:08 +03:00
10 changed files with 1427 additions and 57 deletions
+4 -4
View File
@@ -19,9 +19,9 @@ use YakNet\AccessibilityConsole\Console\Command\RulesCommand;
$application = new Application('YakNet Accessibility Console', '3.0.1'); $application = new Application('YakNet Accessibility Console', '3.0.1');
// Add commands // Add commands
$application->add(new ScanCommand()); $application->addCommand(new ScanCommand());
$application->add(new FixCommand()); $application->addCommand(new FixCommand());
$application->add(new InitCommand()); $application->addCommand(new InitCommand());
$application->add(new RulesCommand()); $application->addCommand(new RulesCommand());
$application->run(); $application->run();
+5 -4
View File
@@ -12,7 +12,7 @@
"yaknet" "yaknet"
], ],
"homepage": "https://forum.yak.net.tr", "homepage": "https://forum.yak.net.tr",
"version": "3.0.1", "version": "3.0.5",
"type": "library", "type": "library",
"license": "MIT", "license": "MIT",
"authors": [ "authors": [
@@ -27,11 +27,11 @@
"ext-dom": "*", "ext-dom": "*",
"ext-libxml": "*", "ext-libxml": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"symfony/console": "^6.4|^7.0", "symfony/console": "^6.4|^7.0|^8.0",
"symfony/yaml": "^6.4|^7.0", "symfony/yaml": "^6.4|^7.0|^8.0",
"monolog/monolog": "^3.0", "monolog/monolog": "^3.0",
"vlucas/phpdotenv": "^5.6", "vlucas/phpdotenv": "^5.6",
"google-gemini-php/client": "^1.0", "google-gemini-php/client": "^1.0|^2.0",
"guzzlehttp/guzzle": "^7.8", "guzzlehttp/guzzle": "^7.8",
"masterminds/html5": "^2.8", "masterminds/html5": "^2.8",
"sabberworm/php-css-parser": "^9.4", "sabberworm/php-css-parser": "^9.4",
@@ -57,3 +57,4 @@
} }
} }
} }
+99 -44
View File
@@ -1,66 +1,121 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html lang="tr"><!-- FIXED: Added lang attribute (WCAG 3.1.1) -->
<head> <head>
<title></title> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Erişilebilir Demo Sayfa</title><!-- FIXED: Non-empty page title (WCAG 2.4.2) -->
<style>
body {
font-family: sans-serif;
margin: 0;
padding: 2rem;
background: #ffffff;
color: #1a1a1a; /* FIXED: High contrast text (WCAG 1.4.3) */
line-height: 1.6;
}
/* FIXED: Sufficient contrast (WCAG 1.4.3) - was #888 on #999, now dark on white */
.low-contrast-example {
color: #595959; /* 7:1 contrast on white */
background-color: #ffffff;
padding: 0.5rem;
}
label {
display: block;
margin-bottom: 0.25rem;
font-weight: 600;
}
input {
padding: 0.4rem;
border: 1px solid #ccc;
border-radius: 4px;
width: 100%;
max-width: 300px;
}
iframe {
border: none;
}
video {
display: block;
margin-top: 0.5rem;
}
[role="button"] {
cursor: pointer;
}
</style>
</head> </head>
<body> <body>
<h1 id="main-content">Welcome to a broken page</h1> <h1 id="main-content">Erişilebilir Demo Sayfa</h1>
<!-- FIXED: Removed duplicate id="main-content" (WCAG 4.1.1) -->
<!-- Missing alt attribute --> <!-- FIXED: Added alt attribute (WCAG 1.1.1) -->
<img src="logo.png"> <img src="logo.png" alt="YakNet Şirket">
<!-- Empty link --> <!-- FIXED: Added descriptive link text (WCAG 2.4.4) -->
<a href="https://google.com"></a> <a href="https://google.com">Google Ana Sayfasına Git</a>
<!-- Empty button --> <!-- FIXED: Added discernible button text (WCAG 4.1.2) -->
<button type="submit"></button> <button type="submit">Formu Gönder</button>
<!-- Positive tabindex --> <!-- FIXED: Removed positive tabindex (WCAG 2.4.3) — use natural DOM order -->
<a href="/keyboard" tabindex="5">Keyboard test</a> <a href="/keyboard">Klavye Testi</a>
<!-- Duplicate ID --> <!-- FIXED: Removed duplicate id="main-content" — unique wrapper instead -->
<div id="main-content">This is a duplicate main-content ID div!</div> <div id="duplicate-content-wrapper">Bu bölümün içeriği buradadır.</div>
<!-- Low Contrast --> <!-- FIXED: High contrast text (WCAG 1.4.3) -->
<p style="color: #888888; background-color: #999999;">This text has extremely low contrast ratio!</p> <p class="low-contrast-example">Bu metnin kontrast oranı WCAG AA gereksinimlerini karşılamaktadır.</p>
<!-- Invalid ARIA role and missing controls target --> <!-- FIXED: Valid WAI-ARIA role (WCAG 4.1.2) and removed reference to non-existent element -->
<div role="cool-widget" aria-controls="non-existent-popup">WAI-ARIA elements</div> <div role="region" aria-label="WAI-ARIA Örnek Bölüm">WAI-ARIA bölüm örneği</div>
<!-- Missing Iframe Title --> <!-- FIXED: Added descriptive title to iframe (WCAG 4.1.2) -->
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" width="560" height="315"></iframe> <iframe
src="https://www.youtube.com/embed/aqz-KE-bpKQ"
width="560"
height="315"
title="YouTube Video: Örnek Video İçeriği"
></iframe>
<!-- New WCAG Rule Violations for testing --> <!-- FIXED: Descriptive link text instead of generic "click here" / "devamını oku" (WCAG 2.4.4) -->
<a href="/read-more">Makaleyi Devamı: WCAG Erişilebilirlik Kılavuzu</a>
<a href="/devam-oku">Şiir Serisinin Devamını Oku: "Bahar Rüzgarı"</a>
<!-- LinkTextGeneric: Generic text without context --> <!-- FIXED: Added associated <label> element (WCAG 1.3.1 & 3.3.2) -->
<a href="/read-more">Click here</a> <div>
<a href="/devam-oku">devamını oku</a> <label for="search-input">Web sitesinde ara</label>
<input type="text" id="search-input" placeholder="Arama teriminizi girin...">
<!-- 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> </div>
<a href="#" aria-hidden="true">Hidden link</a>
<!-- Empty heading violation --> <!-- FIXED: Removed redundant words "logo" and "photo" from alt texts (WCAG 1.1.1) -->
<h2></h2> <img src="logo2.png" alt="Şirketimizin simgesi">
<img src="profile.jpg" alt="John Doe vesikalık">
<!-- Video missing track violation --> <!-- FIXED: Removed aria-hidden="true" from focusable elements (WCAG 4.1.2) -->
<video src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" width="320" height="240" controls></video> <div>
<button type="button">Gizli Olmayan Tıklanabilir Düğme</button>
</div>
<a href="#">Normal Bağlantı</a>
<!-- Empty label violation --> <!-- FIXED: Added non-empty heading text (WCAG 2.4.6) -->
<label for="search-input"></label> <h2>Alt Bölüm Başlığı</h2>
<!-- AriaFocusable violation --> <!-- FIXED: Added <track> element for video captions (WCAG 1.2.2) -->
<div role="button">Clickable div button without tabindex</div> <video
src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
width="320"
height="240"
controls
>
<track kind="captions" src="captions.vtt" srclang="tr" label="Türkçe Altyazı" default>
</video>
<p>This page has multiple accessibility issues.</p> <!-- FIXED: Added non-empty label text (WCAG 1.3.1) -->
<label for="name-input">Ad Soyad</label>
<input type="text" id="name-input" name="name">
<!-- FIXED: Added tabindex="0" to make role="button" keyboard focusable (WCAG 2.1.1) -->
<div role="button" tabindex="0">Tıklanabilir Div Düğme (Erişilebilir)</div>
<p>Bu sayfa WCAG 2.1 erişilebilirlik standartlarına uygun olarak düzenlenmiştir.</p>
</body> </body>
</html> </html>
+18 -2
View File
@@ -1,9 +1,25 @@
<html> <html>
<head> <head>
<title>Test Page</title> <title>Test Page</title>
<meta http-equiv="refresh" content="30">
</head> </head>
<body> <body>
<img src="avatar.png"> <img src="avatar.png" alt="Avatar">
<button></button> <button>Submit</button>
<a href="#">Click here for nothing</a>
<a href="javascript:void(0)">Do something</a>
<label for="nonexistent-id">Broken Label</label>
<table>
<tr>
<th>Header 1</th>
<th>Header 2</th>
</tr>
<tr>
<td scope="row">Data 1</td>
<td>Data 2</td>
</tr>
</table>
</body> </body>
</html> </html>
+50
View File
@@ -0,0 +1,50 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class LabelTargetExists extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_LABEL_TARGET'; }
public function getDescription(): string { return 'Label elements with a \'for\' attribute must target an existing element ID.'; }
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) !== 'label') {
return null;
}
if ($element->hasAttribute('for')) {
$forId = trim($element->getAttribute('for'));
if ($forId === '') {
return $this->createViolation(
$element,
$this->getDescription(),
'Provide a valid element ID in the for attribute.'
);
}
$doc = $element->ownerDocument;
if ($doc !== null) {
$xpath = new \DOMXPath($doc);
$elements = $xpath->query('//*[@id="' . $forId . '"]');
if ($elements === false || $elements->length === 0) {
return $this->createViolation(
$element,
$this->getDescription(),
"Ensure the 'for' attribute value (\"$forId\") matches the ID of a form input element."
);
}
}
}
return null;
}
}
+39
View File
@@ -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 LinkHrefJavascript extends AbstractRule
{
public function getId(): string { return 'WCAG_2_1_1_LINK_JAVASCRIPT'; }
public function getDescription(): string { return 'Links must not use javascript: void(0) or # as their href. Use buttons for scripting actions.'; }
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) !== 'a') {
return null;
}
if ($element->hasAttribute('href')) {
$href = trim($element->getAttribute('href'));
// Check for javascript: links or '#' (which represents a placeholder/scripting target)
if (str_starts_with(strtolower($href), 'javascript:') || $href === '#') {
return $this->createViolation(
$element,
$this->getDescription(),
'Convert the link to a <button> element or provide a valid destination URL.'
);
}
}
return null;
}
}
+37
View File
@@ -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 MetaRefreshBanned extends AbstractRule
{
public function getId(): string { return 'WCAG_2_2_1_META_REFRESH'; }
public function getDescription(): string { return 'Do not use meta http-equiv="refresh" for automatic page redirects or updates as it disorients users.'; }
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) !== 'meta') {
return null;
}
if ($element->hasAttribute('http-equiv')) {
$httpEquiv = trim($element->getAttribute('http-equiv'));
if (strtolower($httpEquiv) === 'refresh') {
return $this->createViolation(
$element,
$this->getDescription(),
'Use server-side redirects (e.g. HTTP 301/302) or standard JavaScript routing instead of meta refresh.'
);
}
}
return null;
}
}
+34
View File
@@ -0,0 +1,34 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class ScopeOnlyOnHeaders extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_TABLE_SCOPE'; }
public function getDescription(): string { return 'The scope attribute is only valid on th elements, not td elements.'; }
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) !== 'td') {
return null;
}
if ($element->hasAttribute('scope')) {
return $this->createViolation(
$element,
$this->getDescription(),
'Remove the scope attribute from the td element or change the element to th if it is a header.'
);
}
return null;
}
}
+60
View File
@@ -0,0 +1,60 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class TableCaption extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_TABLE_CAPTION'; }
public function getDescription(): string { return 'Data tables should have a caption element or appropriate ARIA description labels.'; }
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) !== 'table') {
return null;
}
// Data tables usually contain headers (<th> elements). If it is a layout table, this rule is not mandatory.
$thElements = $element->getElementsByTagName('th');
if ($thElements->length === 0) {
return null;
}
// Check if there is a <caption> direct child
$hasCaption = false;
$captionElements = $element->getElementsByTagName('caption');
foreach ($captionElements as $caption) {
if ($caption->parentNode === $element && trim($caption->textContent) !== '') {
$hasCaption = true;
break;
}
}
if ($hasCaption) {
return null;
}
// Check for aria-label or aria-labelledby or summary attributes
$hasAriaLabel = ($element->hasAttribute('aria-label') && trim($element->getAttribute('aria-label')) !== '') ||
($element->hasAttribute('aria-labelledby') && trim($element->getAttribute('aria-labelledby')) !== '');
$hasSummary = $element->hasAttribute('summary') && trim($element->getAttribute('summary')) !== '';
if ($hasAriaLabel || $hasSummary) {
return null;
}
return $this->createViolation(
$element,
$this->getDescription(),
'Add a <caption> element directly inside the <table> or use the aria-label attribute to describe the table.'
);
}
}
File diff suppressed because it is too large Load Diff