[FEATURE] Introduce AssetCollector 27/63327/14
authorBenni Mack <benni@typo3.org>
Wed, 19 Feb 2020 22:14:16 +0000 (23:14 +0100)
committerSusanne Moog <look@susi.dev>
Tue, 25 Feb 2020 06:05:33 +0000 (07:05 +0100)
AssetCollector is a concept to allow custom CSS/JS code,
inline or external, to be added multiple times in e.g. a Fluid
template (via <f:script> or <f:css> ViewHelpers) and only be
added once.

It considers best practices by having a "priority" flag to be either
be moved in the <head> area (for CSS useful in above-the-fold concepts)
or at the bottom of the <body> tag contents.

AssetCollector helps to work with content elements as components,
reducing effectively the amount of CSS to be loaded and also incorporates
the HTTP/2 power where it is not relevant to have all files compressed
and concatenated in one file (although this could be added later-on).

AssetCollector is implemented as singleton and should slowly replace
the various existing options in TypoScript which seem to be confusing.

AssetCollector also collects information about "imagesOnPage"
effectively taking off pressure from PageRenderer and TSFE to
store common data in FE - as this is now handled in AssetCollector,
which can be used in cached and non-cached components.

Resolves: #90522
Releases: master
Change-Id: I6ce8141ad8891a7a8ee6d4f8a7377d93a894c3b8
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/63327
Tested-by: Daniel Goerz <daniel.goerz@posteo.de>
Tested-by: Georg Ringer <georg.ringer@gmail.com>
Tested-by: TYPO3com <noreply@typo3.com>
Tested-by: Kevin Appelt <kevin.appelt@icloud.com>
Tested-by: Daniel Gohlke <daniel.gohlke@extco.de>
Tested-by: Susanne Moog <look@susi.dev>
Reviewed-by: Daniel Goerz <daniel.goerz@posteo.de>
Reviewed-by: Georg Ringer <georg.ringer@gmail.com>
Reviewed-by: Kevin Appelt <kevin.appelt@icloud.com>
Reviewed-by: Daniel Gohlke <daniel.gohlke@extco.de>
Reviewed-by: Susanne Moog <look@susi.dev>
15 files changed:
typo3/sysext/adminpanel/Classes/Modules/Info/GeneralInformation.php
typo3/sysext/core/Classes/Page/AssetCollector.php [new file with mode: 0644]
typo3/sysext/core/Classes/Page/AssetRenderer.php [new file with mode: 0644]
typo3/sysext/core/Classes/Page/PageRenderer.php
typo3/sysext/core/Documentation/Changelog/master/Deprecation-90522-TSFEPropertiesRegardingImages.rst [new file with mode: 0644]
typo3/sysext/core/Documentation/Changelog/master/Feature-90522-IntroduceAssetCollector.rst [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Page/AssetCollectorTest.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Page/AssetDataProvider.php [new file with mode: 0644]
typo3/sysext/core/Tests/Unit/Page/AssetRendererTest.php [new file with mode: 0644]
typo3/sysext/extbase/Classes/Service/ImageService.php
typo3/sysext/extbase/Tests/Unit/Service/ImageScriptServiceTest.php
typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php [new file with mode: 0644]
typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php [new file with mode: 0644]
typo3/sysext/frontend/Classes/ContentObject/ContentObjectRenderer.php
typo3/sysext/frontend/Classes/Controller/TypoScriptFrontendController.php

index 3b98c82..c38585c 100644 (file)
@@ -22,6 +22,7 @@ use TYPO3\CMS\Adminpanel\ModuleApi\DataProviderInterface;
 use TYPO3\CMS\Adminpanel\ModuleApi\ModuleData;
 use TYPO3\CMS\Core\Context\Context;
 use TYPO3\CMS\Core\Context\UserAspect;
+use TYPO3\CMS\Core\Page\AssetCollector;
 use TYPO3\CMS\Core\TimeTracker\TimeTracker;
 use TYPO3\CMS\Core\Utility\GeneralUtility;
 use TYPO3\CMS\Fluid\View\StandaloneView;
@@ -128,17 +129,16 @@ class GeneralInformation extends AbstractSubModule implements DataProviderInterf
 
         $count = 0;
         $totalImageSize = 0;
-        if (!empty($this->getTypoScriptFrontendController()->imagesOnPage)) {
-            foreach ($this->getTypoScriptFrontendController()->imagesOnPage as $file) {
-                $fileSize = @filesize($file);
-                $imagesOnPage['files'][] = [
-                    'name' => $file,
-                    'size' => $fileSize,
-                    'sizeHuman' => GeneralUtility::formatSize($fileSize),
-                ];
-                $totalImageSize += $fileSize;
-                $count++;
-            }
+        $imagesOnPage = GeneralUtility::makeInstance(AssetCollector::class)->getMedia();
+        foreach ($imagesOnPage as $file => $information) {
+            $fileSize = @filesize($file);
+            $imagesOnPage['files'][] = [
+                'name' => $file,
+                'size' => $fileSize,
+                'sizeHuman' => GeneralUtility::formatSize($fileSize),
+            ];
+            $totalImageSize += $fileSize;
+            $count++;
         }
         $imagesOnPage['totalSize'] = GeneralUtility::formatSize($totalImageSize);
         $imagesOnPage['total'] = $count;
diff --git a/typo3/sysext/core/Classes/Page/AssetCollector.php b/typo3/sysext/core/Classes/Page/AssetCollector.php
new file mode 100644 (file)
index 0000000..d46b23e
--- /dev/null
@@ -0,0 +1,186 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Page;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\SingletonInterface;
+use TYPO3\CMS\Core\Utility\ArrayUtility;
+
+/**
+ * The Asset Collector is responsible for keeping track of
+ * - everything within <script> tags: javascript files and inline javascript code
+ * - inline CSS and CSS files
+ *
+ * The goal of the asset collector is to:
+ * - utilize a single "runtime-based" store for adding assets of certain kinds that are added to the output
+ * - allow to deal with assets from non-cacheable plugins and cacheable content in the Frontend
+ * - reduce the "power" and flexibility (I'd say it's a burden) of the "god class" PageRenderer.
+ * - reduce the burden of storing everything in PageRenderer
+ *
+ * As a side-effect this allows to:
+ * - Add a single CSS snippet or CSS file per content block, but assure that the CSS is only added once to the output.
+ *
+ * Note on the implementation:
+ * - We use a Singleton to make use of the AssetCollector throughout Frontend process (similar to PageRenderer).
+ * - Although this is not optimal, I don't see any other way to do so in the current code.
+ *
+ * https://developer.wordpress.org/reference/functions/wp_enqueue_style/
+ */
+class AssetCollector implements SingletonInterface
+{
+    /**
+     * @var array
+     */
+    protected $javaScripts = [];
+
+    /**
+     * @var array
+     */
+    protected $inlineJavaScripts = [];
+
+    /**
+     * @var array
+     */
+    protected $styleSheets = [];
+
+    /**
+     * @var array
+     */
+    protected $inlineStyleSheets = [];
+
+    /**
+     * @var array
+     */
+    protected $media = [];
+
+    public function addJavaScript(string $identifier, string $source, array $attributes, array $options = []): self
+    {
+        $existingAttributes = $this->javaScripts[$identifier]['attributes'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingAttributes, $attributes);
+        $existingOptions = $this->javaScripts[$identifier]['options'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingOptions, $options);
+        $this->javaScripts[$identifier] = [
+            'source' => $source,
+            'attributes' => $existingAttributes,
+            'options' => $existingOptions
+        ];
+        return $this;
+    }
+
+    public function addInlineJavaScript(string $identifier, string $source, array $attributes, array $options = []): self
+    {
+        $existingAttributes = $this->inlineJavaScripts[$identifier]['attributes'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingAttributes, $attributes);
+        $existingOptions = $this->inlineJavaScripts[$identifier]['options'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingOptions, $options);
+        $this->inlineJavaScripts[$identifier] = [
+            'source' => $source,
+            'attributes' => $existingAttributes,
+            'options' => $existingOptions
+        ];
+        return $this;
+    }
+
+    public function addStyleSheet(string $identifier, string $source, array $attributes, array $options = []): self
+    {
+        $existingAttributes = $this->styleSheets[$identifier]['attributes'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingAttributes, $attributes);
+        $existingOptions = $this->styleSheets[$identifier]['options'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingOptions, $options);
+        $this->styleSheets[$identifier] = [
+            'source' => $source,
+            'attributes' => $existingAttributes,
+            'options' => $existingOptions
+        ];
+        return $this;
+    }
+
+    public function addInlineStyleSheet(string $identifier, string $source, array $attributes, array $options = []): self
+    {
+        $existingAttributes = $this->inlineStyleSheets[$identifier]['attributes'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingAttributes, $attributes);
+        $existingOptions = $this->inlineStyleSheets[$identifier]['options'] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingOptions, $options);
+        $this->inlineStyleSheets[$identifier] = [
+            'source' => $source,
+            'attributes' => $existingAttributes,
+            'options' => $existingOptions
+        ];
+        return $this;
+    }
+
+    public function addMedia(string $fileName, array $additionalInformation): self
+    {
+        $existingAdditionalInformation = $this->media[$fileName] ?? [];
+        ArrayUtility::mergeRecursiveWithOverrule($existingAdditionalInformation, $additionalInformation);
+        $this->media[$fileName] = $existingAdditionalInformation;
+        return $this;
+    }
+
+    public function removeJavaScript(string $identifier): self
+    {
+        unset($this->javaScripts[$identifier]);
+        return $this;
+    }
+
+    public function removeInlineJavaScript(string $identifier): self
+    {
+        unset($this->inlineJavaScripts[$identifier]);
+        return $this;
+    }
+
+    public function removeStyleSheet(string $identifier): self
+    {
+        unset($this->styleSheets[$identifier]);
+        return $this;
+    }
+
+    public function removeInlineStyleSheet(string $identifier): self
+    {
+        unset($this->inlineStyleSheets[$identifier]);
+        return $this;
+    }
+
+    public function removeMedia(string $identifier): self
+    {
+        unset($this->media[$identifier]);
+        return $this;
+    }
+
+    public function getMedia(): array
+    {
+        return $this->media;
+    }
+
+    public function getJavaScripts(): array
+    {
+        return $this->javaScripts;
+    }
+
+    public function getInlineJavaScripts(): array
+    {
+        return $this->inlineJavaScripts;
+    }
+
+    public function getStyleSheets(): array
+    {
+        return $this->styleSheets;
+    }
+
+    public function getInlineStyleSheets(): array
+    {
+        return $this->inlineStyleSheets;
+    }
+}
diff --git a/typo3/sysext/core/Classes/Page/AssetRenderer.php b/typo3/sysext/core/Classes/Page/AssetRenderer.php
new file mode 100644 (file)
index 0000000..6f9717f
--- /dev/null
@@ -0,0 +1,93 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Core\Page;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\CMS\Core\Utility\PathUtility;
+
+/**
+ * Class AssetRenderer
+ * @internal The AssetRenderer is used for the asset rendering and is not public API
+ */
+class AssetRenderer
+{
+    protected $assetCollector;
+
+    public function __construct(AssetCollector $assetCollector = null)
+    {
+        $this->assetCollector = $assetCollector ?? GeneralUtility::makeInstance(AssetCollector::class);
+    }
+
+    public function renderInlineJavaScript($priority = false): string
+    {
+        $template = '<script%attributes%>%source%</script>';
+        $assets = $this->assetCollector->getInlineJavaScripts();
+        foreach ($assets as &$assetData) {
+            $assetData['attributes']['type'] = $assetData['attributes']['type'] ?? 'text/javascript';
+        }
+        return $this->render($assets, $template, $priority);
+    }
+
+    public function renderJavaScript($priority = false): string
+    {
+        $template = '<script%attributes%></script>';
+        $assets = $this->assetCollector->getJavaScripts();
+        foreach ($assets as &$assetData) {
+            $assetData['attributes']['src'] = $this->getAbsoluteWebPath($assetData['source']);
+            $assetData['attributes']['type'] = $assetData['attributes']['type'] ?? 'text/javascript';
+        }
+        return $this->render($assets, $template, $priority);
+    }
+
+    public function renderInlineStyleSheets($priority = false): string
+    {
+        $template = '<style%attributes%>%source%</style>';
+        $assets = $this->assetCollector->getInlineStyleSheets();
+        return $this->render($assets, $template, $priority);
+    }
+
+    public function renderStyleSheets(bool $priority = false, string $endingSlash = ''): string
+    {
+        $template = '<link%attributes% ' . $endingSlash . '>';
+        $assets = $this->assetCollector->getStyleSheets();
+        foreach ($assets as &$assetData) {
+            $assetData['attributes']['href'] = $this->getAbsoluteWebPath($assetData['source']);
+            $assetData['attributes']['rel'] = $assetData['attributes']['rel'] ?? 'stylesheet';
+            $assetData['attributes']['type'] = $assetData['attributes']['type'] ?? 'text/css';
+        }
+        return $this->render($assets, $template, $priority);
+    }
+
+    protected function render(array $assets, string $template, bool $priority = false): string
+    {
+        $results = [];
+        foreach ($assets as $assetData) {
+            $attributes = $assetData['attributes'];
+            $attributesString = count($attributes) ? ' ' . GeneralUtility::implodeAttributes($attributes, true) : '';
+            $code = str_replace(['%attributes%', '%source%'], [$attributesString, $assetData['source']], $template);
+            $hasPriority = $assetData['options']['priority'] ?? false;
+            if ($hasPriority === $priority) {
+                $results[] = $code;
+            }
+        }
+        return implode(LF, $results);
+    }
+
+    private function getAbsoluteWebPath(string $file): string
+    {
+        return PathUtility::getAbsoluteWebPath(GeneralUtility::getFileAbsFileName($file));
+    }
+}
index ec3631d..6dc7c5b 100644 (file)
@@ -1787,6 +1787,17 @@ class PageRenderer implements SingletonInterface
             $jsFooterInline = $jsInline . LF . $jsFooterInline;
             $jsInline = '';
         }
