diff --git a/typo3/sysext/core/Documentation/Changelog/13.3/Feature-99510-AddFileEmbeddingOptionToAssetViewhelpers.rst b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-99510-AddFileEmbeddingOptionToAssetViewhelpers.rst new file mode 100644 index 0000000000000000000000000000000000000000..fa845ee3d8aac648d580e6b016dec197623988a1 --- /dev/null +++ b/typo3/sysext/core/Documentation/Changelog/13.3/Feature-99510-AddFileEmbeddingOptionToAssetViewhelpers.rst @@ -0,0 +1,42 @@ +.. include:: /Includes.rst.txt + +.. _feature-99510-1716815124: + +================================================================ +Feature: #99510 - Add file embedding option to asset viewhelpers +================================================================ + +See :issue:`99510` + +Description +=========== + +The ViewHelpers :html:`<f:asset.css>` and :html:`<f:asset.script>` have +been extended with a new argument :html:`inline`. If this argument is set, +the referenced asset file is rendered inline. + +Setting the argument will therefore load the file content of the defined +:html:`href` / :html:`src` as inline style or script. This is especially +useful for content elements which are used as first element on a page and +need some custom CSS to improve the Cumulative Layout Shift (CLS). + +Impact +====== + +To add inline styles and scripts from a referenced file, the new :html:`inline` +argument can be set. For example, to add above-the-fold styles, the +:html:`priority` option can be set, which will put the file contents of +:file:`EXT:sitepackage/Resources/Public/Css/my-hero.css` as inline styles +to the :html:`<head>` section. + +.. code-block:: html + + <f:asset.css identifier="my-hero" href="EXT:sitepackage/Resources/Public/Css/my-hero.css" inline="1" priority="1"/> + +To add JavaScript: + +.. code-block:: html + + <f:asset.script identifier="my-hero" src="EXT:sitepackage/Resources/Public/Js/my-hero.js" inline="1" priority="1"/> + +.. index:: Fluid, Frontend, ext:fluid diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php index 169ba129b8112cbafec237effbbe147b085d31d0..c126d8bdf700d3bfd7bae5f594f15dc7a5aa478f 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\ViewHelpers\Asset; use TYPO3\CMS\Core\Page\AssetCollector; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; @@ -43,6 +44,8 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; * Some available attributes are defaults but do not make sense for this ViewHelper. Relevant attributes specific * for this ViewHelper are: as, crossorigin, disabled, href, hreflang, importance, integrity, media, referrerpolicy, * sizes, type, nonce. + * + * Using the "inline" argument, the file content of the referenced file is added as inline style. */ final class CssViewHelper extends AbstractTagBasedViewHelper { @@ -89,6 +92,7 @@ final class CssViewHelper extends AbstractTagBasedViewHelper $this->registerArgument('useNonce', 'bool', 'Whether to use the global nonce value', false, false); $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 <head> 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 @@ -108,7 +112,14 @@ final class CssViewHelper extends AbstractTagBasedViewHelper 'useNonce' => $this->arguments['useNonce'], ]; if ($file !== null) { - $this->assetCollector->addStyleSheet($identifier, $file, $attributes, $options); + 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 !== '') { diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php index fa57b5d7d43974bfbec59f19f64b796d2011c9fc..e47b1299f2885fe0deaa9a869eb358a60a4d5aa1 100644 --- a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php +++ b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php @@ -18,6 +18,7 @@ declare(strict_types=1); namespace TYPO3\CMS\Fluid\ViewHelpers\Asset; use TYPO3\CMS\Core\Page\AssetCollector; +use TYPO3\CMS\Core\Utility\GeneralUtility; use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper; use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; @@ -42,6 +43,8 @@ use TYPO3Fluid\Fluid\Core\ViewHelper\TagBuilder; * * Some available attributes are defaults but do not make sense for this ViewHelper. Relevant attributes specific * for this ViewHelper are: async, crossorigin, defer, integrity, nomodule, nonce, referrerpolicy, src, type. + * + * Using the "inline" argument, the file content of the referenced file is added as inline script. */ final class ScriptViewHelper extends AbstractTagBasedViewHelper { @@ -90,6 +93,7 @@ final class ScriptViewHelper extends AbstractTagBasedViewHelper $this->registerArgument('useNonce', 'bool', 'Whether to use the global nonce value', false, false); $this->registerArgument('identifier', 'string', 'Use this identifier within templates to only inject your JS once, even though it is added multiple times.', true); $this->registerArgument('priority', 'boolean', 'Define whether the JavaScript should be put in the <head> tag above-the-fold or somewhere in the body part.', false, false); + $this->registerArgument('inline', 'bool', 'Define whether or not the referenced file should be loaded as inline script (Only to be used if \'src\' is set).', false, false); } public function render(): string @@ -111,7 +115,14 @@ final class ScriptViewHelper extends AbstractTagBasedViewHelper 'useNonce' => $this->arguments['useNonce'], ]; if ($src !== null) { - $this->assetCollector->addJavaScript($identifier, $src, $attributes, $options); + if ($this->arguments['inline'] ?? false) { + $content = @file_get_contents(GeneralUtility::getFileAbsFileName(trim($src))); + if ($content !== false) { + $this->assetCollector->addInlineJavaScript($identifier, $content, $attributes, $options); + } + } else { + $this->assetCollector->addJavaScript($identifier, $src, $attributes, $options); + } } else { $content = (string)$this->renderChildren(); if ($content !== '') { diff --git a/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/CssViewHelper.css b/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/CssViewHelper.css new file mode 100644 index 0000000000000000000000000000000000000000..d1ab3835ebce57ca9d6c56211095502e9fe9a41e --- /dev/null +++ b/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/CssViewHelper.css @@ -0,0 +1,3 @@ +.foo { + color: black; +} diff --git a/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/ScriptViewHelper.js b/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/ScriptViewHelper.js new file mode 100644 index 0000000000000000000000000000000000000000..9e66e13dc22d115551abbeebdef10be31539a878 --- /dev/null +++ b/typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/ScriptViewHelper.js @@ -0,0 +1 @@ +alert('test'); diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php index aaa29b8a1bd5373c2ff777bc1f0cf5d1a6397007..7cfcec60e8e45297cfb80f9031c80098d8afe59b 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/CssViewHelperTest.php @@ -28,6 +28,10 @@ final class CssViewHelperTest extends FunctionalTestCase { protected bool $initializeDatabase = false; + protected array $pathsToProvideInTestInstance = [ + 'typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/CssViewHelper.css' => 'test.css', + ]; + public static function sourceDataProvider(): array { return [ @@ -133,4 +137,17 @@ final class CssViewHelperTest extends FunctionalTestCase $collectedInlineStyleSheets = $this->get(AssetCollector::class)->getInlineStyleSheets(); self::assertSame($expectation, $collectedInlineStyleSheets['test']['source']); } + + #[Test] + public function inlineRendersFileContentsInline(): void + { + $context = $this->get(RenderingContextFactory::class)->create(); + $context->getTemplatePaths()->setTemplateSource('<f:asset.css identifier="test" href="test.css" inline="1" priority="0"/>'); + + (new TemplateView($context))->render(); + + $collectedInlineStyleSheets = $this->get(AssetCollector::class)->getInlineStyleSheets(); + self::assertSame(".foo {\n color: black;\n}\n", $collectedInlineStyleSheets['test']['source']); + self::assertSame([], $collectedInlineStyleSheets['test']['attributes']); + } } diff --git a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/ScriptViewHelperTest.php b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/ScriptViewHelperTest.php index 1066d23ec674b2a0e8929e66c9bf6a6c4343e4a7..8d947526ecfb420890f6e0103f0d438b9a8c05b6 100644 --- a/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/ScriptViewHelperTest.php +++ b/typo3/sysext/fluid/Tests/Functional/ViewHelpers/Asset/ScriptViewHelperTest.php @@ -28,6 +28,10 @@ final class ScriptViewHelperTest extends FunctionalTestCase { protected bool $initializeDatabase = false; + protected array $pathsToProvideInTestInstance = [ + 'typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/ScriptViewHelper.js' => 'test.js', + ]; + public static function sourceDataProvider(): array { return [ @@ -61,4 +65,17 @@ final class ScriptViewHelperTest extends FunctionalTestCase self::assertSame($collectedJavaScripts['test']['source'], 'my.js'); self::assertSame($collectedJavaScripts['test']['attributes'], ['async' => 'async', 'defer' => 'defer', 'nomodule' => 'nomodule']); } + + #[Test] + public function inlineRendersFileContentsInline(): void + { + $context = $this->get(RenderingContextFactory::class)->create(); + $context->getTemplatePaths()->setTemplateSource('<f:asset.script identifier="test" src="test.js" inline="1" priority="0"/>'); + + (new TemplateView($context))->render(); + + $collectedInlineJavaScripts = $this->get(AssetCollector::class)->getInlineJavaScripts(); + self::assertSame("alert('test');\n", $collectedInlineJavaScripts['test']['source']); + self::assertSame([], $collectedInlineJavaScripts['test']['attributes']); + } }