Stylesheets::resetCache()); /** * A text's code, line for line, as the design guard reads an application: fenced Blade, HTML, PHP * and JS into a view, fenced CSS into a stylesheet, and every inline span that reads as a class * list (`bg-primary`, `medium:hidden`, `md-ink-variant`) as a `class` attribute on its own line. * Prose, props, attributes and custom properties are left out. * * @return array{view: string, css: string} */ function boostTextAsCode(string $text): array { $view = []; $css = []; $fence = null; $skipping = false; foreach (explode("\n", $text) as $line) { $viewLine = ''; $cssLine = ''; if (preg_match('/^```\s*([a-z]*)/', $line, $open) === 1) { $fence = $fence === null ? ($open[1] ?: 'text') : null; } elseif ($fence !== null) { if (! $skipping && $fence === 'css') { $cssLine = $line; } elseif (! $skipping && in_array($fence, ['blade', 'html', 'php', 'js'], true)) { $viewLine = $line; } } else { if (preg_match('/^## (.+)$/', $line, $heading) === 1) { $skipping = in_array($heading[1], BOOST_SECTIONS_NAMING_WHAT_FAILS, true); } if (! $skipping) { preg_match_all('/`([^`]+)`/', $line, $spans); $viewLine = collect($spans[1]) ->map(fn (string $span): ?string => match (true) { str_contains($span, '<') => $span, boostSpanIsClassList($span) => '
', default => null, }) ->filter() ->implode(' '); } } $view[] = $viewLine; $css[] = $cssLine; } return ['view' => implode("\n", $view), 'css' => implode("\n", $css)]; } /** * Whether an inline span reads as a class list. Not: a CSS declaration (`font: var(…)`), a * command (`php artisan …`), or a single bare word, which is as often a role, prop or value * (`outline`, `link`, `hidden`) as a utility. */ function boostSpanIsClassList(string $span): bool { $tokens = preg_split('/\s+/', trim($span)); if (count($tokens) === 1 && preg_match('/[-:]/', $tokens[0]) !== 1) { return false; } if (preg_match('/^(?:php|npm|npx|composer|git)\b|\b(?:var|calc|color-mix)\(|^[a-z-]+:\s/', $span) === 1) { return false; } foreach ($tokens as $token) { if (preg_match('/^(?:--|data-|aria-|wire:|x-|@|\$)/', $token) === 1 || str_contains($token, '::')) { return false; } if (preg_match('/^[a-z0-9][a-z0-9:\/.\[\]()#%_-]*$/', $token) !== 1) { return false; } } return true; } it('teaches only classes and variants the stylesheets define', function () { $directory = sys_get_temp_dir().'/livewire-material-boost-'.bin2hex(random_bytes(4)); $files = []; foreach (BOOST_TEXTS as $index => $path) { $code = boostTextAsCode(File::get(__DIR__.'/../../'.$path)); File::ensureDirectoryExists($directory); File::put($files["{$directory}/text-{$index}.blade.php"] = "{$directory}/text-{$index}.blade.php", $code['view']); File::put($files["{$directory}/text-{$index}.css"] = "{$directory}/text-{$index}.css", $code['css']); } try { $violations = DesignGuard::scan(realpath($directory))->violations(); } finally { File::deleteDirectory($directory); } $named = array_map(function (string $violation) use ($directory): string { return (string) preg_replace_callback( '#^'.preg_quote(realpath(dirname($directory)) ?: dirname($directory), '#').'/[^/]+/text-(\d+)\.(?:blade\.php|css)#', fn (array $match): string => BOOST_TEXTS[(int) $match[1]], $violation, ); }, $violations); expect($named)->toBe([]); });