Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
115 changes: 92 additions & 23 deletions src/Controller/Admin/DocumentationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down Expand Up @@ -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<string, array<string, TemplatedEmail>> $groupedSmartEmails
*/
protected function renderEmailView(array $groupedSmartEmails): Response
{
return new Response($this->twig->render('@SmartSonata/admin/documentation/email.html.twig', [
'grouped_smart_emails' => $groupedSmartEmails,
]));
}

Expand All @@ -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();
Expand All @@ -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<string, array<string, string>> $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<string, array<string, string>> $markdownNav
*/
protected function renderDocumentationView(?string $markdownContent, array $markdownNav): Response
{
return new Response($this->twig->render($this->markdownTemplate, [
'markdown_content' => $markdownContent,
'markdown_nav' => $markdownNav,
]));
Expand All @@ -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);
Expand Down
14 changes: 14 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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()
;

Expand Down
7 changes: 7 additions & 0 deletions src/DependencyInjection/SmartSonataExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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']]);
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Loader/ParameterLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand Down
25 changes: 25 additions & 0 deletions templates/admin/documentation/markdown.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,29 @@
document.getElementById('markdown-content').innerHTML = marked.parse(content)
});
</script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@11.17.2/dist/mermaid.min.js" integrity="sha384-EOXBFmc3gx5mb+vn0vPvvGqACToJD24hhacX5Yx+8NUUQrHIle/Qi5Bg9o3zKwW2" crossorigin="anonymous"></script>
<script type="application/javascript">
document.addEventListener('DOMContentLoaded', function() {
mermaid.initialize({
startOnLoad: false,
theme: 'default',
securityLevel: 'loose',
});

// Search all code block with 'mermaid' class
const mermaidElements = document.querySelectorAll('pre code.language-mermaid');

mermaidElements.forEach(function(element, index) {
const mermaidDiv = document.createElement('div');
mermaidDiv.className = 'mermaid';
mermaidDiv.id = 'mermaid-' + index;
mermaidDiv.textContent = element.textContent;

// Replace code block by mermaid div
element.parentNode.parentNode.replaceChild(mermaidDiv, element.parentNode);
});

mermaid.init(undefined, '.mermaid');
});
</script>
{% endblock %}
3 changes: 3 additions & 0 deletions tests/fixtures/config/full.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Loading