* * .foo { color: black; } * * ``` * * @see https://docs.typo3.org/permalink/t3viewhelper:typo3-fluid-asset-css */ final class CssViewHelper extends AbstractTagBasedViewHelper { /** * This VH does not produce direct output, thus does not need to be wrapped in an escaping node * * @var bool */ protected $escapeOutput = false; /** * Rendered children string is passed as CSS code, * there is no point in HTML encoding anything from that. * * @var bool */ protected $escapeChildren = true; public function __construct( private readonly AssetCollector $assetCollector, ) { parent::__construct(); } public function initialize(): void { // Add a tag builder, that does not html encode values, because rendering with encoding happens in AssetRenderer $this->setTagBuilder( new class extends TagBuilder { public function addAttribute($attributeName, $attributeValue, $escapeSpecialCharacters = false): void { parent::addAttribute($attributeName, $attributeValue, false); } } ); parent::initialize(); } public function initializeArguments(): void { parent::initializeArguments(); $this->registerArgument('disabled', 'bool', 'Define whether or not the described stylesheet should be loaded and applied to the document.'); $this->registerArgument('csp', 'bool', 'Whether to collect a CSP hash value for this asset (default: true for external files, false for inline)', false, null); $this->registerArgument('identifier', 'string', 'Use this identifier within templates to only inject your CSS once, even though it is added multiple times.', true); $this->registerArgument('priority', 'boolean', 'Define whether the CSS should be included before other CSS. CSS will always be output in the tag.', false, false); $this->registerArgument('inline', 'bool', 'Define whether or not the referenced file should be loaded as inline styles (Only to be used if \'href\' is set).', false, false); } public function render(): string { $identifier = (string)$this->arguments['identifier']; $attributes = $this->tag->getAttributes(); // boolean attributes shall output attr="attr" if set if ($this->arguments['disabled'] ?? false) { $attributes['disabled'] = 'disabled'; } $file = $attributes['href'] ?? null; unset($attributes['href']); $isExternalFile = $file !== null && !($this->arguments['inline'] ?? false); $useCsp = $this->resolveCspOption($isExternalFile); $options = [ 'priority' => $this->arguments['priority'], 'csp' => $useCsp, ]; if ($file !== null) { if ($this->arguments['inline'] ?? false) { $content = @file_get_contents(GeneralUtility::getFileAbsFileName(trim($file))); if ($content !== false) { $this->assetCollector->addInlineStyleSheet($identifier, $content, $attributes, $options); } } else { $this->assetCollector->addStyleSheet($identifier, $file, $attributes, $options); } } else { $content = (string)$this->renderChildren(); if ($content !== '') { $this->assetCollector->addInlineStyleSheet($identifier, $content, $attributes, $options); } } return ''; } private function resolveCspOption(bool $defaultForStatic): bool { $csp = $this->arguments['csp']; if ($csp !== null) { return (bool)$csp; } // Default: true for external files (allows hash collection), false for inline return $defaultForStatic; } }