From f344429f937f408072e78ff2691e7c19d3143768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Buchmann?= <andy.schliesser@gmail.com> Date: Mon, 27 May 2024 14:28:06 +0200 Subject: [PATCH] [FEATURE] Add file embedding option to asset viewhelpers The viewhelpers f:asset.css and f:asset.script are great but missed an option to render referenced files inline. A boolean option "inline" is now added to load the file contents as inline styles or scripts. This is especially useful for content elements which are used first in a page and need some custom css to improve the Cumulative Layout Shift (CLS). Resolves: #99510 Releases: main Change-Id: Ic4282cd4a6ff00594a0aa0cbdf51f49d80806489 Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/84424 Tested-by: Oliver Bartsch <bo@cedev.de> Tested-by: Garvin Hicking <gh@faktor-e.de> Tested-by: Simon Praetorius <simon@praetorius.me> Tested-by: core-ci <typo3@b13.com> Reviewed-by: Oliver Bartsch <bo@cedev.de> Reviewed-by: Simon Praetorius <simon@praetorius.me> Reviewed-by: Garvin Hicking <gh@faktor-e.de> --- ...dFileEmbeddingOptionToAssetViewhelpers.rst | 42 +++++++++++++++++++ .../ViewHelpers/Asset/CssViewHelper.php | 13 +++++- .../ViewHelpers/Asset/ScriptViewHelper.php | 13 +++++- .../Fixtures/ViewHelpers/CssViewHelper.css | 3 ++ .../Fixtures/ViewHelpers/ScriptViewHelper.js | 1 + .../ViewHelpers/Asset/CssViewHelperTest.php | 17 ++++++++ .../Asset/ScriptViewHelperTest.php | 17 ++++++++ 7 files changed, 104 insertions(+), 2 deletions(-) create mode 100644 typo3/sysext/core/Documentation/Changelog/13.3/Feature-99510-AddFileEmbeddingOptionToAssetViewhelpers.rst create mode 100644 typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/CssViewHelper.css create mode 100644 typo3/sysext/fluid/Tests/Functional/Fixtures/ViewHelpers/ScriptViewHelper.js 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 000000000000..fa845ee3d8aa --- /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 169ba129b811..c126d8bdf700 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 fa57b5d7d439..e47b1299f288 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 000000000000..d1ab3835ebce --- /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 000000000000..9e66e13dc22d --- /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 aaa29b8a1bd5..7cfcec60e8e4 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 1066d23ec674..8d947526ecfb 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']); + } } -- GitLab