From ce9a0c8ad5e47b67547c1622c304cf1d4bff19ab Mon Sep 17 00:00:00 2001 From: Sebastian Fischer Date: Sat, 26 Apr 2025 20:03:17 +0200 Subject: [PATCH] [TASK] Allow unique styles per layer --- Classes/EventListener/CssMerger.php | 48 ++++---- Classes/Services/Tailwindcss4Merger.php | 147 ++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 22 deletions(-) create mode 100644 Classes/Services/Tailwindcss4Merger.php diff --git a/Classes/EventListener/CssMerger.php b/Classes/EventListener/CssMerger.php index ab6003b..c171f04 100644 --- a/Classes/EventListener/CssMerger.php +++ b/Classes/EventListener/CssMerger.php @@ -16,6 +16,7 @@ declare(strict_types=1); namespace Evoweb\EwBase\EventListener; use Psr\Http\Message\ServerRequestInterface; +use Evoweb\EwBase\Services\Tailwindcss4Merger; use TYPO3\CMS\Core\Core\RequestId; use TYPO3\CMS\Core\Page\AssetCollector; use TYPO3\CMS\Core\Page\Event\BeforeStylesheetsRenderingEvent; @@ -26,7 +27,8 @@ use TYPO3\CMS\Core\Utility\PathUtility; readonly class CssMerger { public function __construct( - private RequestId $requestId + private RequestId $requestId, + protected Tailwindcss4Merger $tailwindcss4Merger ) { } @@ -34,6 +36,9 @@ readonly class CssMerger { if ($event->isInline() && $event->isPriority()) { $assetCollector = $event->getAssetCollector(); + if (empty($assetCollector->getStyleSheets()) && empty($assetCollector->getInlineStyleSheets())) { + return; + } $styles = []; $styles = $this->getStylesheetFileContent($assetCollector, $styles); @@ -41,18 +46,9 @@ readonly class CssMerger $styles = array_unique($styles); $styles = trim(implode(LF, $styles)); - if (empty($styles)) { - return; - } + $styles = $this->tailwindcss4Merger->cssUnique($styles); - /** @var Site $site */ - $site = $this->getRequest()->getAttribute('site'); - if ( - $site instanceof Site - && ($siteSettings = $site->getSettings()) - && !$siteSettings->isEmpty() - && $siteSettings->get('ew-base.inlineCssStyles') - ) { + if ($this->assertStylesShouldBeAddedInline()) { $assetCollector->addInlineStyleSheet('ew_base', $styles, [], ['priority' => true]); } else { $temporaryFile = GeneralUtility::writeStyleSheetContentToTemporaryFile($styles); @@ -87,16 +83,6 @@ readonly class CssMerger return $styles; } - protected function getStylesheetInlineContent(AssetCollector $assetCollector, array $styles): array - { - $styleSheetsInline = $assetCollector->getInlineStyleSheets(false); - foreach ($styleSheetsInline as $identifier => $asset) { - $styles[] = $asset['source']; - $assetCollector->removeInlineStyleSheet($identifier); - } - return $styles; - } - protected function getFileContent(string $filePath): string { $absoluteFilePath = GeneralUtility::getFileAbsFileName($filePath); @@ -124,6 +110,24 @@ readonly class CssMerger ); } + protected function getStylesheetInlineContent(AssetCollector $assetCollector, array $styles): array + { + $styleSheetsInline = $assetCollector->getInlineStyleSheets(false); + foreach ($styleSheetsInline as $identifier => $asset) { + $styles[] = $asset['source']; + $assetCollector->removeInlineStyleSheet($identifier); + } + return $styles; + } + + protected function assertStylesShouldBeAddedInline(): bool + { + /** @var Site $site */ + $site = $this->getRequest()->getAttribute('site'); + return $site instanceof Site + && $site->getSettings()?->get('ew-base.inlineCssStyles'); + } + protected function getRequest(): ServerRequestInterface { return $GLOBALS['TYPO3_REQUEST']; diff --git a/Classes/Services/Tailwindcss4Merger.php b/Classes/Services/Tailwindcss4Merger.php new file mode 100644 index 0000000..5ec6296 --- /dev/null +++ b/Classes/Services/Tailwindcss4Merger.php @@ -0,0 +1,147 @@ +getLayerOrder($styles); + $atLayerBase = $this->getLayerBase($styles); + $atLayerTheme = $this->getLayerTheme($styles); + $atLayerUtilities = $this->getLayerUtilities($styles); + $atLayerComponents = $this->getLayerComponents($styles); + $atProperty = $this->getProperties($styles); + $atLayerProperties = $this->getLayerProperties($styles); + return implode(chr(10), [ + $atLayerOrder, + $atLayerBase, + $atLayerTheme, + $atLayerUtilities, + $atLayerComponents, + $atProperty, + $atLayerProperties, + ]); + } + + protected function getLayerOrder(string $styles): string + { + preg_match_all('/(?@layer[^;{]+;)/i', $styles, $matches); + return implode(chr(10), array_unique($matches['layer'] ?? [])); + } + + protected function getLayerBase(string $styles): string + { + $matches = $this->getLayerByName($styles, 'base'); + $base = []; + foreach ($matches as $match) { + $match = $this->unwrapLayer($match, 'base'); + $match = preg_replace('/}\n *\./', "}\n||.", $match); + $match = preg_replace('/}\n\W*@/', "}\n||@", $match); + $parts = explode('||', $match); + $base = array_merge($base, $parts); + } + return $this->wrapLayer('base', array_unique($base)); + } + + protected function getLayerTheme(string $styles): string + { + $matches = $this->getLayerByName($styles, 'theme'); + $themes = []; + foreach ($matches as $match) { + preg_match_all('/(?--[^;]+;)/i', $match, $subMatches); + $themes = array_merge($themes, $subMatches['theme'] ?? []); + } + return '@layer theme { + :root, :host {' + . chr(10) + . implode(chr(10), array_unique($themes)) + . chr(10) + . ' } +}'; + } + + protected function getLayerUtilities(string $styles): string + { + $matches = $this->getLayerByName($styles, 'utilities'); + $utilities = []; + foreach ($matches as $match) { + $match = $this->unwrapLayer($match, 'utilities'); + $match = preg_replace('/}\n *\./', "}\n||.", $match); + $parts = explode('||', $match); + $utilities = array_merge($utilities, $parts); + } + return $this->wrapLayer('utilities', array_unique($utilities)); + } + + protected function getLayerComponents(string $styles): string + { + $matches = $this->getLayerByName($styles, 'components'); + $components = []; + foreach ($matches as $match) { + $match = $this->unwrapLayer($match, 'components'); + $match = preg_replace('/}\n *\./', "}\n||.", $match); + $parts = explode('||', $match); + $components = array_merge($components, $parts); + } + return $this->wrapLayer('components', array_unique($components)); + } + + protected function getLayerByName(string $styles, string $name): array + { + preg_match_all('/@layer[^{;]+\{.+?(?=\n})\n}/s', $styles, $matches); + $layer = []; + foreach ($matches['0'] as $match) { + if (str_starts_with($match, '@layer ' . $name) !== false) { + $layer[] = $match; + } + } + return $layer; + } + + protected function getProperties(string $styles): string + { + preg_match_all('/(?@property[^{]+\{[^}]+})/i', $styles, $matches); + return implode(chr(10), array_unique($matches['property'] ?? [])); + } + + protected function getLayerProperties(string $styles): string + { + $matches = $this->getLayerByName($styles, 'properties'); + $properties = [ + ' @supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) { + *, ::before, ::after, ::backdrop {' + ]; + foreach ($matches as $match) { + preg_match_all('/(?--tw[^ ]+: [^;]+;)/i', $match, $subMatches); + $properties = array_merge($properties, $subMatches['properties']); + } + $properties = array_unique($properties); + $properties[] = ' } + }'; + return $this->wrapLayer('properties', $properties); + } + + protected function unwrapLayer(string $styles, string $name): string + { + return trim(str_replace('@layer ' . $name . ' {', '', substr($styles, 0, -1))); + } + + protected function wrapLayer(string $name, array $styles): string + { + return '@layer ' . $name . " {\n" . implode("\n", $styles) . "\n}"; + } +}