Initial Commit
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Html;
|
||||
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Modules\Stylist\Theme\Loader;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
use Modules\Stylist\Theme\Theme;
|
||||
use Modules\Stylist\Theme\UrlGenerator;
|
||||
|
||||
final class ThemeHtmlBuilder
|
||||
{
|
||||
public function __construct(private readonly Stylist $stylist, private readonly UrlGenerator $url, private readonly Loader $loader) {}
|
||||
|
||||
public function stylesheet(?string $path = null): HtmlString
|
||||
{
|
||||
if ($path !== null) {
|
||||
$theme = $this->stylist->current();
|
||||
if (! $theme->has($path)) {
|
||||
return new HtmlString('');
|
||||
}
|
||||
|
||||
return new HtmlString('<link rel="stylesheet" href="'.e($this->url->forTheme($theme, $path)).'">');
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
foreach (array_reverse($this->themeChain()) as $theme) {
|
||||
$stylesheet = $theme->stylesheet();
|
||||
if ($stylesheet !== '' && $theme->has($stylesheet)) {
|
||||
$tags[] = '<link rel="stylesheet" href="'.e($this->url->forTheme($theme, $stylesheet)).'">';
|
||||
}
|
||||
}
|
||||
|
||||
return new HtmlString(implode('', $tags));
|
||||
}
|
||||
|
||||
public function script(?string $path = null): HtmlString
|
||||
{
|
||||
if ($path !== null) {
|
||||
$theme = $this->stylist->current();
|
||||
if (! $theme->has($path)) {
|
||||
return new HtmlString('');
|
||||
}
|
||||
|
||||
return new HtmlString('<script src="'.e($this->url->forTheme($theme, $path)).'" defer></script>');
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
foreach (array_reverse($this->themeChain()) as $theme) {
|
||||
$script = $theme->script();
|
||||
if ($script !== '' && $theme->has($script)) {
|
||||
$tags[] = '<script src="'.e($this->url->forTheme($theme, $script)).'" defer></script>';
|
||||
}
|
||||
}
|
||||
|
||||
return new HtmlString(implode('', $tags));
|
||||
}
|
||||
|
||||
public function variables(): HtmlString
|
||||
{
|
||||
$theme = $this->stylist->current();
|
||||
$default = $this->declarations($this->stylist->variables());
|
||||
$rules = [':root{'.$default.'}'];
|
||||
|
||||
foreach ($theme->appearances() as $name => $definition) {
|
||||
$vars = (array) data_get($definition, 'variables', []);
|
||||
if ($vars) {
|
||||
$rules[] = ':root[data-appearance="'.$this->selector($name).'"]{'.$this->declarations($vars).'}';
|
||||
}
|
||||
}
|
||||
foreach ($theme->accents() as $name => $definition) {
|
||||
$vars = (array) data_get($definition, 'variables', []);
|
||||
if ($vars) {
|
||||
$rules[] = ':root[data-accent="'.$this->selector($name).'"]{'.$this->declarations($vars).'}';
|
||||
}
|
||||
}
|
||||
|
||||
if ($theme->appearances() === [] && $theme->accents() === []) {
|
||||
return new HtmlString('<style>'.implode('', $rules).'</style>');
|
||||
}
|
||||
|
||||
$appearance = json_encode($this->stylist->appearance(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
$accent = json_encode($this->stylist->accent(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
$themeKey = json_encode($theme->key(), JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT);
|
||||
$hasAppearances = $theme->appearances() !== [] ? 'true' : 'false';
|
||||
$hasAccents = $theme->accents() !== [] ? 'true' : 'false';
|
||||
|
||||
$script = <<<JS
|
||||
<script>(function(){var d=document.documentElement,k={$themeKey},ha={$hasAppearances},hc={$hasAccents},a=ha?(localStorage.getItem('stylist:'+k+':appearance')||{$appearance}):'',c=hc?(localStorage.getItem('stylist:'+k+':accent')||{$accent}):'';function apply(){d.dataset.theme=k;if(ha){var dark=a==='dark'||((a==='auto'||a==='system')&&matchMedia('(prefers-color-scheme: dark)').matches);d.dataset.appearance=dark?'dark':'light';d.classList.toggle('dark',dark)}if(hc){d.dataset.accent=c}}apply();window.StylistTheme={setAppearance:function(v){if(!ha)return;a=v;localStorage.setItem('stylist:'+k+':appearance',v);apply()},setAccent:function(v){if(!hc)return;c=v;localStorage.setItem('stylist:'+k+':accent',v);apply()},appearance:function(){return a},accent:function(){return c}};if(ha){matchMedia('(prefers-color-scheme: dark)').addEventListener?.('change',function(){if(a==='auto'||a==='system')apply()})}document.dispatchEvent(new CustomEvent('stylist:ready',{detail:window.StylistTheme}))})();</script>
|
||||
JS;
|
||||
|
||||
return new HtmlString('<style>'.implode('', $rules).'</style>'.$script);
|
||||
}
|
||||
|
||||
public function switcher(): HtmlString
|
||||
{
|
||||
$theme = $this->stylist->current();
|
||||
$appearances = array_keys($theme->appearances());
|
||||
$accents = array_keys($theme->accents());
|
||||
|
||||
if ($appearances === [] && $accents === []) {
|
||||
return new HtmlString('');
|
||||
}
|
||||
|
||||
$appearanceOptions = collect($appearances)->map(fn ($name) => '<option value="'.e($name).'">'.e(ucfirst($name)).'</option>')->implode('');
|
||||
$accentOptions = collect($accents)->map(fn ($name) => '<option value="'.e($name).'">'.e(ucfirst($name)).'</option>')->implode('');
|
||||
$appearanceSelect = $appearances ? '<label>Appearance <select data-stylist-appearance>'.$appearanceOptions.'</select></label>' : '';
|
||||
$accentSelect = $accents ? '<label>Accent <select data-stylist-accent>'.$accentOptions.'</select></label>' : '';
|
||||
|
||||
return new HtmlString('<div data-stylist-switcher class="stylist-theme-switcher">'.$appearanceSelect.$accentSelect.'</div><script>document.addEventListener("stylist:ready",function(){document.querySelectorAll("[data-stylist-switcher]").forEach(function(root){var a=root.querySelector("[data-stylist-appearance]"),c=root.querySelector("[data-stylist-accent]");if(a){a.value=StylistTheme.appearance();a.addEventListener("change",function(){StylistTheme.setAppearance(a.value)})}if(c){c.value=StylistTheme.accent();c.addEventListener("change",function(){StylistTheme.setAccent(c.value)})}})});</script>');
|
||||
}
|
||||
|
||||
/** @return array<int, Theme> */
|
||||
private function themeChain(): array
|
||||
{
|
||||
$chain = [];
|
||||
$theme = $this->stylist->current();
|
||||
$seen = [];
|
||||
|
||||
while (! isset($seen[$theme->key()])) {
|
||||
$seen[$theme->key()] = true;
|
||||
$chain[] = $theme;
|
||||
$parent = $theme->parent();
|
||||
|
||||
if (! $parent) {
|
||||
break;
|
||||
}
|
||||
|
||||
try {
|
||||
$theme = $this->loader->find($parent);
|
||||
} catch (\Throwable) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $chain;
|
||||
}
|
||||
|
||||
private function declarations(array $variables): string
|
||||
{
|
||||
return collect($variables)
|
||||
->filter(fn ($value, $key) => is_string($key) && (is_string($value) || is_numeric($value)))
|
||||
->map(fn ($value, string $key) => '--'.ltrim($key, '-').': '.e((string) $value).';')
|
||||
->implode('');
|
||||
}
|
||||
|
||||
private function selector(string $value): string
|
||||
{
|
||||
return preg_replace('/[^a-z0-9_-]/i', '', $value) ?: 'default';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Stylist\Html;
|
||||
|
||||
use Illuminate\Support\HtmlString;
|
||||
use Modules\Stylist\Theme\Stylist;
|
||||
use Modules\Stylist\Theme\UrlGenerator;
|
||||
|
||||
final class ThemeSeoBuilder
|
||||
{
|
||||
public function __construct(private readonly Stylist $stylist, private readonly UrlGenerator $urls) {}
|
||||
|
||||
public function render(array $overrides = []): HtmlString
|
||||
{
|
||||
$theme = $this->stylist->current();
|
||||
$seo = array_replace($theme->seo(), $overrides);
|
||||
$settings = $this->stylist->settings();
|
||||
$title = (string) ($seo['title'] ?? $settings['site_title'] ?? $theme->name());
|
||||
$description = (string) ($seo['description'] ?? $settings['site_description'] ?? $theme->description());
|
||||
$image = $seo['image'] ?? $theme->preview();
|
||||
if ($image && $theme->has($image)) {
|
||||
$image = $this->urls->asset($image);
|
||||
}
|
||||
$tags = ['<title>'.e($title).'</title>'];
|
||||
if ($description !== '') {
|
||||
$tags[] = '<meta name="description" content="'.e($description).'">';
|
||||
}
|
||||
foreach (['og:title' => $title, 'og:description' => $description, 'twitter:card' => 'summary_large_image', 'twitter:title' => $title, 'twitter:description' => $description] as $property => $content) {
|
||||
if ($content === '') {
|
||||
continue;
|
||||
}$attribute = str_starts_with($property, 'og:') ? 'property' : 'name';
|
||||
$tags[] = '<meta '.$attribute.'="'.e($property).'" content="'.e($content).'">';
|
||||
}
|
||||
if ($image) {
|
||||
$tags[] = '<meta property="og:image" content="'.e($image).'">';
|
||||
$tags[] = '<meta name="twitter:image" content="'.e($image).'">';
|
||||
}
|
||||
if (isset($seo['json_ld']) && is_array($seo['json_ld'])) {
|
||||
$tags[] = '<script type="application/ld+json">'.json_encode($seo['json_ld'], JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE).'</script>';
|
||||
}
|
||||
|
||||
return new HtmlString(implode("\n", $tags));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user