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
Major upgrade v2.0.0
This commit is contained in:
@@ -0,0 +1 @@
|
||||
GEMINI_API_KEY=your_api_key_here
|
||||
@@ -0,0 +1,6 @@
|
||||
/vendor/
|
||||
/.env
|
||||
/composer.lock
|
||||
/rapor.html
|
||||
/.phpunit.result.cache
|
||||
/phpstan-baseline.neon
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 YakNet Bilişim <[email protected]>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,96 @@
|
||||
# YakNet Accessibility Console 2.0 (AI-Powered)
|
||||
|
||||
[](https://php.net)
|
||||
[](LICENSE)
|
||||
[](https://gemini.google.com)
|
||||
|
||||
**YakNet Accessibility Console**, web projelerinizdeki erişilebilirlik (WCAG 2.1) hatalarını sadece tespit etmekle kalmayan, aynı zamanda **Yapay Zeka (AI)** desteğiyle bu hataları otomatik olarak düzeltebilen (Self-Healing) profesyonel bir PHP kütüphanesidir.
|
||||
|
||||
## 🌟 Öne Çıkan Özellikler
|
||||
|
||||
- **🤖 AI Self-Healing:** Tespit edilen WCAG hataları için Google Gemini AI üzerinden akıllı çözüm önerileri alır ve isterseniz kaynak kodunuzu (Blade, Twig, PHP) otomatik olarak tamir eder.
|
||||
- **⚡ Modern CLI:** Rust/Vite tarzı şık bir komut satırı arayüzü ile saniyeler içinde tarama ve düzeltme yapın.
|
||||
- **📊 Görsel Dashboard:** Tarama sonuçlarını modern ve şık bir HTML raporu olarak dışarı aktarın.
|
||||
- **🔍 Akıllı Kaynak Tespiti:** Hataları projenizdeki tam dosya yolu ve satır numarası ile eşleştirir.
|
||||
- **🛠️ Geliştirici Dostu:** PSR-4 uyumlu, modern PHP 8.2+ mimarisi ve genişletilebilir kural motoru.
|
||||
|
||||
## 📦 Kurulum
|
||||
|
||||
Composer ile projenize hemen dahil edin:
|
||||
|
||||
```bash
|
||||
composer require yaknet/accessibility-console
|
||||
```
|
||||
|
||||
## 🚀 CLI Kullanımı
|
||||
|
||||
Kütüphane, `vendor/bin/a11y` (veya kütüphane dizininde `bin/a11y`) üzerinden kullanılabilir.
|
||||
|
||||
### 1. Standart Tarama
|
||||
Bir URL'yi veya yerel bir HTML dosyasını tarayın:
|
||||
```bash
|
||||
bin/a11y scan https://test-siteniz.com
|
||||
```
|
||||
|
||||
### 2. AI Destekli Tarama ve Raporlama
|
||||
Yapay zeka önerileriyle birlikte tarama yapın ve sonucu HTML olarak kaydedin:
|
||||
```bash
|
||||
bin/a11y scan https://test-siteniz.com --ai --report=sonuc.html
|
||||
```
|
||||
|
||||
### 3. Otomatik Düzeltme (Self-Healing) - **DEVRİMSEL**
|
||||
Tespit edilen hataları yapay zekaya düzelttirin ve dosyalarınızı güncelleyin:
|
||||
```bash
|
||||
bin/a11y fix https://test-siteniz.com --project-path=/var/www/projeniz
|
||||
```
|
||||
|
||||
## 💻 Kütüphane Olarak Kullanım
|
||||
|
||||
Kendi PHP uygulamalarınızın içine entegre edin:
|
||||
|
||||
```php
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\StandardRuleSet;
|
||||
|
||||
// 1. Scanner'ı ve Kuralları Hazırla
|
||||
$scanner = new Scanner();
|
||||
foreach (StandardRuleSet::all() as $rule) {
|
||||
$scanner->addRule($rule);
|
||||
}
|
||||
|
||||
// 2. HTML İçeriğini Tara
|
||||
$html = file_get_contents('view.html');
|
||||
$violations = $scanner->scan($html);
|
||||
|
||||
// 3. Sonuçları İşle
|
||||
foreach ($violations as $v) {
|
||||
echo "Hata: {$v->message} (Kural: {$v->ruleId})\n";
|
||||
}
|
||||
```
|
||||
|
||||
## ⚙️ Yapılandırma
|
||||
|
||||
AI özelliklerini kullanabilmek için projenizin kök dizininde bir `.env` dosyası oluşturun ve **Google Gemini API** anahtarınızı ekleyin:
|
||||
|
||||
```env
|
||||
GEMINI_API_KEY=AIzaSyA...your_key_here
|
||||
```
|
||||
|
||||
## 🛠️ Mevcut Kurallar (WCAG 2.1)
|
||||
|
||||
- **A11Y_VIEWPORT:** Zoom engelleme kontrolü.
|
||||
- **WCAG_1_1_1_ALT:** Görsel alt metin kontrolü.
|
||||
- **WCAG_1_3_1_LABEL:** Form etiketi kontrolü.
|
||||
- **WCAG_1_3_1_FIELDSET:** Fieldset/Legend kontrolü.
|
||||
- **WCAG_1_3_1_HEADING:** Başlık hiyerarşisi kontrolü.
|
||||
- **WCAG_2_4_4_LINK_TEXT:** Boş link kontrolü.
|
||||
- **WCAG_3_1_1_LANG:** HTML dil etiketi kontrolü.
|
||||
- **WCAG_4_1_2_BUTTON:** Boş buton kontrolü.
|
||||
|
||||
## 🤝 Katkıda Bulunma
|
||||
|
||||
Bu proje açık kaynaklıdır ve katkılarınıza açıktır. Lütfen hata bildirimleri için `Issue` açmaktan veya yeni özellikler için `Pull Request` göndermekten çekinmeyin.
|
||||
|
||||
## 📜 Lisans
|
||||
|
||||
Bu yazılım **YakNet Bilişim** tarafından geliştirilmiştir ve **MIT Lisansı** altında lisanslanmıştır. Detaylar için [LICENSE](LICENSE) dosyasına göz atabilirsiniz.
|
||||
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env php
|
||||
<?php
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
use Symfony\Component\Console\Application;
|
||||
use YakNet\AccessibilityConsole\Console\Command\ScanCommand;
|
||||
use YakNet\AccessibilityConsole\Console\Command\FixCommand;
|
||||
|
||||
$application = new Application('YakNet Accessibility Console', '2.0.0');
|
||||
|
||||
// Add commands
|
||||
$application->add(new ScanCommand());
|
||||
$application->add(new FixCommand());
|
||||
|
||||
$application->run();
|
||||
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"name": "yaknet/accessibility-console",
|
||||
"description": "A professional AI-powered WCAG 2.1 accessibility scanner with self-healing capabilities.",
|
||||
"keywords": [
|
||||
"accessibility",
|
||||
"wcag",
|
||||
"a11y",
|
||||
"scanner",
|
||||
"ai",
|
||||
"gemini",
|
||||
"self-healing",
|
||||
"yaknet"
|
||||
],
|
||||
"homepage": "https://forum.yak.net.tr",
|
||||
"type": "library",
|
||||
"license": "MIT",
|
||||
"authors": [
|
||||
{
|
||||
"name": "YakNet Bilişim",
|
||||
"email": "[email protected]",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"ext-dom": "*",
|
||||
"ext-libxml": "*",
|
||||
"ext-mbstring": "*",
|
||||
"symfony/console": "^6.4|^7.0",
|
||||
"symfony/yaml": "^6.4|^7.0",
|
||||
"monolog/monolog": "^3.0",
|
||||
"vlucas/phpdotenv": "^5.6",
|
||||
"google-gemini-php/client": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "^10.5",
|
||||
"phpstan/phpstan": "^1.10"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"YakNet\\AccessibilityConsole\\": "src/"
|
||||
}
|
||||
},
|
||||
"bin": [
|
||||
"bin/a11y"
|
||||
],
|
||||
"minimum-stability": "stable",
|
||||
"prefer-stable": true
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Broken Page</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Welcome to a broken page</h1>
|
||||
|
||||
<!-- Missing alt attribute -->
|
||||
<img src="logo.png">
|
||||
|
||||
<!-- Empty link -->
|
||||
<a href="https://google.com"></a>
|
||||
|
||||
<!-- Empty button -->
|
||||
<button type="submit"></button>
|
||||
|
||||
<p>This page has multiple accessibility issues.</p>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,195 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Dynamic Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container" style="padding: 0;">
|
||||
<h1>My Dynamic Site</h1>
|
||||
<nav>
|
||||
<a href="?page=home">Home</a>
|
||||
<a href="?page=contact">Contact</a>
|
||||
<a href="?page=about">About</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h2>Latest Products</h2>
|
||||
<p>Welcome to our dynamic product catalog. (Intentionally filled with errors)</p>
|
||||
|
||||
|
||||
<div class="products">
|
||||
<div class="card">
|
||||
<!-- ERROR: Dynamic image without alt text -->
|
||||
<img src="images/gadget.jpg" width="100">
|
||||
<h3>Super Gadget</h3>
|
||||
<p>$99</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
<!-- Error: Button might need specific accessible name if icon-only, but text is okay here. -->
|
||||
</div>
|
||||
<div class="card">
|
||||
<!-- ERROR: Dynamic image without alt text -->
|
||||
<img src="images/widget.png" width="100">
|
||||
<h3>Mega Widget</h3>
|
||||
<p>$49</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
<!-- Error: Button might need specific accessible name if icon-only, but text is okay here. -->
|
||||
</div>
|
||||
<div class="card">
|
||||
<!-- ERROR: Dynamic image without alt text -->
|
||||
<img src="images/thing.jpg" width="100">
|
||||
<h3>Ultra Thing</h3>
|
||||
<p>$199</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
<!-- Error: Button might need specific accessible name if icon-only, but text is okay here. -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Sponsors</h3>
|
||||
<div>
|
||||
<!-- ERROR: Decorative image causing noise, missing alt="" -->
|
||||
<img src="https://via.placeholder.com/150">
|
||||
<img src="https://via.placeholder.com/150">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer style="text-align: center; padding: 2rem; background: #f4f4f4; margin-top: 2rem;">
|
||||
© 2026 YakNet Demo
|
||||
</footer>
|
||||
|
||||
<div id='yaknet-a11y-console-toggle' onclick='document.getElementById("yaknet-a11y-console").style.display="block"'
|
||||
style='position:fixed; bottom:10px; right:10px; background:#ff9900; color:white; padding:8px 15px; border-radius:4px; cursor:pointer; font-family:sans-serif; z-index:99999; font-weight:bold; font-size:14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); border: 1px solid #cc7a00;'>
|
||||
⚠️ Accessibility: 5 Errors
|
||||
</div>
|
||||
|
||||
<div id='yaknet-a11y-console'
|
||||
style='display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:100000; font-family:Verdana, sans-serif;'>
|
||||
<div
|
||||
style='width:90%; max-width:1200px; margin:50px auto; background:#fff; box-shadow:0 0 20px rgba(0,0,0,0.5); overflow:hidden; border-radius:4px;'>
|
||||
|
||||
<!-- Header imitating PHP Error -->
|
||||
<div
|
||||
style='background:#f7f7f7; color:#333; padding:15px 20px; border-bottom:1px solid #ccc; font-size:18px; font-weight:bold; display:flex; justify-content:space-between; align-items:center;'>
|
||||
<span>
|
||||
<span
|
||||
style='background:#d9534f; color:white; padding:2px 6px; border-radius:3px; font-size:14px; vertical-align:middle; margin-right:10px;'>FATAL
|
||||
ERROR</span>
|
||||
Accessibility Violations Detected
|
||||
</span>
|
||||
<button onclick='document.getElementById("yaknet-a11y-console").style.display="none"'
|
||||
style='border:none; background:none; font-size:24px; cursor:pointer; color:#999; line-height:1;'>×</button>
|
||||
</div>
|
||||
|
||||
<div style='max-height:80vh; overflow-y:auto; padding:0;'>
|
||||
<table style='width:100%; border-collapse:collapse; font-size:14px;'>
|
||||
<thead style='background:#eeeff0; color:#333; text-align:left;'>
|
||||
<tr>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd; width: 50px;'>#</th>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd;'>Violation / Message</th>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd;'>Source File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style='border-bottom:1px solid #eee; background:#fff;'>
|
||||
<td style='padding:12px 15px; vertical-align:top; color:#999;'>1</td>
|
||||
<td style='padding:12px 15px; vertical-align:top;'>
|
||||
<div style='color:#cc0000; font-weight:bold; margin-bottom:5px; font-size:15px;'>Image
|
||||
is missing alt attribute.</div>
|
||||
<div style='font-size:12px; color:#555; margin-bottom:8px;'>Rule:
|
||||
<code>img-alt-text</code></div>
|
||||
<div
|
||||
style='background:#f1f1f1; padding:8px; border-left:3px solid #ccc; font-family:monospace; font-size:12px; margin-bottom:8px; color:#333; overflow-x:auto;'>
|
||||
<img src="images/gadget.jpg" width="100">
|
||||
</div>
|
||||
<div style='font-size:13px; color:#222;'>
|
||||
<span style='color:#28a745; font-weight:bold;'>Fix:</span> Add an alt="" attribute
|
||||
describing the image, or alt="" if decorative.
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style='padding:12px 15px; vertical-align:top; font-family:monospace; font-size:13px; color:#333;'>
|
||||
<div style='margin-bottom:5px;'>
|
||||
home.php<strong>:14</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style='border-bottom:1px solid #eee; background:#f9f9f9;'>
|
||||
<td style='padding:12px 15px; vertical-align:top; color:#999;'>2</td>
|
||||
<td style='padding:12px 15px; vertical-align:top;'>
|
||||
<div style='color:#cc0000; font-weight:bold; margin-bottom:5px; font-size:15px;'>Image
|
||||
is missing alt attribute.</div>
|
||||
<div style='font-size:12px; color:#555; margin-bottom:8px;'>Rule:
|
||||
<code>img-alt-text</code></div>
|
||||
<div
|
||||
style='background:#f1f1f1; padding:8px; border-left:3px solid #ccc; font-family:monospace; font-size:12px; margin-bottom:8px; color:#333; overflow-x:auto;'>
|
||||
<img src="images/widget.png" width="100">
|
||||
</div>
|
||||
<div style='font-size:13px; color:#222;'>
|
||||
<span style='color:#28a745; font-weight:bold;'>Fix:</span> Add an alt="" attribute
|
||||
describing the image, or alt="" if decorative.
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style='padding:12px 15px; vertical-align:top; font-family:monospace; font-size:13px; color:#333;'>
|
||||
<div style='margin-bottom:5px;'>
|
||||
home.php<strong>:14</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,112 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr"> <!-- FIXED: Added lang attribute -->
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Dynamic Demo (Fixed)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container" style="padding: 0;">
|
||||
<h1>My Dynamic Site (Accessible)</h1>
|
||||
<nav>
|
||||
<a href="?page=home">Home</a>
|
||||
<a href="?page=contact">Contact</a>
|
||||
<a href="?page=about">About</a>
|
||||
<!-- FIXED: Added aria-label to empty link, or removed it/added text -->
|
||||
<a href="#" class="btn" aria-label="Profile">👤</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<h2>Latest Products</h2>
|
||||
<p>Welcome to our accessible product catalog.</p>
|
||||
|
||||
|
||||
<div class="products">
|
||||
<div class="card">
|
||||
<!-- FIXED: Added alt attribute describing the product -->
|
||||
<img src="images/gadget.jpg" width="100" alt="Super Gadget">
|
||||
<h3>Super Gadget</h3>
|
||||
<p>$99</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<!-- FIXED: Added alt attribute describing the product -->
|
||||
<img src="images/widget.png" width="100" alt="Mega Widget">
|
||||
<h3>Mega Widget</h3>
|
||||
<p>$49</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
</div>
|
||||
<div class="card">
|
||||
<!-- FIXED: Added alt attribute describing the product -->
|
||||
<img src="images/thing.jpg" width="100" alt="Ultra Thing">
|
||||
<h3>Ultra Thing</h3>
|
||||
<p>$199</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Sponsors</h3>
|
||||
<div>
|
||||
<!-- FIXED: Added empty alt="" for decorative images -->
|
||||
<img src="https://via.placeholder.com/150" alt="">
|
||||
<img src="https://via.placeholder.com/150" alt="">
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<footer style="text-align: center; padding: 2rem; background: #f4f4f4; margin-top: 2rem;">
|
||||
© 2026 YakNet Demo
|
||||
</footer>
|
||||
<div
|
||||
style="position:fixed; bottom:10px; right:10px; background:#28a745; color:white; padding:8px 15px; border-radius:4px; font-family:sans-serif; font-weight:bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
|
||||
✅ Accessibility: 0 Errors</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,23 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Dynamic Demo (Fixed)</title>
|
||||
<!-- Styles omitted for brevity -->
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<!-- Content omitted for brevity -->
|
||||
<h1>My Dynamic Site (Accessible)</h1>
|
||||
<p>Success widget demonstration.</p>
|
||||
|
||||
<!-- The widget injected by HtmlReporter when setShowSuccess(true) is called -->
|
||||
<div id='yaknet-a11y-success' onclick='this.style.display="none"'
|
||||
style='position:fixed; bottom:10px; right:10px; background:#28a745; color:white; padding:8px 15px; border-radius:4px; font-family:sans-serif; font-weight:bold; box-shadow: 0 2px 5px rgba(0,0,0,0.2); cursor:pointer; z-index:9999;'>
|
||||
✅ Accessibility: 0 Errors
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
// 1. Setup Autoloading (Mocking Composer)
|
||||
require_once __DIR__ . '/../src/Scanner.php';
|
||||
require_once __DIR__ . '/../src/Violation.php';
|
||||
require_once __DIR__ . '/../src/SourceLocator.php';
|
||||
require_once __DIR__ . '/../src/Reporter/HtmlReporter.php';
|
||||
require_once __DIR__ . '/../src/Rule/RuleInterface.php';
|
||||
require_once __DIR__ . '/../src/Rules/ImgAltText.php';
|
||||
require_once __DIR__ . '/../src/Rules/FormLabel.php';
|
||||
require_once __DIR__ . '/../src/Rules/HtmlHasLang.php';
|
||||
require_once __DIR__ . '/../src/Rules/EmptyLink.php';
|
||||
|
||||
use YakNet\AccessibilityConsole\Scanner;
|
||||
use YakNet\AccessibilityConsole\SourceLocator;
|
||||
use YakNet\AccessibilityConsole\Reporter\HtmlReporter;
|
||||
use YakNet\AccessibilityConsole\Rules\ImgAltText;
|
||||
use YakNet\AccessibilityConsole\Rules\FormLabel;
|
||||
|
||||
// 2. Start Buffering to capture output
|
||||
ob_start();
|
||||
|
||||
// 3. Simple Router
|
||||
$page = $_GET['page'] ?? 'home';
|
||||
$viewFile = __DIR__ . "/views/{$page}.php";
|
||||
|
||||
if (!file_exists($viewFile)) {
|
||||
$viewFile = __DIR__ . '/views/home.php';
|
||||
}
|
||||
|
||||
// 4. Render Layout (which includes the view)
|
||||
$viewPath = $viewFile;
|
||||
include __DIR__ . '/views/layout.php';
|
||||
|
||||
// 5. Capture Output
|
||||
$html = ob_get_clean();
|
||||
|
||||
// 6. RUN ACCESSIBILITY SCANNER
|
||||
// ==========================================
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ImgAltText());
|
||||
$scanner->addRule(new FormLabel());
|
||||
$scanner->addRule(new \YakNet\AccessibilityConsole\Rules\HtmlHasLang());
|
||||
$scanner->addRule(new \YakNet\AccessibilityConsole\Rules\EmptyLink());
|
||||
|
||||
// Scan the captured HTML
|
||||
$violations = $scanner->scan($html);
|
||||
|
||||
if (!empty($violations)) {
|
||||
// Locate Sources
|
||||
// We point it to the 'views' directory so it knows where to look for the error source
|
||||
$locator = new SourceLocator(__DIR__ . '/views');
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$location = $locator->locate($violation->snippet);
|
||||
if ($location) {
|
||||
$violation->setSourceLocation($location['file'], $location['line']);
|
||||
}
|
||||
}
|
||||
|
||||
// Render Report
|
||||
$reporter = new HtmlReporter();
|
||||
$reportHtml = $reporter->render($violations);
|
||||
|
||||
// Inject into body
|
||||
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
||||
}
|
||||
|
||||
// 7. Output Final HTML
|
||||
echo $html;
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
// 1. Setup Autoloading
|
||||
require_once __DIR__ . '/../src/Scanner.php';
|
||||
require_once __DIR__ . '/../src/Violation.php';
|
||||
require_once __DIR__ . '/../src/SourceLocator.php';
|
||||
require_once __DIR__ . '/../src/Reporter/HtmlReporter.php';
|
||||
require_once __DIR__ . '/../src/Rule/RuleInterface.php';
|
||||
require_once __DIR__ . '/../src/Rules/ImgAltText.php';
|
||||
require_once __DIR__ . '/../src/Rules/FormLabel.php';
|
||||
require_once __DIR__ . '/../src/Rules/HtmlHasLang.php';
|
||||
require_once __DIR__ . '/../src/Rules/EmptyLink.php';
|
||||
|
||||
use YakNet\AccessibilityConsole\Scanner;
|
||||
use YakNet\AccessibilityConsole\SourceLocator;
|
||||
use YakNet\AccessibilityConsole\Reporter\HtmlReporter;
|
||||
use YakNet\AccessibilityConsole\Rules\ImgAltText;
|
||||
use YakNet\AccessibilityConsole\Rules\FormLabel;
|
||||
use YakNet\AccessibilityConsole\Rules\HtmlHasLang;
|
||||
use YakNet\AccessibilityConsole\Rules\EmptyLink;
|
||||
|
||||
// 2. Start Buffering
|
||||
ob_start();
|
||||
|
||||
// 3. Simple Router for Fixed Views
|
||||
$page = $_GET['page'] ?? 'home';
|
||||
$viewFile = __DIR__ . "/views-fixed/{$page}.php";
|
||||
|
||||
if (!file_exists($viewFile)) {
|
||||
$viewFile = __DIR__ . '/views-fixed/home.php';
|
||||
}
|
||||
|
||||
// 4. Render Layout
|
||||
$viewPath = $viewFile;
|
||||
include __DIR__ . '/views-fixed/layout.php';
|
||||
|
||||
// 5. Capture Output
|
||||
$html = ob_get_clean();
|
||||
|
||||
// 6. RUN ACCESSIBILITY SCANNER
|
||||
$scanner = new Scanner();
|
||||
$scanner->addRule(new ImgAltText());
|
||||
$scanner->addRule(new FormLabel());
|
||||
$scanner->addRule(new HtmlHasLang());
|
||||
$scanner->addRule(new EmptyLink());
|
||||
|
||||
$violations = $scanner->scan($html);
|
||||
|
||||
if (!empty($violations)) {
|
||||
// Locate Sources
|
||||
$locator = new SourceLocator(__DIR__ . '/views-fixed');
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$location = $locator->locate($violation->snippet);
|
||||
if ($location) {
|
||||
$violation->setSourceLocation($location['file'], $location['line']);
|
||||
}
|
||||
}
|
||||
|
||||
$reporter = new HtmlReporter();
|
||||
// Enable success widget for this demo
|
||||
$reporter->setShowSuccess(true);
|
||||
|
||||
$reportHtml = $reporter->render($violations);
|
||||
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
||||
} else {
|
||||
// Violations is empty, but render() will return the success widget because we set it to true
|
||||
$reporter = new HtmlReporter();
|
||||
$reporter->setShowSuccess(true);
|
||||
$reportHtml = $reporter->render($violations);
|
||||
|
||||
$html = str_replace('</body>', $reportHtml . '</body>', $html);
|
||||
}
|
||||
|
||||
echo $html;
|
||||
@@ -0,0 +1,102 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Accessibility Console Preview</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Accessibility Console Preview</h1>
|
||||
<p>This is a static preview of how the error console looks.</p>
|
||||
<p>Click the "Accessibility: 2 Errors" button in the bottom right.</p>
|
||||
|
||||
<!-- Hardcoded output from HtmlReporter for demonstration -->
|
||||
<div id='yaknet-a11y-console-toggle' onclick='document.getElementById("yaknet-a11y-console").style.display="block"'
|
||||
style='position:fixed; bottom:10px; right:10px; background:#ff9900; color:white; padding:8px 15px; border-radius:4px; cursor:pointer; font-family:sans-serif; z-index:99999; font-weight:bold; font-size:14px; box-shadow: 0 2px 5px rgba(0,0,0,0.2); border: 1px solid #cc7a00;'>
|
||||
⚠️ Accessibility: 2 Errors
|
||||
</div>
|
||||
|
||||
<div id='yaknet-a11y-console'
|
||||
style='display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(0,0,0,0.7); z-index:100000; font-family:Verdana, sans-serif;'>
|
||||
<div
|
||||
style='width:90%; max-width:1200px; margin:50px auto; background:#fff; box-shadow:0 0 20px rgba(0,0,0,0.5); overflow:hidden; border-radius:4px;'>
|
||||
|
||||
<div
|
||||
style='background:#f7f7f7; color:#333; padding:15px 20px; border-bottom:1px solid #ccc; font-size:18px; font-weight:bold; display:flex; justify-content:space-between; align-items:center;'>
|
||||
<span>
|
||||
<span
|
||||
style='background:#d9534f; color:white; padding:2px 6px; border-radius:3px; font-size:14px; vertical-align:middle; margin-right:10px;'>FATAL
|
||||
ERROR</span>
|
||||
Accessibility Violations Detected
|
||||
</span>
|
||||
<button onclick='document.getElementById("yaknet-a11y-console").style.display="none"'
|
||||
style='border:none; background:none; font-size:24px; cursor:pointer; color:#999; line-height:1;'>×</button>
|
||||
</div>
|
||||
|
||||
<div style='max-height:80vh; overflow-y:auto; padding:0;'>
|
||||
<table style='width:100%; border-collapse:collapse; font-size:14px;'>
|
||||
<thead style='background:#eeeff0; color:#333; text-align:left;'>
|
||||
<tr>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd; width: 50px;'>#</th>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd;'>Violation / Message</th>
|
||||
<th style='padding:12px 15px; border-bottom:2px solid #ddd;'>Source File</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr style='border-bottom:1px solid #eee; background:#fff;'>
|
||||
<td style='padding:12px 15px; vertical-align:top; color:#999;'>1</td>
|
||||
<td style='padding:12px 15px; vertical-align:top;'>
|
||||
<div style='color:#cc0000; font-weight:bold; margin-bottom:5px; font-size:15px;'>Image
|
||||
is missing alt attribute.</div>
|
||||
<div style='font-size:12px; color:#555; margin-bottom:8px;'>Rule:
|
||||
<code>img-alt-text</code></div>
|
||||
|
||||
<div
|
||||
style='background:#f1f1f1; padding:8px; border-left:3px solid #ccc; font-family:monospace; font-size:12px; margin-bottom:8px; color:#333; overflow-x:auto;'>
|
||||
<img src="logo.png" />
|
||||
</div>
|
||||
<div style='font-size:13px; color:#222;'>
|
||||
<span style='color:#28a745; font-weight:bold;'>Fix:</span> Add an alt="" attribute
|
||||
describing the image, or alt="" if decorative.
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style='padding:12px 15px; vertical-align:top; font-family:monospace; font-size:13px; color:#333;'>
|
||||
<div style='margin-bottom:5px;'>
|
||||
C:\Users\User\Project\views\header.php<strong>:42</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr style='border-bottom:1px solid #eee; background:#f9f9f9;'>
|
||||
<td style='padding:12px 15px; vertical-align:top; color:#999;'>2</td>
|
||||
<td style='padding:12px 15px; vertical-align:top;'>
|
||||
<div style='color:#cc0000; font-weight:bold; margin-bottom:5px; font-size:15px;'>Form
|
||||
field <input> missing label.</div>
|
||||
<div style='font-size:12px; color:#555; margin-bottom:8px;'>Rule:
|
||||
<code>form-label</code></div>
|
||||
|
||||
<div
|
||||
style='background:#f1f1f1; padding:8px; border-left:3px solid #ccc; font-family:monospace; font-size:12px; margin-bottom:8px; color:#333; overflow-x:auto;'>
|
||||
<input type="text" name="username" />
|
||||
</div>
|
||||
<div style='font-size:13px; color:#222;'>
|
||||
<span style='color:#28a745; font-weight:bold;'>Fix:</span> Associate a <label>
|
||||
via "for" attribute, wrap in <label>, or use "aria-label".
|
||||
</div>
|
||||
</td>
|
||||
<td
|
||||
style='padding:12px 15px; vertical-align:top; font-family:monospace; font-size:13px; color:#333;'>
|
||||
<div style='margin-bottom:5px;'>
|
||||
C:\Users\User\Project\views\login.php<strong>:15</strong>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,32 @@
|
||||
<h2>Contact Us</h2>
|
||||
<p>Get in touch with us.</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- FIXED: Wrapped in label -->
|
||||
<label>
|
||||
Name:
|
||||
<input type="text" name="fullname" placeholder="John Doe" style="padding: 8px; width: 100%;">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- FIXED: Added for/id association -->
|
||||
<label for="email">Email Address</label>
|
||||
<br>
|
||||
<input type="email" id="email" name="email" style="padding: 8px; width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label for="message">Message</label>
|
||||
<br>
|
||||
<textarea id="message" name="message" rows="5" style="width: 100%;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- FIXED: Added inner text or aria-label -->
|
||||
<a href="#" class="btn" aria-label="Send via WhatsApp">
|
||||
<i class="fa fa-send"></i> Send
|
||||
</a>
|
||||
|
||||
<button type="submit" class="btn">Send Message</button>
|
||||
</form>
|
||||
@@ -0,0 +1,36 @@
|
||||
<h2>Latest Products</h2>
|
||||
<p>Welcome to our accessible product catalog.</p>
|
||||
|
||||
<?php
|
||||
$products = [
|
||||
['id' => 1, 'name' => 'Super Gadget', 'image' => 'gadget.jpg', 'price' => '$99'],
|
||||
['id' => 2, 'name' => 'Mega Widget', 'image' => 'widget.png', 'price' => '$49'],
|
||||
['id' => 3, 'name' => 'Ultra Thing', 'image' => 'thing.jpg', 'price' => '$199'],
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="products">
|
||||
<?php foreach ($products as $product): ?>
|
||||
<div class="card">
|
||||
<!-- FIXED: Added alt attribute describing the product -->
|
||||
<img src="images/<?php echo $product['image']; ?>" width="100"
|
||||
alt="<?php echo htmlspecialchars($product['name']); ?>">
|
||||
<h3>
|
||||
<?php echo $product['name']; ?>
|
||||
</h3>
|
||||
<p>
|
||||
<?php echo $product['price']; ?>
|
||||
</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Sponsors</h3>
|
||||
<div>
|
||||
<!-- FIXED: Added empty alt="" for decorative images -->
|
||||
<img src="https://via.placeholder.com/150" alt="">
|
||||
<img src="https://via.placeholder.com/150" alt="">
|
||||
</div>
|
||||
@@ -0,0 +1,75 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="tr"> <!-- FIXED: Added lang attribute -->
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Dynamic Demo (Fixed)</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #28a745;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container" style="padding: 0;">
|
||||
<h1>My Dynamic Site (Accessible)</h1>
|
||||
<nav>
|
||||
<a href="?page=home">Home</a>
|
||||
<a href="?page=contact">Contact</a>
|
||||
<a href="?page=about">About</a>
|
||||
<!-- FIXED: Added aria-label to empty link, or removed it/added text -->
|
||||
<a href="#" class="btn" aria-label="Profile">👤</a>
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<?php include $viewPath; ?>
|
||||
</main>
|
||||
|
||||
<footer style="text-align: center; padding: 2rem; background: #f4f4f4; margin-top: 2rem;">
|
||||
©
|
||||
<?php echo date('Y'); ?> YakNet Demo
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,31 @@
|
||||
<h2>Contact Us</h2>
|
||||
<p>Get in touch with us.</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- ERROR: No label for input -->
|
||||
<div>Name:</div>
|
||||
<input type="text" name="fullname" placeholder="John Doe" style="padding: 8px; width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<!-- ERROR: Label exists but not associated with For/ID -->
|
||||
<label>Email Address</label>
|
||||
<br>
|
||||
<input type="email" name="email" style="padding: 8px; width: 100%;">
|
||||
</div>
|
||||
|
||||
<div style="margin-bottom: 1rem;">
|
||||
<label for="message">Message</label>
|
||||
<br>
|
||||
<textarea id="message" name="message" rows="5" style="width: 100%;"></textarea>
|
||||
</div>
|
||||
|
||||
<!-- ERROR: Submit button without clear text if it was an input type=image, but button is usually fine.
|
||||
Let's add a problematic icon link -->
|
||||
<a href="#" class="btn">
|
||||
<i class="fa fa-send"></i> <!-- Empty link text error (if we had that rule) -->
|
||||
</a>
|
||||
|
||||
<button type="submit" class="btn">Send Message</button>
|
||||
</form>
|
||||
@@ -0,0 +1,36 @@
|
||||
<h2>Latest Products</h2>
|
||||
<p>Welcome to our dynamic product catalog. (Intentionally filled with errors)</p>
|
||||
|
||||
<?php
|
||||
$products = [
|
||||
['id' => 1, 'name' => 'Super Gadget', 'image' => 'gadget.jpg', 'price' => '$99'],
|
||||
['id' => 2, 'name' => 'Mega Widget', 'image' => 'widget.png', 'price' => '$49'],
|
||||
['id' => 3, 'name' => 'Ultra Thing', 'image' => 'thing.jpg', 'price' => '$199'],
|
||||
];
|
||||
?>
|
||||
|
||||
<div class="products">
|
||||
<?php foreach ($products as $product): ?>
|
||||
<div class="card">
|
||||
<!-- ERROR: Dynamic image without alt text -->
|
||||
<img src="images/<?php echo $product['image']; ?>" width="100">
|
||||
<h3>
|
||||
<?php echo $product['name']; ?>
|
||||
</h3>
|
||||
<p>
|
||||
<?php echo $product['price']; ?>
|
||||
</p>
|
||||
<button class="btn">Buy Now</button>
|
||||
<!-- Error: Button might need specific accessible name if icon-only, but text is okay here. -->
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>Sponsors</h3>
|
||||
<div>
|
||||
<!-- ERROR: Decorative image causing noise, missing alt="" -->
|
||||
<img src="https://via.placeholder.com/150">
|
||||
<img src="https://via.placeholder.com/150">
|
||||
</div>
|
||||
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html> <!-- Missing lang attribute error -->
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>YakNet Dynamic Demo</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
header {
|
||||
background: #333;
|
||||
color: #fff;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
nav a {
|
||||
color: #fff;
|
||||
margin-right: 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2rem;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.btn {
|
||||
background: #007bff;
|
||||
color: white;
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<div class="container" style="padding: 0;">
|
||||
<h1>My Dynamic Site</h1>
|
||||
<nav>
|
||||
<a href="?page=home">Home</a>
|
||||
<a href="?page=contact">Contact</a>
|
||||
<a href="?page=about">About</a>
|
||||
<a href="#" class="btn"></a> <!-- Error: Empty Link -->
|
||||
</nav>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="container">
|
||||
<?php include $viewPath; ?>
|
||||
</main>
|
||||
|
||||
<footer style="text-align: center; padding: 2rem; background: #f4f4f4; margin-top: 2rem;">
|
||||
©
|
||||
<?php echo date('Y'); ?> YakNet Demo
|
||||
</footer>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\AI;
|
||||
|
||||
use Gemini;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
|
||||
class GeminiFixer
|
||||
{
|
||||
private ?\Gemini\Client $client = null;
|
||||
|
||||
public function __construct(private readonly ?string $apiKey)
|
||||
{
|
||||
if ($this->apiKey && class_exists(Gemini::class)) {
|
||||
$this->client = Gemini::client($this->apiKey);
|
||||
}
|
||||
}
|
||||
|
||||
public function suggestFix(Violation $violation): ?string
|
||||
{
|
||||
if (!$this->client) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$prompt = <<<PROMPT
|
||||
You are a WCAG 2.1 Accessibility expert.
|
||||
A scanner found an error in this HTML snippet:
|
||||
Snippet: {$violation->htmlSnippet}
|
||||
Error: {$violation->message}
|
||||
Severity: {$violation->severity->value}
|
||||
Standard: {$violation->standard->value}
|
||||
|
||||
Please provide ONLY the fixed version of the HTML snippet.
|
||||
Do not explain, do not add markdown code blocks, just return the corrected HTML.
|
||||
If the error is about missing 'alt' text, add a meaningful alt text based on context.
|
||||
If it's about ARIA roles, add the correct ones.
|
||||
PROMPT;
|
||||
|
||||
try {
|
||||
$result = $this->client->geminiPro()->generateContent($prompt);
|
||||
$suggestion = trim($result->text());
|
||||
|
||||
// Clean up if AI included markdown blocks anyway
|
||||
$suggestion = preg_replace('/^```html\s*|\s*```$/i', '', $suggestion);
|
||||
|
||||
return $suggestion;
|
||||
} catch (\Throwable $e) {
|
||||
// Log error if needed
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use YakNet\AccessibilityConsole\AI\GeminiFixer;
|
||||
use YakNet\AccessibilityConsole\Core\Config;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\StandardRuleSet;
|
||||
use YakNet\AccessibilityConsole\Source\SourceLocator;
|
||||
|
||||
class FixCommand extends Command
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('fix')
|
||||
->setDescription('Automatically fix accessibility violations using AI')
|
||||
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
|
||||
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping')
|
||||
->addOption('dry-run', null, InputOption::VALUE_NONE, 'Show what would be fixed without modifying files');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$target = $input->getArgument('target');
|
||||
$projectPath = $input->getOption('project-path') ?? getcwd();
|
||||
$dryRun = $input->getOption('dry-run');
|
||||
|
||||
$config = new Config();
|
||||
$apiKey = $config->getGeminiApiKey();
|
||||
|
||||
if (!$apiKey) {
|
||||
$io->error("GEMINI_API_KEY not found in .env or environment variables. AI fixing requires an API key.");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->title("AI Self-Healing Mode - Fixing: $target");
|
||||
|
||||
// 1. Fetch HTML
|
||||
$html = @file_get_contents($target);
|
||||
if (!$html) {
|
||||
$io->error("Could not read target: $target");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 2. Scan
|
||||
$scanner = new Scanner();
|
||||
foreach (StandardRuleSet::all() as $rule) {
|
||||
$scanner->addRule($rule);
|
||||
}
|
||||
$violations = $scanner->scan($html);
|
||||
|
||||
if (empty($violations)) {
|
||||
$io->success("Everything looks good! No violations to fix.");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->note("Found " . count($violations) . " violations. Starting AI analysis...");
|
||||
|
||||
$fixer = new GeminiFixer($apiKey);
|
||||
$locator = new SourceLocator($projectPath);
|
||||
$fixedCount = 0;
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$io->write("Analyzing <fg=yellow>{$violation->ruleId}</>... ");
|
||||
|
||||
// Locate source
|
||||
$location = $locator->locate($violation->htmlSnippet);
|
||||
if (!$location) {
|
||||
$io->writeln("<fg=red>Skipped (Source not found)</>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Get AI Suggestion
|
||||
$suggestion = $fixer->suggestFix($violation);
|
||||
if (!$suggestion) {
|
||||
$io->writeln("<fg=red>Skipped (AI could not generate fix)</>");
|
||||
continue;
|
||||
}
|
||||
|
||||
$io->writeln("<fg=green>Fix Found!</>");
|
||||
$io->writeln(" <fg=gray>Original:</> <fg=red>" . htmlspecialchars(trim($violation->htmlSnippet)) . "</>");
|
||||
$io->writeln(" <fg=gray>AI Fix: </> <fg=green>" . htmlspecialchars(trim($suggestion)) . "</>");
|
||||
|
||||
if ($dryRun) {
|
||||
$io->writeln(" <fg=blue>[DRY RUN] File modification skipped.</>");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Apply Fix
|
||||
if ($this->applyFix($location['file'], $violation->htmlSnippet, $suggestion)) {
|
||||
$io->writeln(" <fg=bright-green>✔ Applied to {$location['file']}:{$location['line']}</>");
|
||||
$fixedCount++;
|
||||
} else {
|
||||
$io->writeln(" <fg=red>✘ Failed to apply fix to file.</>");
|
||||
}
|
||||
$io->writeln("");
|
||||
}
|
||||
|
||||
if ($fixedCount > 0) {
|
||||
$io->success("Self-healing complete! $fixedCount violations were automatically fixed.");
|
||||
} else {
|
||||
$io->warning("No files were modified. Check error logs for details.");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
private function applyFix(string $filePath, string $originalSnippet, string $fixedSnippet): bool
|
||||
{
|
||||
$content = file_get_contents($filePath);
|
||||
if ($content === false) return false;
|
||||
|
||||
// Try exact replacement first
|
||||
if (str_contains($content, $originalSnippet)) {
|
||||
$newContent = str_replace($originalSnippet, $fixedSnippet, $content);
|
||||
} else {
|
||||
// Fallback: Fuzzy replacement (simplified for this demo)
|
||||
// In a real professional lib, we'd use more advanced regex or DOM patching
|
||||
$newContent = str_replace(trim($originalSnippet), trim($fixedSnippet), $content);
|
||||
}
|
||||
|
||||
if ($newContent === $content) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return file_put_contents($filePath, $newContent) !== false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Console\Command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use YakNet\AccessibilityConsole\AI\GeminiFixer;
|
||||
use YakNet\AccessibilityConsole\Core\Config;
|
||||
use YakNet\AccessibilityConsole\Core\Scanner;
|
||||
use YakNet\AccessibilityConsole\Rules\StandardRuleSet;
|
||||
use YakNet\AccessibilityConsole\Source\SourceLocator;
|
||||
|
||||
class ScanCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'scan';
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('scan')
|
||||
->setDescription('Scan a URL or file for accessibility violations')
|
||||
->addArgument('target', InputArgument::REQUIRED, 'The URL or file path to scan')
|
||||
->addOption('ai', null, InputOption::VALUE_NONE, 'Enable AI-powered fix suggestions')
|
||||
->addOption('report', null, InputOption::VALUE_REQUIRED, 'Save report to file (html or json)')
|
||||
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$target = $input->getArgument('target');
|
||||
$enableAi = $input->getOption('ai');
|
||||
$projectPath = $input->getOption('project-path') ?? getcwd();
|
||||
|
||||
$io->title("Accessibility Console - Scanning: $target");
|
||||
|
||||
// 1. Fetch HTML
|
||||
$html = @file_get_contents($target);
|
||||
if (!$html) {
|
||||
$io->error("Could not read target: $target");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 2. Setup Scanner
|
||||
$scanner = new Scanner();
|
||||
foreach (StandardRuleSet::all() as $rule) {
|
||||
$scanner->addRule($rule);
|
||||
}
|
||||
|
||||
// 3. Scan
|
||||
$violations = $scanner->scan($html);
|
||||
|
||||
if (empty($violations)) {
|
||||
$io->success("No violations found!");
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->section("Found " . count($violations) . " violations");
|
||||
|
||||
// 4. Setup Helpers
|
||||
$locator = new SourceLocator($projectPath);
|
||||
$config = new Config();
|
||||
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
|
||||
|
||||
foreach ($violations as $violation) {
|
||||
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
|
||||
|
||||
// Show Snippet
|
||||
$io->writeln(" <fg=gray>Snippet:</> <fg=yellow>" . htmlspecialchars($violation->htmlSnippet) . "</>");
|
||||
|
||||
// Source Mapping
|
||||
$location = $locator->locate($violation->htmlSnippet);
|
||||
if ($location) {
|
||||
$io->writeln(" <fg=gray>Source:</> <fg=cyan>{$location['file']}:{$location['line']}</>");
|
||||
}
|
||||
|
||||
// AI Fixer
|
||||
if ($enableAi && $fixer) {
|
||||
$io->write(" <fg=gray>AI Fix Suggestion:</> ");
|
||||
$suggestion = $fixer->suggestFix($violation);
|
||||
if ($suggestion) {
|
||||
$io->writeln("<fg=green>" . htmlspecialchars($suggestion) . "</>");
|
||||
} else {
|
||||
$io->writeln("<fg=red>Could not generate suggestion (check API key)</>");
|
||||
}
|
||||
}
|
||||
|
||||
$io->writeln("");
|
||||
}
|
||||
|
||||
$io->warning("Scanned " . count($violations) . " errors. Use --ai to get fix suggestions.");
|
||||
|
||||
// Export Report
|
||||
$reportPath = $input->getOption('report');
|
||||
if ($reportPath) {
|
||||
$extension = pathinfo($reportPath, PATHINFO_EXTENSION);
|
||||
if ($extension === 'json') {
|
||||
$data = array_map(fn($v) => $v->toArray(), $violations);
|
||||
file_put_contents($reportPath, json_encode($data, JSON_PRETTY_PRINT));
|
||||
} else {
|
||||
$reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter();
|
||||
file_put_contents($reportPath, $reporter->render($violations, $target));
|
||||
}
|
||||
$io->success("Report saved to: $reportPath");
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
class Config
|
||||
{
|
||||
private array $settings = [];
|
||||
|
||||
public function __construct(array $settings = [])
|
||||
{
|
||||
$this->settings = $settings;
|
||||
}
|
||||
|
||||
public static function fromYaml(string $path): self
|
||||
{
|
||||
// Placeholder for YAML loading logic
|
||||
return new self([]);
|
||||
}
|
||||
|
||||
public function get(string $key, mixed $default = null): mixed
|
||||
{
|
||||
return $this->settings[$key] ?? $default;
|
||||
}
|
||||
|
||||
public function getGeminiApiKey(): ?string
|
||||
{
|
||||
return $this->get('gemini_api_key') ?? $_ENV['GEMINI_API_KEY'] ?? getenv('GEMINI_API_KEY') ?: null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
use YakNet\AccessibilityConsole\Rules\RuleInterface;
|
||||
|
||||
class RuleEngine
|
||||
{
|
||||
/** @var RuleInterface[] */
|
||||
private array $rules = [];
|
||||
|
||||
public function addRule(RuleInterface $rule): void
|
||||
{
|
||||
$this->rules[] = $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return RuleInterface[]
|
||||
*/
|
||||
public function getRules(): array
|
||||
{
|
||||
return $this->rules;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
use DOMDocument;
|
||||
use YakNet\AccessibilityConsole\Rules\RuleInterface;
|
||||
|
||||
class Scanner
|
||||
{
|
||||
private array $rules = [];
|
||||
|
||||
public function __construct(
|
||||
private readonly ?RuleEngine $engine = null
|
||||
) {
|
||||
if ($this->engine) {
|
||||
$this->rules = $this->engine->getRules();
|
||||
}
|
||||
}
|
||||
|
||||
public function addRule(RuleInterface $rule): void
|
||||
{
|
||||
$this->rules[] = $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Violation[]
|
||||
*/
|
||||
public function scan(string $html): array
|
||||
{
|
||||
if (trim($html) === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$dom = new DOMDocument();
|
||||
$previousState = libxml_use_internal_errors(true);
|
||||
$loaded = $dom->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_NOERROR | LIBXML_NOWARNING);
|
||||
libxml_clear_errors();
|
||||
libxml_use_internal_errors($previousState);
|
||||
|
||||
if (!$loaded) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$violations = [];
|
||||
$elements = $dom->getElementsByTagName('*');
|
||||
|
||||
foreach ($elements as $element) {
|
||||
if (!$element instanceof \DOMElement) continue;
|
||||
foreach ($this->rules as $rule) {
|
||||
try {
|
||||
$violation = $rule->check($element);
|
||||
if ($violation) {
|
||||
$violations[] = $violation;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $violations;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
enum Severity: string
|
||||
{
|
||||
case INFO = 'info';
|
||||
case WARNING = 'warning';
|
||||
case ERROR = 'error';
|
||||
case CRITICAL = 'critical';
|
||||
|
||||
public function color(): string
|
||||
{
|
||||
return match($this) {
|
||||
self::INFO => 'info',
|
||||
self::WARNING => 'comment',
|
||||
self::ERROR => 'error',
|
||||
self::CRITICAL => 'error',
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
readonly class Violation
|
||||
{
|
||||
public function __construct(
|
||||
public string $ruleId,
|
||||
public string $message,
|
||||
public Severity $severity,
|
||||
public WCAGStandard $standard,
|
||||
public string $htmlSnippet,
|
||||
public ?array $location = null, // ['file' => '...', 'line' => 0]
|
||||
public ?string $fixSuggestion = null
|
||||
) {}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'ruleId' => $this->ruleId,
|
||||
'message' => $this->message,
|
||||
'severity' => $this->severity->value,
|
||||
'standard' => $this->standard->value,
|
||||
'htmlSnippet' => $this->htmlSnippet,
|
||||
'location' => $this->location,
|
||||
'fixSuggestion' => $this->fixSuggestion,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Core;
|
||||
|
||||
enum WCAGStandard: string
|
||||
{
|
||||
case A = 'A';
|
||||
case AA = 'AA';
|
||||
case AAA = 'AAA';
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Reporting;
|
||||
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
|
||||
class HtmlDashboardReporter
|
||||
{
|
||||
/**
|
||||
* @param Violation[] $violations
|
||||
*/
|
||||
public function render(array $violations, string $target): string
|
||||
{
|
||||
$count = count($violations);
|
||||
$date = date('Y-m-d H:i:s');
|
||||
|
||||
$rows = '';
|
||||
foreach ($violations as $v) {
|
||||
$severityClass = strtolower($v->severity->value);
|
||||
$rows .= "
|
||||
<tr class='severity-{$severityClass}'>
|
||||
<td><span class='badge'>{$v->ruleId}</span></td>
|
||||
<td>{$v->message}</td>
|
||||
<td><pre><code>" . htmlspecialchars($v->htmlSnippet) . "</code></pre></td>
|
||||
<td>" . ($v->location ? "{$v->location['file']}:{$v->location['line']}" : 'N/A') . "</td>
|
||||
</tr>";
|
||||
}
|
||||
|
||||
return <<<HTML
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>A11y Report - {$target}</title>
|
||||
<style>
|
||||
body { font-family: 'Inter', sans-serif; background: #f4f7f6; color: #333; margin: 0; padding: 20px; }
|
||||
.container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 12px; box-shadow: 0 4px 20px rgba(0,0,0,0.08); }
|
||||
h1 { color: #2c3e50; margin-top: 0; }
|
||||
.summary { display: flex; gap: 20px; margin-bottom: 30px; }
|
||||
.card { background: #3498db; color: white; padding: 20px; border-radius: 8px; flex: 1; text-align: center; }
|
||||
.card.error { background: #e74c3c; }
|
||||
table { width: 100%; border-collapse: collapse; margin-top: 20px; }
|
||||
th, td { text-align: left; padding: 12px; border-bottom: 1px solid #eee; }
|
||||
th { background: #f8f9fa; }
|
||||
pre { background: #272822; color: #f8f8f2; padding: 10px; border-radius: 4px; overflow-x: auto; font-size: 12px; }
|
||||
.badge { background: #34495e; color: white; padding: 4px 8px; border-radius: 4px; font-size: 11px; }
|
||||
.severity-error { background: #fff5f5; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Accessibility Report</h1>
|
||||
<p>Target: <strong>{$target}</strong> | Generated: {$date}</p>
|
||||
|
||||
<div class="summary">
|
||||
<div class="card error">
|
||||
<h2>{$count}</h2>
|
||||
<p>Violations Found</p>
|
||||
</div>
|
||||
<div class="card">
|
||||
<h2>WCAG 2.1</h2>
|
||||
<p>Standard Level</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rule ID</th>
|
||||
<th>Issue</th>
|
||||
<th>Code Snippet</th>
|
||||
<th>Source Location</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{$rows}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
HTML;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
abstract class AbstractRule implements RuleInterface
|
||||
{
|
||||
abstract public function getId(): string;
|
||||
abstract public function getDescription(): string;
|
||||
abstract public function getStandard(): WCAGStandard;
|
||||
abstract public function getSeverity(): Severity;
|
||||
|
||||
protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation
|
||||
{
|
||||
return new Violation(
|
||||
ruleId: $this->getId(),
|
||||
message: $message,
|
||||
severity: $this->getSeverity(),
|
||||
standard: $this->getStandard(),
|
||||
htmlSnippet: $element->ownerDocument->saveHTML($element),
|
||||
fixSuggestion: $suggestion
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 ButtonName extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_4_1_2_BUTTON'; }
|
||||
public function getDescription(): string { return 'Buttons must have discernible text.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'button') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim($element->textContent);
|
||||
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
|
||||
return $this->createViolation($element, $this->getDescription(), 'Add text or an aria-label to the button.');
|
||||
}
|
||||
|
||||
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 EmptyLink extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_2_4_4_LINK_TEXT'; }
|
||||
public function getDescription(): string { return 'Links must have discernible text.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'a') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$text = trim($element->textContent);
|
||||
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
|
||||
// Check for images inside
|
||||
$imgs = $element->getElementsByTagName('img');
|
||||
foreach ($imgs as $img) {
|
||||
if ($img->hasAttribute('alt') && trim($img->getAttribute('alt')) !== '') {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->createViolation($element, $this->getDescription(), 'Add text content or an aria-label to the link.');
|
||||
}
|
||||
|
||||
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 FieldsetLegend extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_FIELDSET'; }
|
||||
public function getDescription(): string { return 'Fieldsets should have a legend.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'fieldset') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$legends = $element->getElementsByTagName('legend');
|
||||
if ($legends->length === 0) {
|
||||
return $this->createViolation($element, $this->getDescription(), 'Add a <legend> inside the fieldset.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class FormLabel extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_3_1_LABEL'; }
|
||||
public function getDescription(): string { return 'Form inputs must have an associated label.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
$tags = ['input', 'select', 'textarea'];
|
||||
if (!in_array(strtolower($element->tagName), $tags)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Skip hidden inputs and buttons
|
||||
$type = strtolower($element->getAttribute('type'));
|
||||
if ($type === 'hidden' || $type === 'submit' || $type === 'button' || $type === 'reset') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($element->hasAttribute('id')) {
|
||||
$id = $element->getAttribute('id');
|
||||
$xpath = new \DOMXPath($element->ownerDocument);
|
||||
$labels = $xpath->query("//label[@for='$id']");
|
||||
if ($labels->length > 0) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// Check for aria-label or aria-labelledby
|
||||
if ($element->hasAttribute('aria-label') || $element->hasAttribute('aria-labelledby')) {
|
||||
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.');
|
||||
}
|
||||
}
|
||||
@@ -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 HeadingOrder extends AbstractRule
|
||||
{
|
||||
private static int $lastLevel = 0;
|
||||
|
||||
public function getId(): string { return 'WCAG_1_3_1_HEADING'; }
|
||||
public function getDescription(): string { return 'Headings should follow a logical nesting order.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::WARNING; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (!preg_match('/^h([1-6])$/i', $element->tagName, $matches)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$currentLevel = (int)$matches[1];
|
||||
|
||||
// This rule is a bit complex in a per-element check without global state,
|
||||
// but we can at least check if an h2 comes before an h1 in some cases or jumps.
|
||||
// For simplicity in this engine, we warn if it's not starting with H1 or jumps too many levels.
|
||||
|
||||
if ($currentLevel > self::$lastLevel + 1 && self::$lastLevel !== 0) {
|
||||
$violation = $this->createViolation($element, "Heading level h$currentLevel jumps from h" . self::$lastLevel, "Ensure headings follow a logical order (h1, then h2, etc.)");
|
||||
self::$lastLevel = $currentLevel;
|
||||
return $violation;
|
||||
}
|
||||
|
||||
self::$lastLevel = $currentLevel;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class HtmlHasLang extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_3_1_1_LANG'; }
|
||||
public function getDescription(): string { return 'The html element must have a lang attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'html') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('lang') || trim($element->getAttribute('lang')) === '') {
|
||||
return $this->createViolation($element, $this->getDescription(), 'Add lang="en" (or your language) to the html tag.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Severity;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
use YakNet\AccessibilityConsole\Core\WCAGStandard;
|
||||
|
||||
class ImgAltText extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'WCAG_1_1_1_ALT'; }
|
||||
public function getDescription(): string { return 'Images must have an alt attribute.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'img') {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$element->hasAttribute('alt')) {
|
||||
// Check for decorative roles
|
||||
$role = $element->getAttribute('role');
|
||||
if ($role === 'presentation' || $role === 'none') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->createViolation($element, $this->getDescription(), 'Add an alt attribute. Use alt="" for decorative images.');
|
||||
}
|
||||
|
||||
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 MetaViewport extends AbstractRule
|
||||
{
|
||||
public function getId(): string { return 'A11Y_VIEWPORT'; }
|
||||
public function getDescription(): string { return 'Viewport meta tag should not disable zooming.'; }
|
||||
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
|
||||
public function getSeverity(): Severity { return Severity::ERROR; }
|
||||
|
||||
public function check(DOMElement $element): ?Violation
|
||||
{
|
||||
if (strtolower($element->tagName) !== 'meta' || $element->getAttribute('name') !== 'viewport') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$content = $element->getAttribute('content');
|
||||
if (str_contains($content, 'user-scalable=no') || str_contains($content, 'maximum-scale=1')) {
|
||||
return $this->createViolation($element, $this->getDescription(), 'Remove user-scalable=no or maximum-scale=1 to allow users to zoom.');
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
use DOMElement;
|
||||
use YakNet\AccessibilityConsole\Core\Violation;
|
||||
|
||||
interface RuleInterface
|
||||
{
|
||||
public function getId(): string;
|
||||
public function check(DOMElement $element): ?Violation;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Rules;
|
||||
|
||||
class StandardRuleSet
|
||||
{
|
||||
/**
|
||||
* @return RuleInterface[]
|
||||
*/
|
||||
public static function all(): array
|
||||
{
|
||||
return [
|
||||
new HtmlHasLang(),
|
||||
new ImgAltText(),
|
||||
new EmptyLink(),
|
||||
new ButtonName(),
|
||||
new FormLabel(),
|
||||
new FieldsetLegend(),
|
||||
new HeadingOrder(),
|
||||
new MetaViewport(),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
namespace YakNet\AccessibilityConsole\Source;
|
||||
|
||||
use RecursiveDirectoryIterator;
|
||||
use RecursiveIteratorIterator;
|
||||
use SplFileInfo;
|
||||
|
||||
class SourceLocator
|
||||
{
|
||||
private array $extensions = ['php', 'html', 'twig', 'blade.php'];
|
||||
|
||||
public function __construct(
|
||||
private readonly string $searchPath
|
||||
) {}
|
||||
|
||||
public function locate(string $snippet): ?array
|
||||
{
|
||||
if (!is_dir($this->searchPath) || !is_readable($this->searchPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cleanSnippet = trim(preg_replace('/\s+/', ' ', $snippet));
|
||||
if ($cleanSnippet === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$searchSnippet = mb_strlen($cleanSnippet) > 150 ? mb_substr($cleanSnippet, 0, 150) : $cleanSnippet;
|
||||
|
||||
try {
|
||||
$directory = new RecursiveDirectoryIterator($this->searchPath, \FilesystemIterator::SKIP_DOTS);
|
||||
$iterator = new RecursiveIteratorIterator($directory, RecursiveIteratorIterator::SELF_FIRST);
|
||||
} catch (\UnexpectedValueException) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$bestFuzzyMatch = null;
|
||||
$highestScore = 0;
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
/** @var SplFileInfo $file */
|
||||
if ($file->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!$this->isValidExtension($file->getFilename())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->searchInFile($file->getPathname(), $searchSnippet, $cleanSnippet);
|
||||
|
||||
if ($result['type'] === 'exact') {
|
||||
return $result['location'];
|
||||
}
|
||||
|
||||
if ($result['type'] === 'fuzzy' && $result['score'] > $highestScore) {
|
||||
$highestScore = $result['score'];
|
||||
$bestFuzzyMatch = $result['location'];
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return ($bestFuzzyMatch && $highestScore >= 2) ? $bestFuzzyMatch : null;
|
||||
}
|
||||
|
||||
private function isValidExtension(string $filename): bool
|
||||
{
|
||||
foreach ($this->extensions as $ext) {
|
||||
if (str_ends_with($filename, $ext)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function searchInFile(string $filepath, string $searchSnippet, string $fullSnippet): array
|
||||
{
|
||||
$content = file_get_contents($filepath);
|
||||
if ($content === false) {
|
||||
return ['type' => 'none'];
|
||||
}
|
||||
|
||||
$lines = explode("\n", $content);
|
||||
$parsedSnippet = $this->parseSnippet($fullSnippet);
|
||||
|
||||
$bestLine = 0;
|
||||
$maxScore = 0;
|
||||
|
||||
foreach ($lines as $index => $line) {
|
||||
$lineNumber = $index + 1;
|
||||
$cleanLine = trim(preg_replace('/\s+/', ' ', $line));
|
||||
|
||||
if (str_contains($cleanLine, $searchSnippet)) {
|
||||
return [
|
||||
'type' => 'exact',
|
||||
'location' => ['file' => $filepath, 'line' => $lineNumber]
|
||||
];
|
||||
}
|
||||
|
||||
$score = $this->calculateFuzzyScore($cleanLine, $parsedSnippet);
|
||||
if ($score > $maxScore) {
|
||||
$maxScore = $score;
|
||||
$bestLine = $lineNumber;
|
||||
}
|
||||
}
|
||||
|
||||
if ($maxScore > 0) {
|
||||
return [
|
||||
'type' => 'fuzzy',
|
||||
'score' => $maxScore,
|
||||
'location' => ['file' => $filepath, 'line' => $bestLine]
|
||||
];
|
||||
}
|
||||
|
||||
return ['type' => 'none'];
|
||||
}
|
||||
|
||||
private function parseSnippet(string $snippet): array
|
||||
{
|
||||
preg_match('/<([a-zA-Z0-9]+)/', $snippet, $matches);
|
||||
$tagName = $matches[1] ?? null;
|
||||
|
||||
preg_match_all('/([a-zA-Z0-9-]+)=["\']([^"\']*)["\']/', $snippet, $matches, PREG_SET_ORDER);
|
||||
|
||||
$attributes = [];
|
||||
foreach ($matches as $match) {
|
||||
$attributes[$match[1]] = $match[2];
|
||||
}
|
||||
|
||||
return ['tag' => $tagName, 'attributes' => $attributes];
|
||||
}
|
||||
|
||||
private function calculateFuzzyScore(string $line, array $parsed): float
|
||||
{
|
||||
if (!$parsed['tag'] || !str_contains(strtolower($line), '<' . strtolower($parsed['tag']))) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$score = 1.0;
|
||||
|
||||
if (in_array(strtolower($parsed['tag']), ['html', 'head', 'body', 'title'])) {
|
||||
$score += 1.0;
|
||||
}
|
||||
|
||||
foreach ($parsed['attributes'] as $name => $value) {
|
||||
if (str_contains($line, "$name=\"$value\"") || str_contains($line, "$name='$value'")) {
|
||||
$score += 2.0;
|
||||
} elseif (str_contains($line, $name . '=')) {
|
||||
$score += 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
return $score;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user