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