diff --git a/Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.php b/Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.php new file mode 100644 index 00000000..13c4a97b --- /dev/null +++ b/Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.php @@ -0,0 +1,169 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace Fr\Typo3Handlebars\Frontend\ContentObject; + +use Fr\Typo3Handlebars\Renderer; +use TYPO3\CMS\Core; +use TYPO3\CMS\Frontend; + +/** + * HandlebarsTemplateContentObject + * + * @author Elias Häußler + * @license GPL-2.0-or-later + */ +final class HandlebarsTemplateContentObject extends Frontend\ContentObject\FluidTemplateContentObject +{ + public function __construct( + Frontend\ContentObject\ContentDataProcessor $contentDataProcessor, + private readonly Renderer\Renderer $renderer, + private readonly Renderer\Template\Path\ContentObjectPathProvider $pathProvider, + private readonly Core\TypoScript\TypoScriptService $typoScriptService, + ) { + parent::__construct($contentDataProcessor); + } + + /** + * @param array $conf + */ + public function render($conf = []): string + { + if (!is_array($conf)) { + $conf = []; + } + + // Create handlebars view + $view = $this->createView($conf); + + // Populate template paths + $this->pathProvider->push( + array_intersect_key( + $this->typoScriptService->convertTypoScriptArrayToPlainArray($conf), + [ + 'partialRootPath' => true, + 'partialRootPaths' => true, + 'templateRootPath' => true, + 'templateRootPaths' => true, + ], + ), + ); + + $view->assignMultiple($this->resolveVariables($conf)); + + $this->renderPageAssetsIntoPageRenderer($conf, $view); + + try { + $content = $this->renderer->render($view); + } finally { + // Remove current content object rendering from path provider stack + $this->pathProvider->pop(); + } + + return $this->applyStandardWrapToRenderedContent($content, $conf); + } + + /** + * @param array $config + */ + private function createView(array $config): Renderer\Template\View\HandlebarsView + { + $format = $this->cObj?->stdWrapValue('format', $config, null); + $view = new Renderer\Template\View\HandlebarsView(); + + if (is_string($format)) { + $view->setFormat($format); + } + + if (isset($config['templateName']) || isset($config['templateName.'])) { + return $view->setTemplatePath( + (string)$this->cObj?->stdWrapValue('templateName', $config), + ); + } + + if (isset($config['template']) || isset($config['template.'])) { + return $view->setTemplateSource( + (string)$this->cObj?->stdWrapValue('template', $config), + ); + } + + if (isset($config['file']) || isset($config['file.'])) { + return $view->setTemplatePath( + (string)$this->cObj?->stdWrapValue('file', $config), + ); + } + + return $view; + } + + /** + * @param array $config + * @return array + */ + private function resolveVariables(array $config): array + { + $variables = $this->getContentObjectVariables($config); + + if ($this->cObj !== null) { + $variables = $this->contentDataProcessor->process($this->cObj, $config, $variables); + } + + if (isset($config['settings.'])) { + $variables['settings'] = $this->typoScriptService->convertTypoScriptArrayToPlainArray($config['settings.']); + } + + return $variables; + } + + /** + * @param array $config + */ + private function renderPageAssetsIntoPageRenderer(array $config, Renderer\Template\View\HandlebarsView $view): void + { + $headerAssets = $this->renderAssets($config['headerAssets.'] ?? [], $view); + $footerAssets = $this->renderAssets($config['footerAssets.'] ?? [], $view); + + if (\trim($headerAssets) !== '') { + $this->getPageRenderer()->addHeaderData($headerAssets); + } + + if (\trim($footerAssets) !== '') { + $this->getPageRenderer()->addFooterData($footerAssets); + } + } + + /** + * @param array $config + */ + private function renderAssets(array $config, Renderer\Template\View\HandlebarsView $baseView): string + { + if ($config === []) { + return ''; + } + + $view = $this->createView($config); + $view->assignMultiple($baseView->getVariables()); + + return $this->renderer->render($view); + } +} diff --git a/Classes/Renderer/Template/Path/ContentObjectPathProvider.php b/Classes/Renderer/Template/Path/ContentObjectPathProvider.php new file mode 100644 index 00000000..daba921c --- /dev/null +++ b/Classes/Renderer/Template/Path/ContentObjectPathProvider.php @@ -0,0 +1,157 @@ + + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +namespace Fr\Typo3Handlebars\Renderer\Template\Path; + +use TYPO3\CMS\Core; + +/** + * ContentObjectPathProvider + * + * @author Elias Häußler + * @license GPL-2.0-or-later + * + * @phpstan-type RootPathStackItem array{ + * current: array, + * merged: array|null, + * } + */ +final class ContentObjectPathProvider implements PathProvider, Core\SingletonInterface +{ + /** + * @var list + */ + private array $stack = []; + + private ?int $currentItem = null; + + /** + * @param array $configuration + */ + public function push(array $configuration): void + { + $partialRootPaths = []; + $templateRootPaths = []; + + if (is_array($configuration['templateRootPaths'] ?? null)) { + $templateRootPaths = $configuration['templateRootPaths']; + } + if (is_array($configuration['partialRootPaths'] ?? null)) { + $partialRootPaths = $configuration['partialRootPaths']; + } + + // A single root path receives highest priority + if (is_string($configuration['templateRootPath'] ?? null)) { + $templateRootPaths[PHP_INT_MAX] = $configuration['templateRootPath']; + } + if (is_string($configuration['partialRootPath'] ?? null)) { + $partialRootPaths[PHP_INT_MAX] = $configuration['partialRootPath']; + } + + ksort($templateRootPaths); + ksort($partialRootPaths); + + $this->stack[] = [ + 'partialRootPaths' => [ + 'current' => $partialRootPaths, + 'merged' => null, + ], + 'templateRootPaths' => [ + 'current' => $templateRootPaths, + 'merged' => null, + ], + ]; + + if ($this->currentItem === null) { + $this->currentItem = 0; + } else { + $this->currentItem++; + } + } + + public function pop(): void + { + if ($this->stack === []) { + return; + } + + array_pop($this->stack); + + if ($this->stack === []) { + $this->currentItem = null; + } else { + $this->currentItem--; + } + } + + public function getPartialRootPaths(): array + { + return $this->getMergedRootPathsFromStack('partialRootPaths'); + } + + public function getTemplateRootPaths(): array + { + return $this->getMergedRootPathsFromStack('templateRootPaths'); + } + + public function isCacheable(): bool + { + // Caching is done internally, based on the current stack + return false; + } + + /** + * @param 'partialRootPaths'|'templateRootPaths' $type + * @return array + */ + private function getMergedRootPathsFromStack(string $type): array + { + if ($this->currentItem === null) { + return []; + } + + // Merge and cache root paths + if ($this->stack[$this->currentItem][$type]['merged'] === null) { + /* @phpstan-ignore offsetAccess.notFound */ + $this->stack[$this->currentItem][$type]['merged'] = $this->stack[$this->currentItem][0]['current']; + + for ($i = 1; $i < $this->currentItem; $i++) { + Core\Utility\ArrayUtility::mergeRecursiveWithOverrule( + $this->stack[$this->currentItem][$type]['merged'], + $this->stack[$i][$type]['current'], + ); + } + } + + /* @phpstan-ignore return.type */ + return $this->stack[$this->currentItem][$type]['merged']; + } + + public static function getPriority(): int + { + return 100; + } +} diff --git a/Configuration/Services.yaml b/Configuration/Services.yaml index 45bfb265..c3bd3dbc 100644 --- a/Configuration/Services.yaml +++ b/Configuration/Services.yaml @@ -16,6 +16,12 @@ services: Fr\Typo3Handlebars\Renderer\Template\TemplateResolver: alias: 'handlebars.template_resolver' + # Content object + Fr\Typo3Handlebars\Frontend\ContentObject\HandlebarsTemplateContentObject: + tags: + - name: frontend.contentobject + identifier: 'HANDLEBARSTEMPLATE' + # Template handlebars.template_resolver: class: 'Fr\Typo3Handlebars\Renderer\Template\HandlebarsTemplateResolver'