Files
cresta31/Modules/Stylist/Services/ThemeAccessibilityValidator.php
2026-08-15 21:59:18 +01:00

73 lines
2.5 KiB
PHP

<?php
namespace Modules\Stylist\Services;
use Illuminate\Filesystem\Filesystem;
use Modules\Stylist\Theme\Theme;
final class ThemeAccessibilityValidator
{
public function __construct(private readonly Filesystem $files) {}
/** @return array<int, array{severity:string,message:string,file:string}> */
public function validate(Theme $theme): array
{
$issues = [];
foreach ($this->files->allFiles($theme->path().'/views') as $file) {
$content = $file->getContents();
$relative = $file->getRelativePathname();
$missingAlt = 0;
if (preg_match_all('/<img\\b(?:(?:"[^"]*")|(?:\'[^\']*\')|[^\'">])*>/is', $content, $images)) {
foreach ($images[0] as $image) {
if (! preg_match('/\\balt\\s*=\\s*(?:"[^"]*"|\'[^\']*\')/is', $image)) {
$missingAlt++;
}
}
}
if ($missingAlt > 0) {
$issues[] = [
'severity' => 'error',
'message' => $missingAlt.' image(s) missing alt text.',
'file' => $relative,
];
}
if (preg_match('/<html\\b(?![^>]*\\blang=)/i', $content)) {
$issues[] = [
'severity' => 'warning',
'message' => 'HTML element is missing a lang attribute.',
'file' => $relative,
];
}
$unnamedButtons = 0;
if (preg_match_all('/<button\\b((?:(?:"[^"]*")|(?:\'[^\']*\')|[^\'">])*)>(.*?)<\\/button>/is', $content, $buttons, PREG_SET_ORDER)) {
foreach ($buttons as $button) {
$attributes = $button[1];
$body = $button[2];
$hasAccessibleAttribute = preg_match('/\\b(?:aria-label|aria-labelledby|title)\\s*=\\s*(?:"[^"]+"|\'[^\']+\')/is', $attributes) === 1;
$visibleText = trim(html_entity_decode(strip_tags($body), ENT_QUOTES | ENT_HTML5));
if (! $hasAccessibleAttribute && $visibleText === '') {
$unnamedButtons++;
}
}
}
if ($unnamedButtons > 0) {
$issues[] = [
'severity' => 'warning',
'message' => $unnamedButtons.' button(s) may lack an accessible name.',
'file' => $relative,
];
}
}
return $issues;
}
}