Compare commits

..
4 Commits
29 changed files with 3595 additions and 236 deletions
+3 -1
View File
@@ -30,7 +30,9 @@
"symfony/yaml": "^6.4|^7.0",
"monolog/monolog": "^3.0",
"vlucas/phpdotenv": "^5.6",
"google-gemini-php/client": "^1.0"
"google-gemini-php/client": "^1.0",
"guzzlehttp/guzzle": "^7.8",
"masterminds/html5": "^2.8"
},
"require-dev": {
"phpunit/phpunit": "^10.5",
+14 -2
View File
@@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>Broken Page</title>
<title></title>
</head>
<body>
<h1 id="main-content">Welcome to a broken page</h1>
@@ -28,7 +28,7 @@
<div role="cool-widget" aria-controls="non-existent-popup">WAI-ARIA elements</div>
<!-- Missing Iframe Title -->
<iframe src="https://www.youtube.com/embed/dQw4w9WgXcQ" width="560" height="315"></iframe>
<iframe src="https://www.youtube.com/embed/aqz-KE-bpKQ" width="560" height="315"></iframe>
<!-- New WCAG Rule Violations for testing -->
@@ -49,6 +49,18 @@
</div>
<a href="#" aria-hidden="true">Hidden link</a>
<!-- Empty heading violation -->
<h2></h2>
<!-- Video missing track violation -->
<video src="https://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4" width="320" height="240" controls></video>
<!-- Empty label violation -->
<label for="search-input"></label>
<!-- AriaFocusable violation -->
<div role="button">Clickable div button without tabindex</div>
<p>This page has multiple accessibility issues.</p>
</body>
</html>
+10 -19
View File
@@ -1,19 +1,11 @@
<?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';
// 1. Setup Autoloading
require_once __DIR__ . '/../vendor/autoload.php';
use YakNet\AccessibilityConsole\Scanner;
use YakNet\AccessibilityConsole\SourceLocator;
use YakNet\AccessibilityConsole\Reporter\HtmlReporter;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Source\SourceLocator;
use YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter;
use YakNet\AccessibilityConsole\Rules\ImgAltText;
use YakNet\AccessibilityConsole\Rules\FormLabel;
@@ -48,19 +40,18 @@ $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);
$location = $locator->locate($violation->htmlSnippet);
if ($location) {
$violation->setSourceLocation($location['file'], $location['line']);
$violation->location = $location;
}
}
// Render Report
$reporter = new HtmlReporter();
$reportHtml = $reporter->render($violations);
// Render Report (Format as results mapped by page URL)
$reporter = new HtmlDashboardReporter();
$reportHtml = $reporter->render(['http://localhost:8080/index.php' => $violations], 'demo/index.php');
// Inject into body
$html = str_replace('</body>', $reportHtml . '</body>', $html);
+153 -18
View File
@@ -12,6 +12,7 @@ use YakNet\AccessibilityConsole\AI\GeminiFixer;
use YakNet\AccessibilityConsole\Core\Config;
use YakNet\AccessibilityConsole\Core\Scanner;
use YakNet\AccessibilityConsole\Rules\StandardRuleSet;
use YakNet\AccessibilityConsole\Rules\RuleLevels;
use YakNet\AccessibilityConsole\Source\SourceLocator;
class ScanCommand extends Command
@@ -26,7 +27,11 @@ class ScanCommand extends Command
->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');
->addOption('project-path', null, InputOption::VALUE_REQUIRED, 'Base path for source mapping')
->addOption('crawl', null, InputOption::VALUE_NONE, 'Crawl internal links recursively')
->addOption('depth', null, InputOption::VALUE_REQUIRED, 'Maximum crawling depth', '3')
->addOption('max-pages', null, InputOption::VALUE_REQUIRED, 'Maximum number of pages to crawl', '20')
->addOption('level', 'l', InputOption::VALUE_REQUIRED, 'Accessibility scan level (1-5)', '4');
}
protected function execute(InputInterface $input, OutputInterface $output): int
@@ -44,36 +49,162 @@ class ScanCommand extends Command
$projectPath = '.';
}
$io->title("Accessibility Console - Scanning: $target");
$levelOption = $input->getOption('level');
$level = is_numeric($levelOption) ? (int)$levelOption : 4;
if ($level < 1 || $level > 5) {
$io->error("Scan level must be between 1 and 5.");
return Command::FAILURE;
}
// 1. Fetch HTML
$io->title("Accessibility Console - Scanning: $target (Level $level)");
// 1. Setup Scanner
$scanner = new Scanner();
$rules = RuleLevels::getRulesForLevel($level);
foreach ($rules as $rule) {
$scanner->addRule($rule);
}
// 2. Fetch and Scan
$isUrl = str_starts_with($target, 'http://') || str_starts_with($target, 'https://');
$crawl = (bool)$input->getOption('crawl') && $isUrl;
/** @var array<string, \YakNet\AccessibilityConsole\Core\Violation[]> $results */
$results = [];
if ($crawl) {
$depthOption = $input->getOption('depth');
$depthLimit = is_numeric($depthOption) ? (int)$depthOption : 3;
$maxPagesOption = $input->getOption('max-pages');
$maxPages = is_numeric($maxPagesOption) ? (int)$maxPagesOption : 20;
$io->comment("Starting recursive crawl from: $target (Max Depth: $depthLimit, Max Pages: $maxPages)");
$parsedBase = parse_url($target);
$baseHost = $parsedBase['host'] ?? '';
$queue = [
['url' => $target, 'depth' => 1]
];
$visited = [];
$client = new \GuzzleHttp\Client([
'timeout' => 10,
'allow_redirects' => true,
'headers' => [
'User-Agent' => 'YakNet Accessibility Console Crawler/1.0',
]
]);
while (!empty($queue) && count($visited) < $maxPages) {
$current = array_shift($queue);
if (!is_array($current)) {
continue;
}
$url = $current['url'];
$depth = (int)$current['depth'];
// Remove fragment
$cleanUrl = strtok($url, '#');
if ($cleanUrl === false || isset($visited[$cleanUrl])) {
continue;
}
$visited[$cleanUrl] = true;
$io->write(" <fg=gray>Crawling [Depth $depth]:</> <fg=cyan>$cleanUrl</>... ");
try {
$response = $client->request('GET', $cleanUrl);
$html = (string)$response->getBody();
$io->writeln("<fg=green>OK</>");
// Scan the page HTML
$pageViolations = $scanner->scan($html);
$results[$cleanUrl] = $pageViolations;
// Extract links if depth is less than limit
if ($depth < $depthLimit) {
$html5 = new \Masterminds\HTML5(['disable_html_ns' => true]);
$doc = $html5->loadHTML($html);
$links = $doc->getElementsByTagName('a');
foreach ($links as $link) {
$href = $link->getAttribute('href');
if (empty($href) || str_starts_with($href, 'javascript:') || str_starts_with($href, 'mailto:') || str_starts_with($href, 'tel:')) {
continue;
}
// Resolve relative URI
try {
$baseUri = new \GuzzleHttp\Psr7\Uri($cleanUrl);
$relativeUri = new \GuzzleHttp\Psr7\Uri($href);
$absoluteUri = \GuzzleHttp\Psr7\UriResolver::resolve($baseUri, $relativeUri);
$absoluteUrl = (string)$absoluteUri;
} catch (\Throwable) {
continue;
}
// Check host compatibility
$parsedAbsolute = parse_url($absoluteUrl);
$absoluteHost = $parsedAbsolute['host'] ?? '';
if ($absoluteHost === $baseHost) {
// Add to queue if not already visited or in queue
$cleanAbsolute = strtok($absoluteUrl, '#');
if ($cleanAbsolute !== false && !isset($visited[$cleanAbsolute])) {
$inQueue = false;
foreach ($queue as $q) {
if ($q['url'] === $cleanAbsolute) {
$inQueue = true;
break;
}
}
if (!$inQueue) {
$queue[] = ['url' => $cleanAbsolute, 'depth' => $depth + 1];
}
}
}
}
}
} catch (\Throwable $e) {
$io->writeln("<fg=red>FAIL (" . $e->getMessage() . ")</>");
}
}
} else {
// Standard single page/file scan
$html = @file_get_contents($target);
if ($html === false || $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);
$violations = $scanner->scan($html);
$results[$target] = $violations;
}
// 3. Scan
$violations = $scanner->scan($html);
$totalViolations = 0;
foreach ($results as $url => $violations) {
$totalViolations += count($violations);
}
if (empty($violations)) {
if ($totalViolations === 0) {
$io->success("No violations found!");
return Command::SUCCESS;
}
$io->section("Found " . count($violations) . " violations");
$io->section("Found a total of $totalViolations violations across " . count($results) . " pages");
// 4. Setup Helpers
// 3. Setup Helpers
$locator = new SourceLocator($projectPath);
$config = new Config();
$fixer = $enableAi ? new GeminiFixer($config->getGeminiApiKey()) : null;
foreach ($results as $url => $violations) {
if (empty($violations)) {
continue;
}
$io->section("Page: $url (" . count($violations) . " violations)");
foreach ($violations as $violation) {
$io->writeln("<fg=white;bg=red;options=bold> {$violation->ruleId} </>\t<fg=red>{$violation->message}</>");
@@ -100,19 +231,23 @@ class ScanCommand extends Command
$io->writeln("");
}
}
$io->warning("Scanned " . count($violations) . " errors. Use --ai to get fix suggestions.");
$io->warning("Scanned " . $totalViolations . " errors. Use --ai to get fix suggestions.");
// Export Report
// 4. Export Report
$reportPath = $input->getOption('report');
if (is_string($reportPath) && $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));
$serialized = [];
foreach ($results as $url => $violations) {
$serialized[$url] = array_map(fn($v) => $v->toArray(), $violations);
}
file_put_contents($reportPath, json_encode($serialized, JSON_PRETTY_PRINT));
} else {
$reporter = new \YakNet\AccessibilityConsole\Reporting\HtmlDashboardReporter();
file_put_contents($reportPath, $reporter->render($violations, $target));
file_put_contents($reportPath, $reporter->render($results, $target));
}
$io->success("Report saved to: $reportPath");
}
+5
View File
@@ -19,6 +19,11 @@ abstract class AbstractRule
*/
abstract public function getSeverity(): Severity;
public function getLevel(): int
{
return 4;
}
protected function createViolation(string $message, \DOMElement $element): Violation
{
$doc = $element->ownerDocument;
+4 -3
View File
@@ -28,9 +28,10 @@ class Scanner
return [];
}
$doc = new \DOMDocument();
// Suppress warnings for malformed HTML
@$doc->loadHTML('<?xml encoding="utf-8" ?>' . $html, LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD);
$html5 = new \Masterminds\HTML5([
'disable_html_ns' => true,
]);
$doc = $html5->loadHTML($html);
return $this->engine->run($doc);
}
File diff suppressed because it is too large Load Diff
+5
View File
@@ -14,6 +14,11 @@ abstract class AbstractRule implements RuleInterface
abstract public function getStandard(): WCAGStandard;
abstract public function getSeverity(): Severity;
public function getLevel(): int
{
return 4;
}
protected function createViolation(DOMElement $element, string $message, ?string $suggestion = null): Violation
{
$doc = $element->ownerDocument;
+55
View File
@@ -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 AriaFocusable extends AbstractRule
{
/** @var array<int, string> */
private static array $interactiveRoles = [
'button', 'link', 'checkbox', 'menuitem', 'radio', 'textbox',
'searchbox', 'slider', 'spinbutton', 'switch', 'tab'
];
/** @var array<int, string> */
private static array $nativeFocusable = [
'a', 'button', 'input', 'select', 'textarea', 'iframe'
];
public function getId(): string { return 'WCAG_2_1_1_ARIA_FOCUSABLE'; }
public function getDescription(): string { return 'Elements with interactive ARIA roles must be focusable (e.g. have a tabindex attribute).'; }
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 (!$element->hasAttribute('role')) {
return null;
}
$role = strtolower(trim($element->getAttribute('role')));
if (!in_array($role, self::$interactiveRoles, true)) {
return null;
}
$tagName = strtolower($element->tagName);
if (in_array($tagName, self::$nativeFocusable, true)) {
return null;
}
if (!$element->hasAttribute('tabindex')) {
return $this->createViolation(
$element,
$this->getDescription(),
'Add tabindex="0" to make the element focusable via keyboard navigation.'
);
}
return null;
}
}
+1
View File
@@ -13,6 +13,7 @@ class AriaHiddenFocusable extends AbstractRule
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 getLevel(): int { return 5; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -78,4 +78,5 @@ class AriaRole extends AbstractRule
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): Severity { return Severity::ERROR; }
public function getLevel(): int { return 3; }
}
+1
View File
@@ -13,6 +13,7 @@ class ButtonName extends AbstractRule
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 getLevel(): int { return 1; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -13,6 +13,7 @@ class ColorContrast extends AbstractRule
public function getDescription(): string { return 'Text must have sufficient contrast against the background (4.5:1 minimum).'; }
public function getStandard(): WCAGStandard { return WCAGStandard::AA; }
public function getSeverity(): Severity { return Severity::WARNING; }
public function getLevel(): int { return 5; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -48,4 +48,5 @@ class DuplicateId extends AbstractRule
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): Severity { return Severity::ERROR; }
public function getLevel(): int { return 3; }
}
+35
View File
@@ -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 EmptyHeading extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_EMPTY_HEADING'; }
public function getDescription(): string { return 'Heading elements (h1-h6) must have discernible text content.'; }
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 (!preg_match('/^h([1-6])$/i', $element->tagName)) {
return null;
}
$text = trim($element->textContent);
if ($text === '' && !$element->hasAttribute('aria-label') && !$element->hasAttribute('title')) {
return $this->createViolation(
$element,
$this->getDescription(),
'Add descriptive text or an aria-label to the heading element.'
);
}
return null;
}
}
+35
View File
@@ -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 EmptyLabel extends AbstractRule
{
public function getId(): string { return 'WCAG_1_3_1_EMPTY_LABEL'; }
public function getDescription(): string { return 'Label elements must have discernible text content.'; }
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;
}
$text = trim($element->textContent);
if ($text === '') {
return $this->createViolation(
$element,
$this->getDescription(),
'Add text content to the label element to describe its associated input.'
);
}
return null;
}
}
+1
View File
@@ -13,6 +13,7 @@ class EmptyLink extends AbstractRule
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 getLevel(): int { return 1; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -13,6 +13,7 @@ class FieldsetLegend extends AbstractRule
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 getLevel(): int { return 2; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -13,6 +13,7 @@ class FormLabel extends AbstractRule
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 getLevel(): int { return 2; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -15,6 +15,7 @@ class HeadingOrder extends AbstractRule
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 getLevel(): int { return 2; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -13,6 +13,7 @@ class HtmlHasLang extends AbstractRule
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 getLevel(): int { return 1; }
public function check(DOMElement $element): ?Violation
{
+1
View File
@@ -13,6 +13,7 @@ class IframeTitle extends AbstractRule
public function getDescription(): string { return 'Iframe elements must have a non-empty title attribute to describe their content.'; }
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
{
+1
View File
@@ -13,6 +13,7 @@ class ImgAltText extends AbstractRule
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 getLevel(): int { return 1; }
public function check(DOMElement $element): ?Violation
{
+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 PageTitle extends AbstractRule
{
public function getId(): string { return 'WCAG_2_4_2_PAGE_TITLE'; }
public function getDescription(): string { return 'Web pages must have a non-empty title element in the head.'; }
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) !== 'head') {
return null;
}
$titles = $element->getElementsByTagName('title');
if ($titles->length === 0) {
return $this->createViolation(
$element,
$this->getDescription(),
'Add a <title> element inside the <head>.'
);
}
$titleText = '';
$firstTitle = $titles->item(0);
if ($firstTitle !== null) {
$titleText = trim($firstTitle->textContent);
}
if ($titleText === '') {
return $this->createViolation(
$element,
$this->getDescription(),
'Provide descriptive text inside the <title> element.'
);
}
return null;
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
class RuleLevels
{
/**
* Get rule instances for the specified level.
* Levels are incremental: Level 5 includes all rules from Levels 1, 2, 3, and 4.
*
* @param int $level
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
*/
public static function getRulesForLevel(int $level): array
{
$rules = [];
$files = glob(__DIR__ . '/*.php');
if ($files === false) {
return [];
}
foreach ($files as $file) {
$className = pathinfo($file, PATHINFO_FILENAME);
$fqcn = 'YakNet\AccessibilityConsole\Rules\\' . $className;
if (class_exists($fqcn)) {
$reflection = new \ReflectionClass($fqcn);
if ($reflection->isAbstract() || $reflection->isInterface() || $reflection->isTrait()) {
continue;
}
// Check if it is a concrete rule
$isRule = $reflection->isSubclassOf(\YakNet\AccessibilityConsole\Core\AbstractRule::class) ||
$reflection->implementsInterface(\YakNet\AccessibilityConsole\Rules\RuleInterface::class);
if ($isRule) {
// Exclude Turkish custom rules to match standard behavior
if (in_array($className, ['EmptyLinkRule', 'HeadingOrderRule', 'ImageAltRule'], true)) {
continue;
}
$instance = $reflection->newInstance();
// Check level
$ruleLevel = 4; // default
if (method_exists($instance, 'getLevel')) {
/** @var mixed $levelVal */
$levelVal = $instance->getLevel();
if (is_int($levelVal)) {
$ruleLevel = $levelVal;
}
}
if ($ruleLevel <= $level) {
/** @var \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule $instance */
$rules[] = $instance;
}
}
}
}
// Sort rules by class name to ensure deterministic order
usort($rules, function ($a, $b) {
return strcmp(get_class($a), get_class($b));
});
return $rules;
}
}
+2 -22
View File
@@ -5,30 +5,10 @@ namespace YakNet\AccessibilityConsole\Rules;
class StandardRuleSet
{
/**
* @return RuleInterface[]
* @return array<int, \YakNet\AccessibilityConsole\Rules\RuleInterface|\YakNet\AccessibilityConsole\Core\AbstractRule>
*/
public static function all(): array
{
/** @var RuleInterface[] $rules */
$rules = [
new HtmlHasLang(),
new ImgAltText(),
new EmptyLink(),
new ButtonName(),
new FormLabel(),
new FieldsetLegend(),
new HeadingOrder(),
new MetaViewport(),
new TabindexOrder(),
new ColorContrast(),
new DuplicateId(),
new AriaRole(),
new IframeTitle(),
new LinkTextGeneric(),
new PlaceholderAsLabel(),
new ImageAltRedundant(),
new AriaHiddenFocusable(),
];
return $rules;
return RuleLevels::getRulesForLevel(5);
}
}
+1
View File
@@ -13,6 +13,7 @@ class TabindexOrder extends AbstractRule
public function getDescription(): string { return 'Avoid positive tabindex values to maintain natural keyboard focus order.'; }
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
{
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace YakNet\AccessibilityConsole\Rules;
use DOMElement;
use YakNet\AccessibilityConsole\Core\Severity;
use YakNet\AccessibilityConsole\Core\Violation;
use YakNet\AccessibilityConsole\Core\WCAGStandard;
class VideoTrack extends AbstractRule
{
public function getId(): string { return 'WCAG_1_2_2_VIDEO_TRACK'; }
public function getDescription(): string { return 'Video elements must contain at least one track element for captions or subtitles.'; }
public function getStandard(): WCAGStandard { return WCAGStandard::A; }
public function getSeverity(): Severity { return Severity::WARNING; }
public function getLevel(): int { return 2; }
public function check(DOMElement $element): ?Violation
{
if (strtolower($element->tagName) !== 'video') {
return null;
}
// Check if there is a track element inside
$tracks = $element->getElementsByTagName('track');
if ($tracks->length === 0) {
return $this->createViolation(
$element,
$this->getDescription(),
'Add a <track kind="captions"> or <track kind="subtitles"> element inside the video element.'
);
}
return null;
}
}
+1861 -70
View File
File diff suppressed because it is too large Load Diff