+        // Use AssetRenderer to inject all JavaScripts and CSS files
+        $assetRenderer = GeneralUtility::makeInstance(AssetRenderer::class);
+        $jsInline .= $assetRenderer->renderInlineJavaScript(true);
+        $jsFooterInline .= $assetRenderer->renderInlineJavaScript();
+        $jsFiles .= $assetRenderer->renderJavaScript(true);
+        $jsFooterFiles .= $assetRenderer->renderJavaScript();
+        $cssInline .= $assetRenderer->renderInlineStyleSheets(true);
+        $jsFooterFiles .= $assetRenderer->renderInlineStyleSheets();
+        $cssLibs .= $assetRenderer->renderStyleSheets(true, $this->endingSlash);
+        $cssFiles .= $assetRenderer->renderStyleSheets(false, $this->endingSlash);
+
         $this->executePostRenderHook($jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs);
         return [$jsLibs, $jsFiles, $jsFooterFiles, $cssLibs, $cssFiles, $jsInline, $cssInline, $jsFooterInline, $jsFooterLibs];
     }
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90522-TSFEPropertiesRegardingImages.rst b/typo3/sysext/core/Documentation/Changelog/master/Deprecation-90522-TSFEPropertiesRegardingImages.rst
new file mode 100644 (file)
index 0000000..b754b05
--- /dev/null
@@ -0,0 +1,35 @@
+.. include:: ../../Includes.txt
+
+======================================================
+Deprecation: #90522 - TSFE properties regarding images
+======================================================
+
+See :issue:`90522`
+
+Description
+===========
+
+The image related properties :php:`$imagesOnPage` and :php:`$lastImageInfo` of
+:php:`TypoScriptFrontendController` have been marked as deprecated.
+
+Impact
+======
+
+Calling these deprecated properties will trigger a deprecation notice.
+
+Affected Installations
+======================
+
+All installations using these properties are affected.
+
+Migration
+=========
+
+For :php:`$imagesOnPage` the AssetCollector may be used instead:
+
+.. code-block:: php
+
+   $assetCollector = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(TYPO3\CMS\Core\Page\AssetCollector::class);
+   $imagesOnPage = $assetCollector->getMedia();
+
+.. index:: Frontend, PHP-API, NotScanned, ext:core
diff --git a/typo3/sysext/core/Documentation/Changelog/master/Feature-90522-IntroduceAssetCollector.rst b/typo3/sysext/core/Documentation/Changelog/master/Feature-90522-IntroduceAssetCollector.rst
new file mode 100644 (file)
index 0000000..116724a
--- /dev/null
@@ -0,0 +1,80 @@
+.. include:: ../../Includes.txt
+
+==========================================
+Feature: #90522 - Introduce AssetCollector
+==========================================
+
+See :issue:`90522`
+
+Description
+===========
+
+AssetCollector is a concept to allow custom CSS/JS code, inline or external, to be added multiple
+times in e.g. a Fluid template (via <f:script> or <f:css> ViewHelpers) but only rendered once
+in the output.
+
+It supports best practices for optimizing page performance by having a "priority" flag to either
+move the asset to the <head> section (useful for CSS in above-the-fold concepts) or to the
+bottom of the <body> tag.
+
+AssetCollector helps to work with content elements as components, effectively reducing the amount
+of CSS to be loaded. It also incorporates the HTTP/2 power where it is not relevant to have all
+files compressed and concatenated in one file (although this could be added later-on).
+
+AssetCollector is implemented as singleton and should slowly replace the various existing options
+in TypoScript.
+
+AssetCollector also collects information about "imagesOnPage" effectively taking off pressure from
+PageRenderer and TSFE to store common data in FE - as this is now handled in AssetCollector,
+which can be used in cached and non-cached components.
+
+The new API
+-----------
+
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::addJavaScript(string $identifier, string $source, array $attributes, array $options = []): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::addInlineJavaScript(string $identifier, string $source, array $attributes, array $options = []): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::addStyleSheet(string $identifier, string $source, array $attributes, array $options = []): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::addInlineStyleSheet(string $identifier, string $source, array $attributes, array $options = []): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::addMedia(string $fileName, array $additionalInformation): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeJavaScript(string $identifier): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeInlineJavaScript(string $identifier): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeStyleSheet(string $identifier): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeInlineStyleSheet(string $identifier): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::removeMedia(string $identifier): self`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getJavaScripts(): array`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineJavaScripts(): array`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getStyleSheets(): array`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getInlineStyleSheets(): array`
+- :php:`\TYPO3\CMS\Core\Page\AssetCollector::getMedia(): array`
+
+New ViewHelpers
+---------------
+
+There are also two new ViewHelpers, the :html:`<f:asset.css>` and the - :html:`<f:asset.script>` ViewHelper which use the AssetCollector API.
+
+.. code-block:: html
+
+   <f:asset.css identifier="identifier123" href="EXT:my_ext/Resources/Public/Css/foo.css" />
+   <f:asset.css identifier="identifier123">
+      .foo { color: black; }
+   </f:asset.css>
+
+   <f:asset.script identifier="identifier123" src="EXT:my_ext/Resources/Public/JavaScript/foo.js" />
+   <f:asset.script identifier="identifier123">
+      alert('hello world');
+   </f:asset.script>
+
+
+Examples
+--------
+
+    // Add a JavaScript file to the collector with script attribute data-foo="bar"
+    GeneralUtility::makeInstance(AssetCollector::class)
+       ->addJavaScript('my_ext_foo', 'EXT:my_ext/Resources/Public/JavaScript/foo.js', ['data-foo' => 'bar']]);
+
+    // Add a JavaScript file to the collector with script attribute data-foo="bar" and priority
+    // which means rendering before other script tags
+    GeneralUtility::makeInstance(AssetCollector::class)
+       ->addJavaScript('my_ext_foo', 'EXT:my_ext/Resources/Public/JavaScript/foo.js', ['data-foo' => 'bar'], ['priority' => true]]);
+
+.. index:: Backend, Frontend, PHP-API, ext:core
diff --git a/typo3/sysext/core/Tests/Unit/Page/AssetCollectorTest.php b/typo3/sysext/core/Tests/Unit/Page/AssetCollectorTest.php
new file mode 100644 (file)
index 0000000..e592def
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Page;
+
+use TYPO3\CMS\Core\Page\AssetCollector;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class AssetCollectorTest extends UnitTestCase
+{
+    /**
+     * @var AssetCollector
+     */
+    protected $assetCollector;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->resetSingletonInstances = true;
+        $this->assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
+    }
+
+    /**
+     * @param array $files
+     * @param array $expectedResult
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::filesDataProvider
+     */
+    public function testStyleSheets(array $files, array $expectedResult): void
+    {
+        foreach ($files as $file) {
+            [$identifier, $source, $attributes, $options] = $file;
+            $this->assetCollector->addStyleSheet($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedResult, $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getMedia());
+        foreach ($files as $file) {
+            [$identifier] = $file;
+            $this->assetCollector->removeStyleSheet($identifier);
+        }
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getMedia());
+    }
+
+    /**
+     * @param array $files
+     * @param array $expectedResult
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::filesDataProvider
+     */
+    public function testJavaScript(array $files, array $expectedResult): void
+    {
+        foreach ($files as $file) {
+            [$identifier, $source, $attributes, $options] = $file;
+            $this->assetCollector->addJavaScript($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedResult, $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+        foreach ($files as $file) {
+            [$identifier] = $file;
+            $this->assetCollector->removeJavaScript($identifier);
+        }
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+    }
+
+    /**
+     * @param array $sources
+     * @param array $expectedResult
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::inlineDataProvider
+     */
+    public function testInlineJavaScript(array $sources, array $expectedResult): void
+    {
+        foreach ($sources as $source) {
+            [$identifier, $source, $attributes, $options] = $source;
+            $this->assetCollector->addInlineJavaScript($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedResult, $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+        foreach ($sources as $source) {
+            [$identifier] = $source;
+            $this->assetCollector->removeInlineJavaScript($identifier);
+        }
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+    }
+
+    /**
+     * @param array $sources
+     * @param array $expectedResult
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::inlineDataProvider
+     */
+    public function testInlineStyles(array $sources, array $expectedResult): void
+    {
+        foreach ($sources as $source) {
+            [$identifier, $source, $attributes, $options] = $source;
+            $this->assetCollector->addInlineStyleSheet($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedResult, $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+        foreach ($sources as $source) {
+            [$identifier] = $source;
+            $this->assetCollector->removeInlineStyleSheet($identifier);
+        }
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        self::assertSame([], $this->assetCollector->getMedia());
+    }
+
+    /**
+     * @param array $images
+     * @param array $expectedResult
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::mediaDataProvider
+     */
+    public function testMedia(array $images, array $expectedResult): void
+    {
+        foreach ($images as $image) {
+            [$fileName, $additionalInformation] = $image;
+            $this->assetCollector->addMedia($fileName, $additionalInformation);
+        }
+        self::assertSame($expectedResult, $this->assetCollector->getMedia());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+        foreach ($images as $image) {
+            [$fileName] = $image;
+            $this->assetCollector->removeMedia($fileName);
+        }
+        self::assertSame([], $this->assetCollector->getMedia());
+        self::assertSame([], $this->assetCollector->getInlineStyleSheets());
+        self::assertSame([], $this->assetCollector->getInlineJavaScripts());
+        self::assertSame([], $this->assetCollector->getJavaScripts());
+        self::assertSame([], $this->assetCollector->getStyleSheets());
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Page/AssetDataProvider.php b/typo3/sysext/core/Tests/Unit/Page/AssetDataProvider.php
new file mode 100644 (file)
index 0000000..530472e
--- /dev/null
@@ -0,0 +1,403 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Page;
+
+class AssetDataProvider
+{
+    public static function filesDataProvider(): array
+    {
+        return [
+            '1 file from fileadmin' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', [], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link href="fileadmin/foo.ext" rel="stylesheet" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script src="fileadmin/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '1 file from extension' => [
+                [
+                    ['file1', 'EXT:core/Resource/Public/foo.ext', [], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'EXT:core/Resource/Public/foo.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link href="typo3/sysext/core/Resource/Public/foo.ext" rel="stylesheet" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script src="typo3/sysext/core/Resource/Public/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '2 files' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', [], []],
+                    ['file2', 'EXT:core/Resource/Public/foo.ext', [], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ],
+                    'file2' => [
+                        'source' => 'EXT:core/Resource/Public/foo.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link href="fileadmin/foo.ext" rel="stylesheet" type="text/css" >' . LF . '<link href="typo3/sysext/core/Resource/Public/foo.ext" rel="stylesheet" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script src="fileadmin/foo.ext" type="text/javascript"></script>' . LF . '<script src="typo3/sysext/core/Resource/Public/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '2 files with override' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', [], []],
+                    ['file2', 'EXT:core/Resource/Public/foo.ext', [], []],
+                    ['file1', 'EXT:core/Resource/Public/bar.ext', [], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'EXT:core/Resource/Public/bar.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ],
+                    'file2' => [
+                        'source' => 'EXT:core/Resource/Public/foo.ext',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link href="typo3/sysext/core/Resource/Public/bar.ext" rel="stylesheet" type="text/css" >' . LF . '<link href="typo3/sysext/core/Resource/Public/foo.ext" rel="stylesheet" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script src="typo3/sysext/core/Resource/Public/bar.ext" type="text/javascript"></script>' . LF . '<script src="typo3/sysext/core/Resource/Public/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '1 file with attributes' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', ['rel' => 'foo'], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [
+                            'rel' => 'foo'
+                        ],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link rel="foo" href="fileadmin/foo.ext" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script rel="foo" src="fileadmin/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '1 file with attributes override' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', ['rel' => 'foo', 'another' => 'keep on override'], []],
+                    ['file1', 'fileadmin/foo.ext', ['rel' => 'bar'], []]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [
+                            'rel' => 'bar',
+                            'another' => 'keep on override'
+                        ],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link rel="bar" another="keep on override" href="fileadmin/foo.ext" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script rel="bar" another="keep on override" src="fileadmin/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '1 file with options' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', [], ['priority' => true]]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [],
+                        'options' => [
+                            'priority' => true
+                        ],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '',
+                    'css_prio' => '<link href="fileadmin/foo.ext" rel="stylesheet" type="text/css" >',
+                    'js_no_prio' => '',
+                    'js_prio' => '<script src="fileadmin/foo.ext" type="text/javascript"></script>',
+                ]
+            ],
+            '1 file with options override' => [
+                [
+                    ['file1', 'fileadmin/foo.ext', [], ['priority' => true, 'another' => 'keep on override']],
+                    ['file1', 'fileadmin/foo.ext', [], ['priority' => false]]
+                ],
+                [
+                    'file1' => [
+                        'source' => 'fileadmin/foo.ext',
+                        'attributes' => [],
+                        'options' => [
+                            'priority' => false,
+                            'another' => 'keep on override'
+                        ],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<link href="fileadmin/foo.ext" rel="stylesheet" type="text/css" >',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script src="fileadmin/foo.ext" type="text/javascript"></script>',
+                    'js_prio' => '',
+                ]
+            ],
+        ];
+    }
+
+    public static function inlineDataProvider(): array
+    {
+        return [
+            'simple data' => [
+                [
+                    ['identifier_1', 'foo bar baz', [], []]
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style>foo bar baz</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script type="text/javascript">foo bar baz</script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '2 times simple data' => [
+                [
+                    ['identifier_1', 'foo bar baz', [], []],
+                    ['identifier_2', 'bar baz foo', [], []]
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [],
+                        'options' => [],
+                    ],
+                    'identifier_2' => [
+                        'source' => 'bar baz foo',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style>foo bar baz</style>' . LF . '<style>bar baz foo</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script type="text/javascript">foo bar baz</script>' . LF . '<script type="text/javascript">bar baz foo</script>',
+                    'js_prio' => '',
+                ]
+            ],
+            '2 times simple data with override' => [
+                [
+                    ['identifier_1', 'foo bar baz', [], []],
+                    ['identifier_2', 'bar baz foo', [], []],
+                    ['identifier_1', 'baz foo bar', [], []],
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'baz foo bar',
+                        'attributes' => [],
+                        'options' => [],
+                    ],
+                    'identifier_2' => [
+                        'source' => 'bar baz foo',
+                        'attributes' => [],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style>baz foo bar</style>' . LF . '<style>bar baz foo</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script type="text/javascript">baz foo bar</script>' . LF . '<script type="text/javascript">bar baz foo</script>',
+                    'js_prio' => '',
+                ]
+            ],
+            'simple data with attributes' => [
+                [
+                    ['identifier_1', 'foo bar baz', ['rel' => 'foo'], []],
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [
+                            'rel' => 'foo'
+                        ],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style rel="foo">foo bar baz</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script rel="foo" type="text/javascript">foo bar baz</script>',
+                    'js_prio' => '',
+                ]
+            ],
+            'simple data with attributes override' => [
+                [
+                    ['identifier_1', 'foo bar baz', ['rel' => 'foo', 'another' => 'keep on override'], []],
+                    ['identifier_1', 'foo bar baz', ['rel' => 'bar'], []],
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [
+                            'rel' => 'bar',
+                            'another' => 'keep on override'
+                        ],
+                        'options' => [],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style rel="bar" another="keep on override">foo bar baz</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script rel="bar" another="keep on override" type="text/javascript">foo bar baz</script>',
+                    'js_prio' => '',
+                ]
+            ],
+            'simple data with options' => [
+                [
+                    ['identifier_1', 'foo bar baz', [], ['priority' => true]]
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [],
+                        'options' => [
+                            'priority' => true
+                        ],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '',
+                    'css_prio' => '<style>foo bar baz</style>',
+                    'js_no_prio' => '',
+                    'js_prio' => '<script type="text/javascript">foo bar baz</script>',
+                ]
+            ],
+            'simple data with options override' => [
+                [
+                    ['identifier_1', 'foo bar baz', [], ['priority' => true, 'another' => 'keep on override']],
+                    ['identifier_1', 'foo bar baz', [], ['priority' => false]]
+                ],
+                [
+                    'identifier_1' => [
+                        'source' => 'foo bar baz',
+                        'attributes' => [],
+                        'options' => [
+                            'priority' => false,
+                            'another' => 'keep on override'
+                        ],
+                    ]
+                ],
+                [
+                    'css_no_prio' => '<style>foo bar baz</style>',
+                    'css_prio' => '',
+                    'js_no_prio' => '<script type="text/javascript">foo bar baz</script>',
+                    'js_prio' => '',
+                ]
+            ],
+        ];
+    }
+
+    public static function mediaDataProvider(): array
+    {
+        return [
+            '1 image no additional information' => [
+                [
+                    ['fileadmin/foo.png', []]
+                ],
+                [
+                    'fileadmin/foo.png' => []
+                ]
+            ],
+            '2 images no additional information' => [
+                [
+                    ['fileadmin/foo.png', []],
+                    ['fileadmin/bar.png', []],
+                ],
+                [
+                    'fileadmin/foo.png' => [],
+                    'fileadmin/bar.png' => [],
+                ]
+            ],
+            '1 image with additional information' => [
+                [
+                    ['fileadmin/foo.png', ['foo' => 'bar']]
+                ],
+                [
+                    'fileadmin/foo.png' => ['foo' => 'bar']
+                ]
+            ],
+            '2 images with additional information' => [
+                [
+                    ['fileadmin/foo.png', ['foo' => 'bar']],
+                    ['fileadmin/bar.png', ['foo' => 'baz']],
+                ],
+                [
+                    'fileadmin/foo.png' => ['foo' => 'bar'],
+                    'fileadmin/bar.png' => ['foo' => 'baz'],
+                ]
+            ],
+            '2 images with additional information override' => [
+                [
+                    ['fileadmin/foo.png', ['foo' => 'bar']],
+                    ['fileadmin/bar.png', ['foo' => 'baz']],
+                    ['fileadmin/foo.png', ['foo' => 'baz']],
+                ],
+                [
+                    'fileadmin/foo.png' => ['foo' => 'baz'],
+                    'fileadmin/bar.png' => ['foo' => 'baz'],
+                ]
+            ],
+            '2 images with additional information override keep existing' => [
+                [
+                    ['fileadmin/foo.png', ['foo' => 'bar', 'bar' => 'baz']],
+                    ['fileadmin/bar.png', ['foo' => 'baz']],
+                    ['fileadmin/foo.png', ['foo' => 'baz']],
+                ],
+                [
+                    'fileadmin/foo.png' => ['foo' => 'baz', 'bar' => 'baz'],
+                    'fileadmin/bar.png' => ['foo' => 'baz'],
+                ]
+            ],
+        ];
+    }
+}
diff --git a/typo3/sysext/core/Tests/Unit/Page/AssetRendererTest.php b/typo3/sysext/core/Tests/Unit/Page/AssetRendererTest.php
new file mode 100644 (file)
index 0000000..b3bd0fd
--- /dev/null
@@ -0,0 +1,92 @@
+<?php
+declare(strict_types = 1);
+
+namespace TYPO3\CMS\Core\Tests\Unit\Page;
+
+use TYPO3\CMS\Core\Page\AssetCollector;
+use TYPO3\CMS\Core\Page\AssetRenderer;
+use TYPO3\CMS\Core\Utility\GeneralUtility;
+use TYPO3\TestingFramework\Core\Unit\UnitTestCase;
+
+class AssetRendererTest extends UnitTestCase
+{
+    /**
+     * @var AssetRenderer
+     */
+    protected $assetRenderer;
+
+    public function setUp(): void
+    {
+        parent::setUp();
+        $this->resetSingletonInstances = true;
+        $this->assetRenderer = GeneralUtility::makeInstance(AssetRenderer::class);
+    }
+
+    /**
+     * @param array $files
+     * @param array $expectedResult
+     * @param array $expectedMarkup
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::filesDataProvider
+     */
+    public function testStyleSheets(array $files, array $expectedResult, array $expectedMarkup): void
+    {
+        $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
+        foreach ($files as $file) {
+            [$identifier, $source, $attributes, $options] = $file;
+            $assetCollector->addStyleSheet($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedMarkup['css_no_prio'], $this->assetRenderer->renderStyleSheets());
+        self::assertSame($expectedMarkup['css_prio'], $this->assetRenderer->renderStyleSheets(true));
+    }
+
+    /**
+     * @param array $files
+     * @param array $expectedResult
+     * @param array $expectedMarkup
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::filesDataProvider
+     */
+    public function testJavaScript(array $files, array $expectedResult, array $expectedMarkup): void
+    {
+        $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
+        foreach ($files as $file) {
+            [$identifier, $source, $attributes, $options] = $file;
+            $assetCollector->addJavaScript($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedMarkup['js_no_prio'], $this->assetRenderer->renderJavaScript());
+        self::assertSame($expectedMarkup['js_prio'], $this->assetRenderer->renderJavaScript(true));
+    }
+
+    /**
+     * @param array $sources
+     * @param array $expectedResult
+     * @param array $expectedMarkup
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::inlineDataProvider
+     */
+    public function testInlineJavaScript(array $sources, array $expectedResult, array $expectedMarkup): void
+    {
+        $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
+        foreach ($sources as $source) {
+            [$identifier, $source, $attributes, $options] = $source;
+            $assetCollector->addInlineJavaScript($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedMarkup['js_no_prio'], $this->assetRenderer->renderInlineJavaScript());
+        self::assertSame($expectedMarkup['js_prio'], $this->assetRenderer->renderInlineJavaScript(true));
+    }
+
+    /**
+     * @param array $sources
+     * @param array $expectedResult
+     * @param array $expectedMarkup
+     * @dataProvider \TYPO3\CMS\Core\Tests\Unit\Page\AssetDataProvider::inlineDataProvider
+     */
+    public function testInlineStyleSheets(array $sources, array $expectedResult, array $expectedMarkup): void
+    {
+        $assetCollector = GeneralUtility::makeInstance(AssetCollector::class);
+        foreach ($sources as $source) {
+            [$identifier, $source, $attributes, $options] = $source;
+            $assetCollector->addInlineStyleSheet($identifier, $source, $attributes, $options);
+        }
+        self::assertSame($expectedMarkup['css_no_prio'], $this->assetRenderer->renderInlineStyleSheets());
+        self::assertSame($expectedMarkup['css_prio'], $this->assetRenderer->renderInlineStyleSheets(true));
+    }
+}
index 32b463a..db04869 100644 (file)
@@ -17,6 +17,7 @@ namespace TYPO3\CMS\Extbase\Service;
  */
 
 use TYPO3\CMS\Core\LinkHandling\LinkService;
+use TYPO3\CMS\Core\Page\AssetCollector;
 use TYPO3\CMS\Core\Resource\File;
 use TYPO3\CMS\Core\Resource\FileInterface;
 use TYPO3\CMS\Core\Resource\FileReference;
@@ -183,6 +184,10 @@ class ImageService implements \TYPO3\CMS\Core\SingletonInterface
             $GLOBALS['TSFE']->lastImageInfo = $this->getCompatibilityImageResourceValues($processedImage);
             $GLOBALS['TSFE']->imagesOnPage[] = $processedImage->getPublicUrl();
         }
+        GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
+            $processedImage->getPublicUrl(),
+            $this->getCompatibilityImageResourceValues($processedImage)
+        );
     }
 
     /**
@@ -195,16 +200,17 @@ class ImageService implements \TYPO3\CMS\Core\SingletonInterface
      */
     protected function getCompatibilityImageResourceValues(ProcessedFile $processedImage): array
     {
+        $originalFile = $processedImage->getOriginalFile();
         return [
             0 => $processedImage->getProperty('width'),
             1 => $processedImage->getProperty('height'),
             2 => $processedImage->getExtension(),
             3 => $processedImage->getPublicUrl(),
-            'origFile' => $processedImage->getOriginalFile()->getPublicUrl(),
-            'origFile_mtime' => $processedImage->getOriginalFile()->getModificationTime(),
+            'origFile' => $originalFile->getPublicUrl(),
+            'origFile_mtime' => $originalFile->getModificationTime(),
             // This is needed by \TYPO3\CMS\Frontend\Imaging\GifBuilder,
             // in order for the setup-array to create a unique filename hash.
-            'originalFile' => $processedImage->getOriginalFile(),
+            'originalFile' => $originalFile,
             'processedFile' => $processedImage
         ];
     }
index 3f0b953..fab5621 100644 (file)
@@ -62,8 +62,11 @@ class ImageScriptServiceTest extends UnitTestCase
     {
         $reference = $this->getMockBuilder(FileReference::class)->disableOriginalConstructor()->getMock();
         $file = $this->createMock(File::class);
-        $file->expects(self::once())->method('process')->willReturn($this->createMock(ProcessedFile::class));
+        $processedFile = $this->createMock(ProcessedFile::class);
+        $file->expects(self::once())->method('process')->willReturn($processedFile);
         $reference->expects(self::once())->method('getOriginalFile')->willReturn($file);
+        $processedFile->expects(self::once())->method('getOriginalFile')->willReturn($file);
+        $processedFile->expects(self::atLeastOnce())->method('getPublicUrl')->willReturn('https://example.com/foo.png');
 
         $this->subject->applyProcessingInstructions($reference, []);
     }
diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/CssViewHelper.php
new file mode 100644 (file)
index 0000000..7e273f6
--- /dev/null
@@ -0,0 +1,101 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Fluid\ViewHelpers\Asset;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Page\AssetCollector;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
+
+/**
+ * CssViewHelper
+ *
+ * Examples
+ * ========
+ *
+ * ::
+ *
+ *    <f:asset.css identifier="identifier123" href="EXT:my_ext/Resources/Public/Css/foo.css" />
+ *    <f:asset.css identifier="identifier123">
+ *       .foo { color: black; }
+ *    </f:asset.css>
+ */
+class CssViewHelper extends AbstractTagBasedViewHelper
+{
+
+    /**
+     * @var AssetCollector
+     */
+    protected $assetCollector;
+
+    /**
+     * @param AssetCollector $assetCollector
+     */
+    public function injectAssetCollector(AssetCollector $assetCollector): void
+    {
+        $this->assetCollector = $assetCollector;
+    }
+
+    /**
+     * @api
+     */
+    public function initializeArguments(): void
+    {
+        parent::initializeArguments();
+        parent::registerUniversalTagAttributes();
+        $this->registerTagAttribute('as', 'string', '', false);
+        $this->registerTagAttribute('crossorigin', 'string', '', false);
+        $this->registerTagAttribute('disabled', 'string', '', false);
+        $this->registerTagAttribute('href', 'string', '', false);
+        $this->registerTagAttribute('hreflang', 'string', '', false);
+        $this->registerTagAttribute('importance', 'string', '', false);
+        $this->registerTagAttribute('integrity', 'string', '', false);
+        $this->registerTagAttribute('media', 'string', '', false);
+        $this->registerTagAttribute('referrerpolicy', 'string', '', false);
+        $this->registerTagAttribute('rel', 'string', '', false);
+        $this->registerTagAttribute('sizes', 'string', '', false);
+        $this->registerTagAttribute('type', 'string', '', false);
+        $this->registerTagAttribute('nonce', 'string', '', 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 put in the <head> tag above-the-fold or somewhere in the body part.',
+            false,
+            false
+        );
+    }
+
+    public function render(): string
+    {
+        $identifier = $this->arguments['identifier'];
+        $attributes = $this->tag->getAttributes();
+        $file = $this->tag->getAttribute('href');
+        unset($attributes['href']);
+        $options = [
+            'priority' => $this->arguments['priority']
+        ];
+        if ($file !== null) {
+            $this->assetCollector->addStyleSheet($identifier, $file, $attributes, $options);
+        } else {
+            $this->assetCollector->addInlineStyleSheet($identifier, $this->renderChildren(), $attributes, $options);
+        }
+        return '';
+    }
+}
diff --git a/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php b/typo3/sysext/fluid/Classes/ViewHelpers/Asset/ScriptViewHelper.php
new file mode 100644 (file)
index 0000000..78b849f
--- /dev/null
@@ -0,0 +1,97 @@
+<?php
+declare(strict_types = 1);
+namespace TYPO3\CMS\Fluid\ViewHelpers\Asset;
+
+/*
+ * This file is part of the TYPO3 CMS project.
+ *
+ * It is free software; you can redistribute it and/or modify it under
+ * the terms of the GNU General Public License, either version 2
+ * of the License, or any later version.
+ *
+ * For the full copyright and license information, please read the
+ * LICENSE.txt file that was distributed with this source code.
+ *
+ * The TYPO3 project - inspiring people to share!
+ */
+
+use TYPO3\CMS\Core\Page\AssetCollector;
+use TYPO3Fluid\Fluid\Core\ViewHelper\AbstractTagBasedViewHelper;
+
+/**
+ * ScriptViewHelper
+ *
+ * Examples
+ * ========
+ *
+ * ::
+ *
+ *    <f:asset.script identifier="identifier123" src="EXT:my_ext/Resources/Public/JavaScript/foo.js" />
+ *    <f:asset.script identifier="identifier123">
+ *       alert('hello world');
+ *    </f:asset.script>
+ */
+class ScriptViewHelper extends AbstractTagBasedViewHelper
+{
+
+    /**
+     * @var AssetCollector
+     */
+    protected $assetCollector;
+
+    /**
+     * @param AssetCollector $assetCollector
+     */
+    public function injectAssetCollector(AssetCollector $assetCollector): void
+    {
+        $this->assetCollector = $assetCollector;
+    }
+
+    /**
+     * @api
+     */
+    public function initializeArguments(): void
+    {
+        parent::initializeArguments();
+        parent::registerUniversalTagAttributes();
+        $this->registerTagAttribute('async', 'string', '', false);
+        $this->registerTagAttribute('crossorigin', 'string', '', false);
+        $this->registerTagAttribute('defer', 'string', '', false);
+        $this->registerTagAttribute('integrity', 'string', '', false);
+        $this->registerTagAttribute('nomodule', 'string', '', false);
+        $this->registerTagAttribute('nonce', 'string', '', false);
+        $this->registerTagAttribute('referrerpolicy', 'string', '', false);
+        $this->registerTagAttribute('src', 'string', '', false);
+        $this->registerTagAttribute('type', 'string', '', 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
+        );
+    }
+
+    public function render(): string
+    {
+        $identifier = $this->arguments['identifier'];
+        $attributes = $this->tag->getAttributes();
+        $src = $this->tag->getAttribute('src');
+        unset($attributes['src']);
+        $options = [
+            'priority' => $this->arguments['priority']
+        ];
+        if ($src !== null) {
+            $this->assetCollector->addJavaScript($identifier, $src, $attributes, $options);
+        } else {
+            $this->assetCollector->addInlineJavaScript($identifier, $this->renderChildren(), $attributes, $options);
+        }
+        return '';
+    }
+}
index d9b9e64..41d185b 100644 (file)
@@ -39,6 +39,7 @@ use TYPO3\CMS\Core\Imaging\ImageManipulation\CropVariantCollection;
 use TYPO3\CMS\Core\LinkHandling\LinkService;
 use TYPO3\CMS\Core\Log\LogManager;
 use TYPO3\CMS\Core\Mail\MailMessage;
+use TYPO3\CMS\Core\Page\AssetCollector;
 use TYPO3\CMS\Core\Resource\Exception;
 use TYPO3\CMS\Core\Resource\Exception\ResourceDoesNotExistException;
 use TYPO3\CMS\Core\Resource\File;
@@ -1055,6 +1056,10 @@ class ContentObjectRenderer implements LoggerAwareInterface
         } else {
             $source = $info[3];
         }
+        GeneralUtility::makeInstance(AssetCollector::class)->addMedia(
+            $source,
+            $info ?? []
+        );
 
         $layoutKey = $this->stdWrap($conf['layoutKey'], $conf['layoutKey.']);
         $imageTagTemplate = $this->getImageTagTemplate($layoutKey, $conf);
index 061523b..a88c20f 100644 (file)
@@ -22,6 +22,7 @@ use TYPO3\CMS\Backend\FrontendBackendUserAuthentication;
 use TYPO3\CMS\Core\Cache\CacheManager;
 use TYPO3\CMS\Core\Charset\CharsetConverter;
 use TYPO3\CMS\Core\Charset\UnknownCharsetException;
+use TYPO3\CMS\Core\Compatibility\PublicPropertyDeprecationTrait;
 use TYPO3\CMS\Core\Configuration\Loader\PageTsConfigLoader;
 use TYPO3\CMS\Core\Configuration\Parser\PageTsConfigParser;
 use TYPO3\CMS\Core\Context\Context;
@@ -50,6 +51,7 @@ use TYPO3\CMS\Core\Localization\Locales;
 use TYPO3\CMS\Core\Locking\Exception\LockAcquireWouldBlockException;
 use TYPO3\CMS\Core\Locking\LockFactory;
 use TYPO3\CMS\Core\Locking\LockingStrategyInterface;
+use TYPO3\CMS\Core\Page\AssetCollector;
 use TYPO3\CMS\Core\Page\PageRenderer;
 use TYPO3\CMS\Core\PageTitle\PageTitleProviderManager;
 use TYPO3\CMS\Core\Resource\StorageRepository;
@@ -93,6 +95,12 @@ use TYPO3\CMS\Frontend\Resource\FilePathSanitizer;
 class TypoScriptFrontendController implements LoggerAwareInterface
 {
     use LoggerAwareTrait;
+    use PublicPropertyDeprecationTrait;
+
+    private $deprecatedPublicProperties = [
+        'imagesOnPage' => 'Using TSFE->imagesOnPage is deprecated and will no longer work with TYPO3 v11.0. Use AssetCollector()->getMedia() instead.',
+        'lastImageInfo' => 'Using TSFE->lastImageInfo is deprecated and will no longer work with TYPO3 v11.0.'
+    ];
 
     /**
      * The page id (int)
@@ -540,15 +548,17 @@ class TypoScriptFrontendController implements LoggerAwareInterface
      * Numerical array where image filenames are added if they are referenced in the
      * rendered document. This includes only TYPO3 generated/inserted images.
      * @var array
+     * @deprecated
      */
-    public $imagesOnPage = [];
+    private $imagesOnPage = [];
 
     /**
      * Is set in ContentObjectRenderer->cImage() function to the info-array of the
      * most recent rendered image. The information is used in ImageTextContentObject
      * @var array
+     * @deprecated
      */
-    public $lastImageInfo = [];
+    private $lastImageInfo = [];
 
     /**
      * Used to generate page-unique keys. Point is that uniqid() functions is very
@@ -2980,6 +2990,11 @@ class TypoScriptFrontendController implements LoggerAwareInterface
             $this->pageRenderer = $pageRenderer;
             GeneralUtility::setSingletonInstance(PageRenderer::class, $pageRenderer);
         }
+        if (!empty($this->config['INTincScript_ext']['assetCollector'])) {
+            /** @var AssetCollector $assetCollectorr */
+            $assetCollector = unserialize($this->config['INTincScript_ext']['assetCollector'], ['allowed_classes' => false]);
+            GeneralUtility::setSingletonInstance(AssetCollector::class, $assetCollector);
+        }
 
         $this->recursivelyReplaceIntPlaceholdersInContent();
         $this->getTimeTracker()->push('Substitute header section');