diff --git a/CHANGELOG.md b/CHANGELOG.md index 1067d1b..26724c5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ CHANGELOG =================== +## v2.13.0 - (2026-09-23) +### Added +- `smart_sonata.documentation` config + - `process_twig` option to disable Twig processing in documentation Markdown files (default: true) + - `markdown_template` option to override the template used to render Markdown documentation (default: `@SmartSonata/admin/documentation/markdown.html.twig`) +- Add mermaid to documentation template + +### Changed +- `DocumentationController` extract `renderEmailView` and `renderDocumentationView` methods + make properties `protected` instead of `private` to ease overriding + ## v2.12.3 - (2026-09-18) ### Fixed - `templates/standard_layout.html.twig` Add missing `integrity`/`crossorigin` attributes on the Iconify CDN script to fix CWE-345 / CWE-693 diff --git a/src/Controller/Admin/DocumentationController.php b/src/Controller/Admin/DocumentationController.php index c13d5e6..db527f1 100644 --- a/src/Controller/Admin/DocumentationController.php +++ b/src/Controller/Admin/DocumentationController.php @@ -5,6 +5,7 @@ use Smart\CoreBundle\Utils\MarkdownUtils; use Smart\SonataBundle\Mailer\BaseMailer; use Smart\SonataBundle\Mailer\EmailProvider; +use Smart\SonataBundle\Mailer\TemplatedEmail; use Smart\SonataBundle\Route\RouteLoader; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Finder\Finder; @@ -19,8 +20,13 @@ class DocumentationController extends AbstractController { - private string $projectDir; - private Environment $twig; + public const DEFAULT_PROCESS_TWIG = true; + public const DEFAULT_MARKDOWN_TEMPLATE = '@SmartSonata/admin/documentation/markdown.html.twig'; + + protected string $projectDir; + protected Environment $twig; + protected bool $processTwig = self::DEFAULT_PROCESS_TWIG; + protected string $markdownTemplate = self::DEFAULT_MARKDOWN_TEMPLATE; public function email( Request $request, @@ -53,8 +59,16 @@ public function email( } } - return new Response($this->twig->render("@SmartSonata/admin/documentation/email.html.twig", [ - 'grouped_smart_emails' => $provider->getGroupedEmails(), + return $this->renderEmailView($provider->getGroupedEmails()); + } + + /** + * @param array> $groupedSmartEmails + */ + protected function renderEmailView(array $groupedSmartEmails): Response + { + return new Response($this->twig->render('@SmartSonata/admin/documentation/email.html.twig', [ + 'grouped_smart_emails' => $groupedSmartEmails, ])); } @@ -67,7 +81,6 @@ public function renderMarkdown(Request $request): Response $markdownContent = null; $markdownNav = []; - $routeNamePrefix = RouteLoader::SMART_DOCUMENTATION_ROUTE_PREFIX; $directoryFinder = new Finder(); foreach ($directoryFinder->directories()->in($this->projectDir . '/documentation')->sortByName(true) as $directory) { $directoryName = $directory->getFilename(); @@ -78,28 +91,74 @@ public function renderMarkdown(Request $request): Response $directoryPath = $directoryName; } - $mdFinder = new Finder(); - $mdFinder->files()->in($this->projectDir . '/documentation/' . $directoryName)->name('*.md')->sortByName(true); - foreach ($mdFinder as $file) { - $filename = $file->getFilename(); - $separator = strpos($filename, '-'); - if ($separator !== false) { - $filename = substr($filename, $separator + 1); - } - $filename = u($filename)->replace('.md', '')->snake()->toString(); - $snakeDirectoryName = u($directoryPath)->snake()->toString(); - $markdownNav[$snakeDirectoryName][$filename] = $routeNamePrefix . $snakeDirectoryName . '_' . $filename; - - if (str_ends_with($directoryName, $directoryParam) && $filename === $filenameParam) { - $markdownContent = $this->transformMarkdown( - $file->getContents(), - $request->getSchemeAndHttpHost() . $request->getRequestUri() - ); + $content = $this->processDirectoryFiles( + $directoryName, + $directoryPath, + $directoryParam, + $filenameParam, + $request, + $markdownNav + ); + + if ($content !== null) { + $markdownContent = $content; + } + } + + return $this->renderDocumentationView($markdownContent, $markdownNav); + } + + /** + * @param array> $markdownNav + */ + private function processDirectoryFiles( + string $directoryName, + string $directoryPath, + string $directoryParam, + string $filenameParam, + Request $request, + array &$markdownNav + ): ?string { + $markdownContent = null; + $routeNamePrefix = RouteLoader::SMART_DOCUMENTATION_ROUTE_PREFIX; + + $mdFinder = new Finder(); + $mdFinder->files()->in($this->projectDir . '/documentation/' . $directoryName)->name('*.md')->sortByName(true); + + foreach ($mdFinder as $file) { + $filename = $file->getFilename(); + $separator = strpos($filename, '-'); + if ($separator !== false) { + $filename = substr($filename, $separator + 1); + } + + $filename = u($filename)->replace('.md', '')->snake()->toString(); + $snakeDirectoryName = u($directoryPath)->snake()->toString(); + $markdownNav[$snakeDirectoryName][$filename] = $routeNamePrefix . $snakeDirectoryName . '_' . $filename; + + if (str_ends_with($directoryName, $directoryParam) && $filename === $filenameParam) { + $rawContent = $file->getContents(); + + if ($this->processTwig) { + $rawContent = $this->twig->createTemplate($rawContent)->render(); } + + $markdownContent = $this->transformMarkdown( + $rawContent, + $request->getSchemeAndHttpHost() . $request->getRequestUri() + ); } } - return new Response($this->twig->render('@SmartSonata/admin/documentation/markdown.html.twig', [ + return $markdownContent; + } + + /** + * @param array> $markdownNav + */ + protected function renderDocumentationView(?string $markdownContent, array $markdownNav): Response + { + return new Response($this->twig->render($this->markdownTemplate, [ 'markdown_content' => $markdownContent, 'markdown_nav' => $markdownNav, ])); @@ -115,6 +174,16 @@ public function setTwig(Environment $twig): void $this->twig = $twig; } + public function setProcessTwig(bool $processTwig): void + { + $this->processTwig = $processTwig; + } + + public function setMarkdownTemplate(string $markdownTemplate): void + { + $this->markdownTemplate = $markdownTemplate; + } + private function transformMarkdown(string $content, string $baseUrl): string { return MarkdownUtils::addAnchorToHeadings($content, $baseUrl); diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index a16dcc3..436e157 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -2,6 +2,7 @@ namespace Smart\SonataBundle\DependencyInjection; +use Smart\SonataBundle\Controller\Admin\DocumentationController; use Symfony\Component\Config\Definition\Builder\ArrayNodeDefinition; use Symfony\Component\Config\Definition\Builder\TreeBuilder; use Symfony\Component\Config\Definition\ConfigurationInterface; @@ -31,6 +32,19 @@ public function getConfigTreeBuilder(): TreeBuilder ->scalarPrototype()->end() ->end() ->append($this->getParametersNode()) + ->arrayNode('documentation') + ->addDefaultsIfNotSet() + ->children() + ->booleanNode('process_twig') + ->defaultValue(DocumentationController::DEFAULT_PROCESS_TWIG) + ->info('Enable Twig processing in documentation Markdown files.') + ->end() + ->scalarNode('markdown_template') + ->defaultValue(DocumentationController::DEFAULT_MARKDOWN_TEMPLATE) + ->info('Template used to render Markdown documentation.') + ->end() + ->end() + ->end() ->end() ; diff --git a/src/DependencyInjection/SmartSonataExtension.php b/src/DependencyInjection/SmartSonataExtension.php index 118cbcd..b7b557b 100644 --- a/src/DependencyInjection/SmartSonataExtension.php +++ b/src/DependencyInjection/SmartSonataExtension.php @@ -2,6 +2,7 @@ namespace Smart\SonataBundle\DependencyInjection; +use Smart\SonataBundle\Controller\Admin\DocumentationController; use Smart\SonataBundle\Mailer\BaseMailer; use Smart\SonataBundle\Mailer\EmailProvider; use Symfony\Component\DependencyInjection\Extension\PrependExtensionInterface; @@ -43,6 +44,12 @@ public function load(array $configs, ContainerBuilder $container) $parameterLoader->addMethodCall('addParameter', [$code, $data]); } } + + /** @var array{process_twig: bool, markdown_template: string} $documentationConfig */ $documentationConfig = $config['documentation']; + $documentationController = $container->getDefinition(DocumentationController::class); + + $documentationController->addMethodCall('setProcessTwig', [$documentationConfig['process_twig']]); + $documentationController->addMethodCall('setMarkdownTemplate', [$documentationConfig['markdown_template']]); } /** diff --git a/src/Loader/ParameterLoader.php b/src/Loader/ParameterLoader.php index 8b7dabc..519694d 100644 --- a/src/Loader/ParameterLoader.php +++ b/src/Loader/ParameterLoader.php @@ -65,7 +65,7 @@ public function load(bool $dryRun = false): array $this->entityManager->getConnection()->commit(); } } catch (\Exception $e) { - if (false === $this->dryRun) { // @phpstan-ignore-line false positive, it can be true + if (false === $this->dryRun) { $this->entityManager->getConnection()->rollBack(); } } diff --git a/templates/admin/documentation/markdown.html.twig b/templates/admin/documentation/markdown.html.twig index 74df242..b1fedf0 100644 --- a/templates/admin/documentation/markdown.html.twig +++ b/templates/admin/documentation/markdown.html.twig @@ -74,4 +74,29 @@ document.getElementById('markdown-content').innerHTML = marked.parse(content) }); + + {% endblock %} diff --git a/tests/fixtures/config/full.yml b/tests/fixtures/config/full.yml index 5a69a68..c5d7838 100644 --- a/tests/fixtures/config/full.yml +++ b/tests/fixtures/config/full.yml @@ -14,3 +14,6 @@ smart_sonata: help: "This parameter is used by the backend to set the duration of cache applied to the Homepage. Set the duration in second." sender: address: 'test@test.test' + documentation: + process_twig: false + markdown_template: '@Custom/documentation.html.twig'