Skip to content

Commit

Permalink
[FEATURE] Introduce HANDLEBARSTEMPLATE content object
Browse files Browse the repository at this point in the history
  • Loading branch information
eliashaeussler committed Jan 16, 2025
1 parent d3e5f99 commit dee0b2b
Show file tree
Hide file tree
Showing 8 changed files with 799 additions and 26 deletions.
185 changes: 185 additions & 0 deletions Classes/Frontend/ContentObject/HandlebarsTemplateContentObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

declare(strict_types=1);

/*
* This file is part of the TYPO3 CMS extension "handlebars".
*
* Copyright (C) 2025 Elias Häußler <e.haeussler@familie-redlich.de>
*
* 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 <https://www.gnu.org/licenses/>.
*/

namespace Fr\Typo3Handlebars\Frontend\ContentObject;

use Fr\Typo3Handlebars\Renderer;
use TYPO3\CMS\Core;
use TYPO3\CMS\Frontend;

/**
* HandlebarsTemplateContentObject
*
* @author Elias Häußler <e.haeussler@familie-redlich.de>
* @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<string, mixed> $conf
*/
public function render($conf = []): string
{
if (!is_array($conf)) {
$conf = [];
}

// Create handlebars view
$view = $this->createView($conf);

// Resolve template paths
/** @var array<string, mixed> $templatePaths */
$templatePaths = $this->typoScriptService->convertTypoScriptArrayToPlainArray(
array_intersect_key(
$conf,
[
'partialRootPath' => true,
'partialRootPaths.' => true,
'templateRootPath' => true,
'templateRootPaths.' => true,
],
),
);

// Populate template paths for availability in subsequent renderings
$this->pathProvider->push($templatePaths);

$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<string, mixed> $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<string, mixed> $config
* @return array<string, mixed>
*/
private function resolveVariables(array $config): array
{
$variables = $this->getContentObjectVariables($config);

// Resolve variables from simple hierarchy (without content objects)
$simpleVariables = \array_diff_key(
$this->typoScriptService->convertTypoScriptArrayToPlainArray($config['variables.'] ?? []),
$variables,
);

// Merge variables
if ($simpleVariables !== []) {
Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($variables, $simpleVariables);
}

// Process variables with configured data processors
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<string, mixed> $config
*/
private function renderPageAssetsIntoPageRenderer(array $config, Renderer\Template\View\HandlebarsView $baseView): void
{
$headerAssets = $this->renderAssets($config['headerAssets.'] ?? [], $baseView);
$footerAssets = $this->renderAssets($config['footerAssets.'] ?? [], $baseView);

if (\trim($headerAssets) !== '') {
$this->getPageRenderer()->addHeaderData($headerAssets);
}

if (\trim($footerAssets) !== '') {
$this->getPageRenderer()->addFooterData($footerAssets);
}
}

/**
* @param array<string, mixed> $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);
}
}
10 changes: 0 additions & 10 deletions Classes/Renderer/Template/BaseTemplateResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,6 @@ abstract class BaseTemplateResolver implements TemplateResolver
{
protected const DEFAULT_FILE_EXTENSIONS = ['hbs', 'handlebars', 'html'];

/**
* @var list<string>
*/
protected array $partialRootPaths = [];

/**
* @var list<string>
*/
protected array $templateRootPaths = [];

/**
* @var list<string>
*/
Expand Down
49 changes: 33 additions & 16 deletions Classes/Renderer/Template/FlatTemplateResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,40 +41,48 @@ final class FlatTemplateResolver extends BaseTemplateResolver
private readonly HandlebarsTemplateResolver $fallbackResolver;

/**
* @var array<string, Finder\SplFileInfo>
* @var array<string, array<string, Finder\SplFileInfo>>
*/
private readonly array $flattenedPartials;

/**
* @var array<string, Finder\SplFileInfo>
*/
private readonly array $flattenedTemplates;
private array $flattenedRootPaths = [];

/**
* @param list<string> $supportedFileExtensions
* @throws Exception\RootPathIsMalicious
* @throws Exception\RootPathIsNotResolvable
*/
public function __construct(
TemplatePaths $templatePaths,
private readonly TemplatePaths $templatePaths,
array $supportedFileExtensions = self::DEFAULT_FILE_EXTENSIONS,
) {
$this->fallbackResolver = new HandlebarsTemplateResolver($templatePaths, $supportedFileExtensions);
[$this->templateRootPaths, $this->partialRootPaths] = $this->resolveTemplatePaths($templatePaths);
$this->supportedFileExtensions = $this->resolveSupportedFileExtensions($supportedFileExtensions);
$this->flattenedPartials = $this->buildPathMap($this->partialRootPaths);
$this->flattenedTemplates = $this->buildPathMap($this->templateRootPaths);
}

/**
* @throws Exception\PartialPathIsNotResolvable
* @throws Exception\RootPathIsMalicious
* @throws Exception\RootPathIsNotResolvable
* @throws Exception\TemplateFormatIsNotSupported
*/
public function resolvePartialPath(string $partialPath, ?string $format = null): string
{
return $this->resolvePath($partialPath, $this->flattenedPartials, $format)
$flattenedPartials = $this->buildPathMap($this->templatePaths->getPartialRootPaths());

return $this->resolvePath($partialPath, $flattenedPartials, $format)
?? $this->fallbackResolver->resolvePartialPath($partialPath, $format);
}

/**
* @throws Exception\RootPathIsMalicious
* @throws Exception\RootPathIsNotResolvable
* @throws Exception\TemplateFormatIsNotSupported
* @throws Exception\TemplatePathIsNotResolvable
*/
public function resolveTemplatePath(string $templatePath, ?string $format = null): string
{
return $this->resolvePath($templatePath, $this->flattenedTemplates, $format)
$flattenedTemplates = $this->buildPathMap($this->templatePaths->getTemplateRootPaths());

return $this->resolvePath($templatePath, $flattenedTemplates, $format)
?? $this->fallbackResolver->resolveTemplatePath($templatePath, $format);
}

Expand Down Expand Up @@ -120,11 +128,20 @@ private function resolvePath(string $path, array $flattenedFiles, ?string $forma
}

/**
* @param list<string> $rootPaths
* @param array<int, string> $rootPaths
* @return array<string, Finder\SplFileInfo>
* @throws Exception\RootPathIsMalicious
* @throws Exception\RootPathIsNotResolvable
*/
private function buildPathMap(array $rootPaths): array
{
$hash = \sha1((string)\json_encode($rootPaths));

if (isset($this->flattenedRootPaths[$hash])) {
return $this->flattenedRootPaths[$hash];
}

$normalizedRootPaths = $this->normalizeRootPaths($rootPaths);
$flattenedPaths = [];

// Instantiate finder
Expand All @@ -140,7 +157,7 @@ private function buildPathMap(array $rootPaths): array
$finder->sortByName();

// Build template map
foreach (array_reverse($rootPaths) as $rootPath) {
foreach (array_reverse($normalizedRootPaths) as $rootPath) {
$path = $this->resolveFilename($rootPath);
$pathFinder = clone $finder;
$pathFinder->in($path);
Expand All @@ -156,7 +173,7 @@ private function buildPathMap(array $rootPaths): array
}
}

return $flattenedPaths;
return $this->flattenedRootPaths[$hash] = $flattenedPaths;
}

/**
Expand Down
10 changes: 10 additions & 0 deletions Classes/Renderer/Template/HandlebarsTemplateResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,16 @@
*/
final class HandlebarsTemplateResolver extends BaseTemplateResolver
{
/**
* @var list<string>
*/
protected array $partialRootPaths = [];

/**
* @var list<string>
*/
protected array $templateRootPaths = [];

/**
* @param string[] $supportedFileExtensions
* @throws Exception\RootPathIsMalicious
Expand Down
Loading

0 comments on commit dee0b2b

Please sign in to comment.