diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e4507563..44f75294 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -20,7 +20,7 @@ jobs: uses: shivammathur/setup-php@v2 with: coverage: none - php-version: 8.3 + php-version: 8.4 tools: composer:v2 ini-values: date.timezone=UTC @@ -37,7 +37,7 @@ jobs: # add here only the PHP versions and OS used in GitHub CI (for tests) # and on the symfony.com server (where the Symfony Docs are built) operating-system: ['ubuntu-latest'] - php-version: ['8.3', '8.4'] + php-version: ['8.4', '8.5'] steps: - name: 'Checkout code' @@ -51,8 +51,8 @@ jobs: tools: composer:v2 ini-values: date.timezone=UTC - - name: Install Composer Dependencies - run: composer install --no-progress + - name: Install Composer Dependencies + run: composer install --no-progress - - name: PHPUnit - run: vendor/bin/simple-phpunit + - name: PHPUnit + run: composer test diff --git a/.gitignore b/.gitignore index 793daca2..2ca70321 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,11 @@ +!tools/*/composer.lock /vendor/ /tests/_output /tests/_cache /var/ /docs.phar /.env +/.php-cs-fixer.cache /.phpunit.result.cache +/.phpunit.cache /composer.lock diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 00000000..2e788d47 --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,34 @@ +setRules([ + '@PHP71Migration' => true, + '@PHPUnit75Migration:risky' => true, + '@Symfony' => true, + '@Symfony:risky' => true, + 'protected_to_private' => false, + 'native_constant_invocation' => ['strict' => false], + 'no_superfluous_phpdoc_tags' => [ + 'remove_inheritdoc' => true, + 'allow_unused_params' => true, // for future-ready params, to be replaced with https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues/7377 + ], + 'header_comment' => ['header' => $fileHeaderComment], + 'modernize_strpos' => true, + 'get_class_to_class_keyword' => true, + 'nullable_type_declaration' => true, + ]) + ->setRiskyAllowed(true) + ->setFinder( + (new PhpCsFixer\Finder()) + ->in(__DIR__.'/src') + ) +; diff --git a/LICENSE b/LICENSE index e5de20ca..333a5070 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,7 @@ The MIT license Copyright (c) 2018-present Ryan Weaver +Copyright (c) 2023-present Wouter de Jong Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/bin/docs-builder b/bin/docs-builder deleted file mode 100755 index be6975a7..00000000 --- a/bin/docs-builder +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env php -getParameterOption(['--symfony-version'], false === getenv('SYMFONY_VERSION') ? 'master' : getenv('SYMFONY_VERSION')); - -if (!$version) { - throw new \Exception('Please pass a --symfony-version= flag or set a SYMFONY_VERSION environment variable to 4.0, master, etc.'); -} - -$application = new Application($version); -$application->run($input); diff --git a/composer.json b/composer.json index aceb20a5..983de84f 100644 --- a/composer.json +++ b/composer.json @@ -5,33 +5,53 @@ "license": "MIT", "autoload": { "psr-4": { + "SymfonyTools\\DocsBuilder\\GuidesExtension\\": "src/GuidesExtension/src", "SymfonyDocsBuilder\\": "src" } }, "autoload-dev": { "psr-4": { + "SymfonyTools\\DocsBuilder\\Tests\\": "tests", "SymfonyDocsBuilder\\Tests\\": "tests" } }, "require": { - "php": ">=8.3", + "php": ">=8.4", "ext-json": "*", "ext-curl": "*", - "doctrine/rst-parser": "^0.5", - "scrivo/highlight.php": "^9.18.1", - "symfony/filesystem": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/finder": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/dom-crawler": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/css-selector": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/console": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/http-client": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "twig/twig": "^2.14 || ^3.3" + "phpdocumentor/guides": "^1.9", + "phpdocumentor/guides-cli": "^1.9", + "phpdocumentor/guides-code": "^1.7", + "phpdocumentor/guides-restructured-text": "^1.10@dev", + "scrivo/highlight.php": "^9.12.0", + "symfony/config": "^8.0", + "symfony/filesystem": "^8.0", + "symfony/finder": "^8.0", + "symfony/event-dispatcher": "^8.0", + "symfony/dependency-injection": "^8.0", + "symfony/dom-crawler": "^8.0", + "symfony/css-selector": "^8.0", + "symfony/console": "^8.0", + "symfony/http-client": "^8.0", + "symfony/string": "^8.0", + "twig/twig": "^3.3", + "twig/string-extra": "^3.6" }, "require-dev": { - "gajus/dindent": "^2.0", - "symfony/phpunit-bridge": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "symfony/process": "^5.2 || ^6.0 || ^7.0 || ^8.0", - "masterminds/html5": "^2.7" + "league/flysystem-memory": "^3.0", + "symfony/phpunit-bridge": "^8.0", + "symfony/process": "^8.0", + "symfony/var-dumper": "^8.0" }, - "bin": ["bin/docs-builder"] + "scripts": { + "test": "SYMFONY_PHPUNIT_VERSION=12 simple-phpunit", + "psalm": [ + "composer update --no-scripts --working-dir=tools/psalm", + "./tools/psalm/vendor/bin/psalm" + ], + "cs": [ + "composer update --no-scripts --working-dir=tools/php-cs-fixer", + "./tools/php-cs-fixer/vendor/bin/php-cs-fixer fix" + ] + } } diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 00000000..145c9f34 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,31 @@ + + + + + tests + + + + + + src + + + + trigger_deprecation + Doctrine\Deprecations\Deprecation::trigger + Doctrine\Deprecations\Deprecation::delegateTriggerToBackend + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 00000000..5cbbfe3e --- /dev/null +++ b/psalm.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + diff --git a/src/Application.php b/src/Application.php deleted file mode 100644 index 7a0932df..00000000 --- a/src/Application.php +++ /dev/null @@ -1,42 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder; - -use Symfony\Component\Console\Application as BaseApplication; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use SymfonyDocsBuilder\Command\BuildDocsCommand; - -class Application -{ - private $application; - private $buildConfig; - - public function __construct(string $symfonyVersion) - { - $this->application = new BaseApplication(); - $this->buildConfig = new BuildConfig(); - } - - public function run(InputInterface $input): int - { - $inputOption = new InputOption( - 'symfony-version', - null, - InputOption::VALUE_REQUIRED, - 'The symfony version of the doc to parse.', - false === getenv('SYMFONY_VERSION') ? 'master' : getenv('SYMFONY_VERSION') - ); - $this->application->getDefinition()->addOption($inputOption); - $this->application->add(new BuildDocsCommand($this->buildConfig)); - - return $this->application->run($input); - } -} diff --git a/src/Command/BuildDocsCommand.php b/src/Command/BuildDocsCommand.php deleted file mode 100644 index def179eb..00000000 --- a/src/Command/BuildDocsCommand.php +++ /dev/null @@ -1,216 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder\Command; - -use Doctrine\Common\EventManager; -use Doctrine\RST\Builder; -use Doctrine\RST\Configuration; -use Doctrine\RST\Meta\Metas; -use Symfony\Component\Console\Command\Command; -use Symfony\Component\Console\Input\InputArgument; -use Symfony\Component\Console\Input\InputInterface; -use Symfony\Component\Console\Input\InputOption; -use Symfony\Component\Console\Output\OutputInterface; -use Symfony\Component\Console\Style\SymfonyStyle; -use Symfony\Component\Filesystem\Filesystem; -use SymfonyDocsBuilder\BuildConfig; -use SymfonyDocsBuilder\CI\MissingFilesChecker; -use SymfonyDocsBuilder\ConfigFileParser; -use SymfonyDocsBuilder\Generator\HtmlForPdfGenerator; -use SymfonyDocsBuilder\Generator\JsonGenerator; -use SymfonyDocsBuilder\KernelFactory; -use SymfonyDocsBuilder\Listener\BuildProgressListener; - -class BuildDocsCommand extends Command -{ - protected static $defaultName = 'build:docs'; - - private $buildConfig; - private $missingFilesChecker; - /** @var SymfonyStyle */ - private $io; - - public function __construct(BuildConfig $buildConfig) - { - parent::__construct(self::$defaultName); - - $this->buildConfig = $buildConfig; - $this->missingFilesChecker = new MissingFilesChecker($buildConfig); - } - - protected function configure(): void - { - parent::configure(); - - $this - ->addArgument('source-dir', InputArgument::OPTIONAL, 'RST files Source directory', getcwd()) - ->addArgument('output-dir', InputArgument::OPTIONAL, 'HTML files output directory') - ->addOption( - 'parse-sub-path', - null, - InputOption::VALUE_OPTIONAL, - 'Parse only given sub directory and combine it into a single file (directory relative from source-dir)', - '' - ) - ->addOption( - 'output-json', - null, - InputOption::VALUE_NONE, - 'If provided, .fjson metadata files will be written' - ) - ->addOption( - 'disable-cache', - null, - InputOption::VALUE_NONE, - 'If provided, caching meta will be disabled' - ) - ->addOption( - 'save-errors', - null, - InputOption::VALUE_REQUIRED, - 'Path where any errors should be saved' - ) - ->addOption( - 'error-output-format', - null, - InputOption::VALUE_REQUIRED, - 'The output format for errors on std out', - Configuration::OUTPUT_FORMAT_CONSOLE - ) - ->addOption( - 'no-theme', - null, - InputOption::VALUE_NONE, - 'Use the default theme instead of the styled one' - ) - ->addOption( - 'fail-on-errors', - null, - InputOption::VALUE_NONE, - 'Return a non-zero code if there are errors/warnings' - ) - ; - } - - protected function initialize(InputInterface $input, OutputInterface $output): void - { - $this->io = new SymfonyStyle($input, $output); - - $sourceDir = $input->getArgument('source-dir'); - if (!file_exists($sourceDir)) { - throw new \InvalidArgumentException(sprintf('RST source directory "%s" does not exist', $sourceDir)); - } - $this->buildConfig->setContentDir($sourceDir); - - $filesystem = new Filesystem(); - $htmlOutputDir = $input->getArgument('output-dir') ?? rtrim(getcwd(), '/').'/html'; - if ($input->getOption('disable-cache') && $filesystem->exists($htmlOutputDir)) { - $filesystem->remove($htmlOutputDir); - } - $filesystem->mkdir($htmlOutputDir); - $this->buildConfig->setOutputDir($htmlOutputDir); - - $parseSubPath = $input->getOption('parse-sub-path'); - if ($parseSubPath && $input->getOption('output-json')) { - throw new \InvalidArgumentException('Cannot pass both --parse-sub-path and --output-json options.'); - } - if (!file_exists($sourceDir.'/'.$parseSubPath)) { - throw new \InvalidArgumentException(sprintf('Given "parse-sub-path" directory "%s" does not exist', $parseSubPath)); - } - $this->buildConfig->setSubdirectoryToBuild($parseSubPath); - - if ($input->getOption('disable-cache')) { - $this->buildConfig->disableBuildCache(); - } - - $this->buildConfig->setTheme($input->getOption('no-theme') ? Configuration::THEME_DEFAULT : 'rtd'); - - $configFileParser = new ConfigFileParser($this->buildConfig, $output); - $configFileParser->processConfigFile($sourceDir); - } - - protected function execute(InputInterface $input, OutputInterface $output): int - { - $builder = new Builder( - KernelFactory::createKernel($this->buildConfig, $this->urlChecker ?? null) - ); - - $configuration = $builder->getConfiguration(); - $configuration->setOutputFormat($input->getOption('error-output-format')); - $this->addProgressListener($configuration->getEventManager()); - - $builder->build( - $this->buildConfig->getContentDir(), - $this->buildConfig->getOutputDir() - ); - - $buildErrors = $builder->getErrorManager()->getErrors(); - - $missingFiles = $this->missingFilesChecker->getMissingFiles(); - foreach ($missingFiles as $missingFile) { - $message = sprintf('Missing file "%s"', $missingFile); - $buildErrors[] = $message; - $this->io->warning($message); - } - - if ($logPath = $input->getOption('save-errors')) { - if (\count($buildErrors) > 0) { - array_unshift($buildErrors, sprintf('Build errors from "%s"', date('Y-m-d h:i:s'))); - } - - $filesystem = new Filesystem(); - $filesystem->dumpFile($logPath, implode("\n", array_map(fn($error) => is_string($error) ? $error : $error->asString(), $buildErrors))); - } - - $metas = $builder->getMetas(); - if ($this->buildConfig->getSubdirectoryToBuild()) { - $this->renderDocForPDF($metas); - } elseif ($input->getOption('output-json')) { - $this->generateJson($metas); - } - - $this->io->newLine(2); - - if (\count($buildErrors) > 0) { - $this->io->success('Build completed with warnings'); - - if ($input->getOption('fail-on-errors')) { - return 1; - } - } else { - $this->io->success('Build completed successfully!'); - } - - return Command::SUCCESS; - } - - private function generateJson(Metas $metas) - { - $this->io->note('Start exporting doc into json files'); - - $jsonGenerator = new JsonGenerator($metas, $this->buildConfig); - $jsonGenerator->setOutput($this->io); - $jsonGenerator->generateJson(); - } - - private function renderDocForPDF(Metas $metas) - { - $htmlForPdfGenerator = new HtmlForPdfGenerator($metas, $this->buildConfig); - $htmlForPdfGenerator->generateHtmlForPdf(); - } - - private function addProgressListener(EventManager $eventManager) - { - $progressListener = new BuildProgressListener($this->io); - $progressListener->attachListeners($eventManager); - } -} diff --git a/src/DocBuilder.php b/src/DocBuilder.php deleted file mode 100644 index 46ce8064..00000000 --- a/src/DocBuilder.php +++ /dev/null @@ -1,87 +0,0 @@ -isBuildCacheEnabled() && $filesystem->exists($config->getOutputDir())) { - $filesystem->remove($config->getOutputDir()); - } - $filesystem->mkdir($config->getOutputDir()); - - $configFileParser = new ConfigFileParser($config, new NullOutput()); - $configFileParser->processConfigFile($config->getContentDir()); - - $builder = new Builder(KernelFactory::createKernel($config)); - $builder->build($config->getContentDir(), $config->getOutputDir()); - - $buildResult = new BuildResult($builder); - - $missingFilesChecker = new MissingFilesChecker($config); - $missingFiles = $missingFilesChecker->getMissingFiles(); - foreach ($missingFiles as $missingFile) { - $buildResult->appendError(sprintf('Missing file "%s"', $missingFile)); - } - - if (!$buildResult->isSuccessful()) { - $buildResult->prependError(sprintf('Build errors from "%s"', date('Y-m-d h:i:s'))); - $filesystem->dumpFile($config->getOutputDir().'/build_errors.txt', implode("\n", $buildResult->getErrors())); - } - - if ($config->isContentAString()) { - $htmlFilePath = $config->getOutputDir().'/index.html'; - if (is_file($htmlFilePath)) { - // generated HTML contents are a full HTML page, so we need to - // extract the contents of the tag - $crawler = new Crawler(file_get_contents($htmlFilePath)); - $buildResult->setStringResult(trim($crawler->filter('body')->html())); - } - } elseif ($config->getSubdirectoryToBuild()) { - $metas = $buildResult->getMetadata(); - $htmlForPdfGenerator = new HtmlForPdfGenerator($metas, $config); - $htmlForPdfGenerator->generateHtmlForPdf(); - } elseif ($config->generateJsonFiles()) { - $metas = $buildResult->getMetadata(); - $jsonGenerator = new JsonGenerator($metas, $config); - $buildResult->setJsonResults($jsonGenerator->generateJson($builder->getIndexName())); - } - - return $buildResult; - } - - public function buildString(string $contents): BuildResult - { - $filesystem = new Filesystem(); - $tmpDir = sys_get_temp_dir().'/doc_builder_build_string_'.random_int(1, 100000000); - if ($filesystem->exists($tmpDir)) { - $filesystem->remove($tmpDir); - } - $filesystem->mkdir($tmpDir); - - $filesystem->dumpFile($tmpDir.'/index.rst', $contents); - - $buildConfig = (new BuildConfig()) - ->setContentIsString() - ->setContentDir($tmpDir) - ->setOutputDir($tmpDir.'/output') - ->disableBuildCache() - ->disableJsonFileGeneration() - ; - - $buildResult = $this->build($buildConfig); - $filesystem->remove($tmpDir); - - return $buildResult; - } -} diff --git a/src/DocsKernel.php b/src/DocsKernel.php deleted file mode 100644 index 8ffbedd7..00000000 --- a/src/DocsKernel.php +++ /dev/null @@ -1,70 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder; - -use Doctrine\Common\EventManager; -use Doctrine\RST\Builder; -use Doctrine\RST\Configuration; -use Doctrine\RST\ErrorManager; -use Doctrine\RST\Event\PostBuildRenderEvent; -use Doctrine\RST\Event\PreNodeRenderEvent; -use Doctrine\RST\Event\PreParseDocumentEvent; -use Doctrine\RST\Kernel; -use SymfonyDocsBuilder\Listener\AdmonitionListener; -use SymfonyDocsBuilder\Listener\AssetsCopyListener; -use SymfonyDocsBuilder\Listener\CopyImagesListener; -use SymfonyDocsBuilder\Listener\DuplicatedHeaderIdListener; - -class DocsKernel extends Kernel -{ - private $buildConfig; - - public function __construct(BuildConfig $buildConfig, ?Configuration $configuration = null, $directives = [], $references = []) - { - parent::__construct($configuration, $directives, $references); - - $this->buildConfig = $buildConfig; - } - - public function initBuilder(Builder $builder): void - { - $this->initializeListeners( - $builder->getConfiguration()->getEventManager(), - $builder->getErrorManager() - ); - - $builder->setScannerFinder($this->buildConfig->createFileFinder()); - } - - private function initializeListeners(EventManager $eventManager, ErrorManager $errorManager) - { - $eventManager->addEventListener( - PreParseDocumentEvent::PRE_PARSE_DOCUMENT, - new AdmonitionListener() - ); - - $eventManager->addEventListener( - PreParseDocumentEvent::PRE_PARSE_DOCUMENT, - new DuplicatedHeaderIdListener() - ); - - $eventManager->addEventListener( - PreNodeRenderEvent::PRE_NODE_RENDER, - new CopyImagesListener($this->buildConfig, $errorManager) - ); - - if (!$this->buildConfig->getSubdirectoryToBuild()) { - $eventManager->addEventListener( - [PostBuildRenderEvent::POST_BUILD_RENDER], - new AssetsCopyListener($this->buildConfig->getOutputDir()) - ); - } - } -} diff --git a/src/GuidesExtension/config/parser.php b/src/GuidesExtension/config/parser.php new file mode 100644 index 00000000..cd2be967 --- /dev/null +++ b/src/GuidesExtension/config/parser.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use phpDocumentor\Guides\RestructuredText\Parser\Productions\DirectiveContentRule; + +return static function (ContainerConfigurator $container) { + $container->services() + ->defaults()->autowire() + + ->load('SymfonyTools\\DocsBuilder\\GuidesExtension\\Directives\\', '../src/Directives') + ->bind('$startingRule', service(DirectiveContentRule::class)) + ->tag('phpdoc.guides.directive') + + ->load('SymfonyTools\\DocsBuilder\\GuidesExtension\\TextRole\\', '../src/TextRole') + ->tag('phpdoc.guides.parser.rst.text_role') + ; +}; diff --git a/src/GuidesExtension/config/renderer.php b/src/GuidesExtension/config/renderer.php new file mode 100644 index 00000000..d83f27a4 --- /dev/null +++ b/src/GuidesExtension/config/renderer.php @@ -0,0 +1,51 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use SymfonyTools\DocsBuilder\GuidesExtension\Highlighter\SymfonyHighlighter; +use SymfonyTools\DocsBuilder\GuidesExtension\NodeRenderer\CodeNodeRenderer; +use SymfonyTools\DocsBuilder\GuidesExtension\NodeRenderer\MenuEntryRenderer; +use SymfonyTools\DocsBuilder\GuidesExtension\Node\ExternalLinkNode; +use SymfonyTools\DocsBuilder\GuidesExtension\Twig\CodeExtension; +use SymfonyTools\DocsBuilder\GuidesExtension\Twig\UrlExtension; +use SymfonyTools\DocsBuilder\GuidesExtension\Renderer\JsonRenderer; +use Twig\Extension\ExtensionInterface; +use Twig\Extra\String\StringExtension; +use phpDocumentor\Guides\Code\Highlighter\Highlighter; +use phpDocumentor\Guides\NodeRenderers\NodeRenderer; +use phpDocumentor\Guides\NodeRenderers\TemplateNodeRenderer; + +return static function (ContainerConfigurator $container) { + $container ->services() + ->defaults()->autowire()->autoconfigure() + ->instanceof(ExtensionInterface::class)->tag('twig.extension') + ->instanceof(NodeRenderer::class)->tag('phpdoc.guides.noderenderer.html', ['priority' => 10]) + + ->set(CodeExtension::class) + ->set(UrlExtension::class) + ->set(StringExtension::class) + + ->set(CodeNodeRenderer::class) + ->set(MenuEntryRenderer::class) + + ->set('symfony.node_renderer.html.inline.external_link', TemplateNodeRenderer::class) + ->arg('$template', 'inline/external-link.html.twig') + ->arg('$nodeClass', ExternalLinkNode::class) + + ->set(SymfonyHighlighter::class) + ->decorate(Highlighter::class) + + ->set(JsonRenderer::class) + ->arg('$nodeRendererFactory', service('phpdoc.guides.noderenderer.factory.json')) + ->tag('phpdoc.renderer.typerenderer', ['format' => 'json', 'noderender_tag' => 'phpdoc.guides.noderenderer.html']) + ; +}; diff --git a/src/GuidesExtension/config/services.php b/src/GuidesExtension/config/services.php new file mode 100644 index 00000000..17abee21 --- /dev/null +++ b/src/GuidesExtension/config/services.php @@ -0,0 +1,29 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\DependencyInjection\Loader\Configurator; + +use Psr\EventDispatcher\EventDispatcherInterface; +use SymfonyTools\DocsBuilder\GuidesExtension\Build\BuildConfig; +use SymfonyTools\DocsBuilder\GuidesExtension\DocBuilder; +use Symfony\Component\EventDispatcher\EventDispatcher; + +return static function (ContainerConfigurator $container) { + $container->services() + ->defaults()->autowire() + + ->set(EventDispatcherInterface::class, EventDispatcher::class) + + ->set(BuildConfig::class)->public() + + ->set(DocBuilder::class)->public() + ; +}; diff --git a/src/Templates/highlight.php/README.md b/src/GuidesExtension/resources/highlight.php/README.md similarity index 100% rename from src/Templates/highlight.php/README.md rename to src/GuidesExtension/resources/highlight.php/README.md diff --git a/src/Templates/highlight.php/php.json b/src/GuidesExtension/resources/highlight.php/php.json similarity index 100% rename from src/Templates/highlight.php/php.json rename to src/GuidesExtension/resources/highlight.php/php.json diff --git a/src/Templates/highlight.php/twig.json b/src/GuidesExtension/resources/highlight.php/twig.json similarity index 100% rename from src/Templates/highlight.php/twig.json rename to src/GuidesExtension/resources/highlight.php/twig.json diff --git a/src/Templates/highlight.php/yaml.json b/src/GuidesExtension/resources/highlight.php/yaml.json similarity index 100% rename from src/Templates/highlight.php/yaml.json rename to src/GuidesExtension/resources/highlight.php/yaml.json diff --git a/src/GuidesExtension/resources/templates/blank/html/layout.html.twig b/src/GuidesExtension/resources/templates/blank/html/layout.html.twig new file mode 100644 index 00000000..3ddfb62f --- /dev/null +++ b/src/GuidesExtension/resources/templates/blank/html/layout.html.twig @@ -0,0 +1 @@ +{% block body %}{% endblock %} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/_helpers.twig b/src/GuidesExtension/resources/templates/symfonycom/html/_helpers.twig new file mode 100644 index 00000000..df8eb3df --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/_helpers.twig @@ -0,0 +1,12 @@ +{% macro link(url, text, attributes = {}) %} +{% set attributes = attributes|merge({ + class: (attributes.class|default ? attributes.class ~ ' ') ~ 'reference ' ~ (url is safe_url ? 'internal' : 'external') + }) +%} + + {{- text|raw -}} + +{% endmacro %} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/admonition.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/admonition.html.twig new file mode 100644 index 00000000..91bf8701 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/admonition.html.twig @@ -0,0 +1,22 @@ +{# icons are from https://heroicons.com/ - MIT License #} +
+

+ {% if name in ['admonition', 'note'] %} + + {% elseif name in ['hint', 'tip'] %} + + {% elseif name in ['attention', 'caution', 'important', 'warning'] %} + + {% elseif name in ['danger', 'error'] %} + + {% elseif name in ['versionadded', 'deprecated'] %} + {# don't show an icon for these directives #} + {% elseif name in ['seealso'] %} + + {% elseif name in ['screencast'] %} + + {% endif %} + {{ text|raw }} +

+ {{ renderNode(node) }} +
diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/code.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/code.html.twig new file mode 100644 index 00000000..2580254a --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/code.html.twig @@ -0,0 +1,15 @@ +{% set size = ['-', 'sm', 'md', 'lg', 'xl'][loc | length] %} + +
+ {% if node.caption -%} +
{{ renderNode(node.caption) }}
+ {% endif -%} +
+
+            {{- line_numbers -}}
+        
+

+            {{- code|raw -}}
+        
+
+
diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/configuration-block.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/configuration-block.html.twig new file mode 100644 index 00000000..f1f8bae7 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/configuration-block.html.twig @@ -0,0 +1,17 @@ +
+
+ {% for tab in node.tabs %} + + {% endfor %} +
+ + {% for tab in node.tabs %} +
+ {{ renderNode(tab.content) }} +
+ {% endfor %} +
diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/directive/tabs.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/directive/tabs.html.twig new file mode 100644 index 00000000..4e9e1c8d --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/directive/tabs.html.twig @@ -0,0 +1,18 @@ +
+
+ {% for tab in node.tabs %} + + {% endfor %} +
+ + {% for tab in node.tabs %} +
+ {{ renderNode(tab.children) }} +
+ {% endfor %} +
+ diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/image.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/image.html.twig new file mode 100644 index 00000000..08c16155 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/image.html.twig @@ -0,0 +1,5 @@ +{% set wrap_image_with_browser = 'with-browser' in node.classes %} +{% set src = node.value is external_target ? node.value : asset(node.value) %} +{% if wrap_image_with_browser %}
{% endif %} + +{% if wrap_image_with_browser %}
{% endif %} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/list/list-item.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/list/list-item.html.twig new file mode 100644 index 00000000..21fd05de --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/list/list-item.html.twig @@ -0,0 +1,5 @@ +
  • + {%- for child in node.children -%} + {{ renderNode(child) }} + {%- endfor -%} +
  • diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-item.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-item.html.twig new file mode 100644 index 00000000..5affffc6 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-item.html.twig @@ -0,0 +1,15 @@ +
  • + {{ renderNode(node.value.value) }} + {% if node.children|length %} + {% include "body/menu/menu-level.html.twig" with { + tocItems: node.children, + level: level + 1 + } %} + {% endif %} + {% if node.sections|length %} + {% include "body/menu/menu-level.html.twig" with { + entries: node.sections, + level: node.level + 1 + } %} + {% endif %} +
  • diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-level.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-level.html.twig new file mode 100644 index 00000000..024fd6ab --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu-level.html.twig @@ -0,0 +1,7 @@ +{% set level = level | default(1) %} +{% set entries = entries | default(node.menuEntries) %} + diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu.html.twig new file mode 100644 index 00000000..82103a1f --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/menu.html.twig @@ -0,0 +1,3 @@ + diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/table-of-content.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/table-of-content.html.twig new file mode 100644 index 00000000..0140d42b --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/menu/table-of-content.html.twig @@ -0,0 +1,3 @@ +
    + {% include "body/menu/menu-level.html.twig" %} +
    diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/table.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/table.html.twig new file mode 100644 index 00000000..757ada30 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/table.html.twig @@ -0,0 +1,31 @@ +
    + + {% if tableHeaderRows is not empty %} + + {% for tableHeaderRow in tableHeaderRows %} + + {% for column in tableHeaderRow.columns %} + 1 %} colspan="{{ column.colspan }}"{% endif %}> + {%- for child in column.children -%} + {{- renderNode(child) -}} + {% endfor %} + {% endfor %} + + {% endfor %} + + {% endif %} + + + {% for tableRow in tableRows %} + + {% for column in tableRow.columns %} + 1 %} colspan="{{ column.colSpan }}"{% endif %}{% if column.rowSpan > 1 %} rowspan="{{ column.rowSpan }}"{% endif %}> + {%- for child in column.children -%} + {{- renderNode(child) -}} + {% endfor %} + {% endfor %} + + {% endfor %} + + +
    diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/topic.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/topic.html.twig new file mode 100644 index 00000000..27e45ff3 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/topic.html.twig @@ -0,0 +1,4 @@ +
    +

    {{ name|raw }}

    + {{ renderNode(node) }} +
    diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/body/version-change.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/body/version-change.html.twig new file mode 100644 index 00000000..8dbcbc24 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/body/version-change.html.twig @@ -0,0 +1,6 @@ +
    +

    + {{ node.versionModified }} +

    + {{ renderNode(node.value) }} +
    diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/inline/doc.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/inline/doc.html.twig new file mode 100644 index 00000000..04701ab3 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/inline/doc.html.twig @@ -0,0 +1 @@ +{{- include('inline/link.html.twig') -}} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/inline/external-link.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/inline/external-link.html.twig new file mode 100644 index 00000000..04701ab3 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/inline/external-link.html.twig @@ -0,0 +1 @@ +{{- include('inline/link.html.twig') -}} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/inline/link.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/inline/link.html.twig new file mode 100644 index 00000000..e8fd5dda --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/inline/link.html.twig @@ -0,0 +1,13 @@ +{%- set attributes = { + title: node.title, + class: (node.classes ? node.classes|join(' ') ~ ' ') ~ 'reference ' ~ (node.url is safe_url ? 'internal' : 'external') + }|filter(v => v != '') +-%} + + {%- for child in node.children -%} + {{ renderNode(child) }} + {%- endfor -%} + diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/inline/literal.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/inline/literal.html.twig new file mode 100644 index 00000000..cfce08ec --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/inline/literal.html.twig @@ -0,0 +1 @@ +{{ node.value|e|fqcn }} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/inline/ref.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/inline/ref.html.twig new file mode 100644 index 00000000..04701ab3 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/inline/ref.html.twig @@ -0,0 +1 @@ +{{- include('inline/link.html.twig') -}} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/structure/header-title.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/structure/header-title.html.twig new file mode 100644 index 00000000..eb2d6ae6 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/structure/header-title.html.twig @@ -0,0 +1 @@ +{{ renderNode(node.value) }} diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/structure/layout.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/structure/layout.html.twig new file mode 100644 index 00000000..dd3529e0 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/structure/layout.html.twig @@ -0,0 +1,13 @@ + + + + {{ title }} + {%- if title!=null and env.projectNode.title!=null %} - {% endif -%} + {%- if env.projectNode.title -%}{{ env.projectNode.title }}{%- endif -%} + {%~ block head %} + {%~ endblock %} + + + {% block body %}{% endblock %} + + diff --git a/src/GuidesExtension/resources/templates/symfonycom/html/structure/sidebar.html.twig b/src/GuidesExtension/resources/templates/symfonycom/html/structure/sidebar.html.twig new file mode 100644 index 00000000..c9c78752 --- /dev/null +++ b/src/GuidesExtension/resources/templates/symfonycom/html/structure/sidebar.html.twig @@ -0,0 +1,4 @@ +
    + + {{ renderNode(node) }} +
    diff --git a/src/GuidesExtension/src/Application.php b/src/GuidesExtension/src/Application.php new file mode 100644 index 00000000..14436fc0 --- /dev/null +++ b/src/GuidesExtension/src/Application.php @@ -0,0 +1,31 @@ +output); + } +} diff --git a/src/GuidesExtension/src/Build/BuildConfig.php b/src/GuidesExtension/src/Build/BuildConfig.php new file mode 100644 index 00000000..9521588c --- /dev/null +++ b/src/GuidesExtension/src/Build/BuildConfig.php @@ -0,0 +1,39 @@ + 'fjson' === $this->outputFormat ? 'html' : $this->outputFormat; + } + + public string $symfonyRepositoryUrl { + get => str_replace('{symfonyVersion}', $this->symfonyVersion, self::SYMFONY_REPOSITORY_URL); + } + + public function __construct( + public string $symfonyVersion = '6.1', + ) { + } + + public function createProjectNode(): ProjectNode + { + return new ProjectNode('Symfony', $this->symfonyVersion); + } +} diff --git a/src/GuidesExtension/src/Build/BuildEnvironment.php b/src/GuidesExtension/src/Build/BuildEnvironment.php new file mode 100644 index 00000000..d22f9343 --- /dev/null +++ b/src/GuidesExtension/src/Build/BuildEnvironment.php @@ -0,0 +1,21 @@ +sourceFilesystem = new FlysystemV3(new LeagueFilesystem($sourceAdapter ?? new InMemoryFilesystemAdapter())); + $this->outputFilesystem = new FlysystemV3(new LeagueFilesystem($outputAdapter ?? new InMemoryFilesystemAdapter())); + } + + #[\Override] + public function getSourceFilesystem(): FileSystem + { + return $this->sourceFilesystem; + } + + #[\Override] + public function getOutputFilesystem(): FileSystem + { + return $this->outputFilesystem; + } +} diff --git a/src/GuidesExtension/src/Build/LocalBuildEnvironment.php b/src/GuidesExtension/src/Build/LocalBuildEnvironment.php new file mode 100644 index 00000000..f3292504 --- /dev/null +++ b/src/GuidesExtension/src/Build/LocalBuildEnvironment.php @@ -0,0 +1,74 @@ +setSourceDir($cwd); + } + } + + public function setSourceDir(string $sourceDir): void + { + if ($sourceDir === $this->sourceDir) { + return; + } + + $this->sourceDir = $sourceDir; + $this->sourceFilesystem = null; + + if (null == $this->outputDir) { + $this->setOutputDir($sourceDir.'/_output'); + } + } + + public function setOutputDir(string $outputDir): void + { + if ($outputDir !== $this->outputDir) { + $this->outputFilesystem = null; + } + $this->outputDir = $outputDir; + } + + #[\Override] + public function getSourceFilesystem(): FileSystem + { + if (null === $this->sourceDir) { + throw new \BadMethodCallException('Cannot get source filesystem: no source directory set.'); + } + + return $this->sourceFilesystem ??= new FlysystemV3(new LeagueFilesystem(new LocalFilesystemAdapter($this->sourceDir))); + } + + #[\Override] + public function getOutputFilesystem(): FileSystem + { + if (null === $this->outputDir) { + throw new \BadMethodCallException('Cannot get output filesystem: no output directory set.'); + } + + return $this->outputFilesystem ??= new FlysystemV3(new LeagueFilesystem(new LocalFilesystemAdapter($this->outputDir))); + } +} diff --git a/src/GuidesExtension/src/Build/StringBuildEnvironment.php b/src/GuidesExtension/src/Build/StringBuildEnvironment.php new file mode 100644 index 00000000..e9aa14dc --- /dev/null +++ b/src/GuidesExtension/src/Build/StringBuildEnvironment.php @@ -0,0 +1,47 @@ +filesystem = new FlysystemV3(new LeagueFilesystem(new InMemoryFilesystemAdapter())); + $this->filesystem->put('index.rst', $contents); + } + + #[\Override] + public function getSourceFilesystem(): FileSystem + { + return $this->filesystem; + } + + #[\Override] + public function getOutputFilesystem(): FileSystem + { + return $this->filesystem; + } + + public function getOutput(): ?string + { + $output = $this->filesystem->read('/index.html'); + + return false === $output ? null : $output; + } +} diff --git a/src/GuidesExtension/src/DependencyInjection/CommandLocator.php b/src/GuidesExtension/src/DependencyInjection/CommandLocator.php new file mode 100644 index 00000000..ae297f2b --- /dev/null +++ b/src/GuidesExtension/src/DependencyInjection/CommandLocator.php @@ -0,0 +1,35 @@ +commands->get($commandName); + } catch (NotFoundExceptionInterface) { + throw new MissingHandlerException(\sprintf('No handler found for command "%s"', $commandName)); + } + } +} diff --git a/src/GuidesExtension/src/DependencyInjection/SymfonyExtension.php b/src/GuidesExtension/src/DependencyInjection/SymfonyExtension.php new file mode 100644 index 00000000..d1a043b2 --- /dev/null +++ b/src/GuidesExtension/src/DependencyInjection/SymfonyExtension.php @@ -0,0 +1,75 @@ +load('services.php'); + $loader->load('parser.php'); + $loader->load('renderer.php'); + } + + #[\Override] + public function prepend(ContainerBuilder $container): void + { + $templatesDir = \dirname(__DIR__, 2).'/resources/templates'; + + $container->prependExtensionConfig('guides', [ + 'default_code_language' => 'php', + 'themes' => [ + 'symfonycom' => $templatesDir.'/symfonycom/html', + ], + ]); + + $container->prependExtensionConfig('code', [ + 'languages' => [ + 'php' => \dirname(__DIR__, 2).'/resources/highlight.php/php.json', + 'twig' => \dirname(__DIR__, 2).'/resources/highlight.php/twig.json', + 'yaml' => \dirname(__DIR__, 2).'/resources/highlight.php/yaml.json', + ], + 'aliases' => [ + 'caddy' => 'plaintext', + 'env' => 'bash', + 'html+jinja' => 'twig', + 'html+twig' => 'twig', + 'jinja' => 'twig', + 'html+php' => 'html', + 'xml+php' => 'xml', + 'php-annotations' => 'php', + 'php-attributes' => 'php', + 'terminal' => 'bash', + 'rst' => 'markdown', + 'php-standalone' => 'php', + 'php-symfony' => 'php', + 'varnish4' => 'c', + 'varnish3' => 'c', + 'vcl' => 'c', + ], + ]); + } +} diff --git a/src/GuidesExtension/src/Directives/BestPracticeDirective.php b/src/GuidesExtension/src/Directives/BestPracticeDirective.php new file mode 100644 index 00000000..49c72d6f --- /dev/null +++ b/src/GuidesExtension/src/Directives/BestPracticeDirective.php @@ -0,0 +1,23 @@ +themeManager->useTheme('symfonycom'); + + $projectNode = $this->buildConfig->createProjectNode(); + + /** @var list $documents */ + $documents = $this->commandBus->handle(new ParseDirectoryCommand($buildEnvironment->getSourceFilesystem(), '/', 'rst', $projectNode)); + + $documents = $this->commandBus->handle(new CompileDocumentsCommand($documents, new CompilerContext($projectNode))); + + $this->rendererFactory->getRenderSet($this->buildConfig->outputFormat)->render( + new RenderCommand( + $this->buildConfig->format, + $documents, + $buildEnvironment->getSourceFilesystem(), + $buildEnvironment->getOutputFilesystem(), + $projectNode + ) + ); + } + + public function buildString(string $contents): string + { + $buildEnvironment = new StringBuildEnvironment($contents); + + $this->build($buildEnvironment); + + $output = $buildEnvironment->getOutput(); + if (null === $output) { + throw new \LogicException('Cannot build HTML from the provided reStructuredText: no HTML output found.'); + } + + return $output; + } +} diff --git a/src/GuidesExtension/src/DocsKernel.php b/src/GuidesExtension/src/DocsKernel.php new file mode 100644 index 00000000..06c6bc65 --- /dev/null +++ b/src/GuidesExtension/src/DocsKernel.php @@ -0,0 +1,135 @@ + $extensions */ + public static function create(array $extensions = []): self + { + $containerFactory = new ContainerFactory([new SymfonyExtension(), self::createDefaultExtension(), new CodeExtension(), ...$extensions]); + + for ($i = 1; $i <= 4; ++$i) { + if (is_dir($vendor = \dirname(__DIR__, $i).'/vendor')) { + break; + } + } + + $containerFactory->loadExtensionConfig(GuidesExtension::class, [ + 'default_code_language' => 'php', + ]); + + $containerFactory->loadExtensionConfig(ReStructuredTextExtension::class, [ + 'code_language_labels' => [ + ['language' => 'caddy', 'label' => 'Caddy'], + ['language' => 'env', 'label' => 'Dotenv'], + ['language' => 'html+jinja', 'label' => 'Twig'], + ['language' => 'html+php', 'label' => 'PHP'], + ['language' => 'html+twig', 'label' => 'Twig'], + ['language' => 'jinja', 'label' => 'Twig'], + ['language' => 'php', 'label' => 'PHP'], + ['language' => 'php-annotations', 'label' => 'Annotations'], + ['language' => 'php-attributes', 'label' => 'Attributes'], + ['language' => 'php-standalone', 'label' => 'Standalone Use'], + ['language' => 'php-symfony', 'label' => 'Framework Use'], + ['language' => 'rst', 'label' => 'RST'], + ['language' => 'terminal', 'label' => 'Bash'], + ['language' => 'varnish3', 'label' => 'Varnish 3'], + ['language' => 'varnish4', 'label' => 'Varnish 4'], + ['language' => 'vcl', 'label' => 'VCL'], + ['language' => 'xml', 'label' => 'XML'], + ['language' => 'xml+php', 'label' => 'XML'], + ['language' => 'yaml', 'label' => 'YAML'], + ], + ]); + + $containerFactory->loadExtensionConfig(CodeExtension::class, [ + 'aliases' => [ + 'env' => 'bash', + 'html+jinja' => 'twig', + 'html+twig' => 'twig', + 'jinja' => 'twig', + 'html+php' => 'html', + 'xml+php' => 'xml', + 'php-annotations' => 'php', + 'php-attributes' => 'php', + 'terminal' => 'bash', + 'rst' => 'markdown', + 'php-standalone' => 'php', + 'php-symfony' => 'php', + 'varnish4' => 'c', + 'varnish3' => 'c', + 'vcl' => 'c', + ], + ]); + + $container = $containerFactory->create($vendor); + + return new self($container); + } + + /** + * @template T + * + * @param class-string $fqcn + * + * @return T + * + * @psalm-suppress InvalidReturnType + * @psalm-suppress InvalidReturnStatement + */ + public function get(string $fqcn): object + { + return $this->container->get($fqcn); + } + + private static function createDefaultExtension(): ExtensionInterface + { + return new class extends Extension { + #[\Override] + public function load(array $configs, ContainerBuilder $container): void + { + $container->register(Logger::class)->setArgument('$name', 'docs-builder'); + $container->setAlias(LoggerInterface::class, new Alias(Logger::class)); + + $container->register(EventDispatcher::class); + $container->setAlias(EventDispatcherInterface::class, new Alias(EventDispatcher::class)); + } + + #[\Override] + public function getAlias(): string + { + return 'docs-builder'; + } + }; + } +} diff --git a/src/GuidesExtension/src/Highlighter/SymfonyHighlighter.php b/src/GuidesExtension/src/Highlighter/SymfonyHighlighter.php new file mode 100644 index 00000000..a2ff1137 --- /dev/null +++ b/src/GuidesExtension/src/Highlighter/SymfonyHighlighter.php @@ -0,0 +1,42 @@ +highlighter)($language, $code, $debugInformation); + + $code = $result->code; + if ('php' === $result->language) { + // highlight the $ in PHP variable names + $code = str_replace('$', '$', $code); + } + + if ('terminal' === $language) { + $code = preg_replace('/^\$ /m', '$ ', $code) ?? $code; + $code = preg_replace('/^C:\\\> /m', 'C:\> ', $code) ?? $code; + } + + return new HighlightResult($result->language, $code); + } +} diff --git a/src/GuidesExtension/src/Node/ExternalLinkNode.php b/src/GuidesExtension/src/Node/ExternalLinkNode.php new file mode 100644 index 00000000..e9dbab1e --- /dev/null +++ b/src/GuidesExtension/src/Node/ExternalLinkNode.php @@ -0,0 +1,31 @@ +title; + } +} diff --git a/src/GuidesExtension/src/NodeRenderer/CodeNodeRenderer.php b/src/GuidesExtension/src/NodeRenderer/CodeNodeRenderer.php new file mode 100644 index 00000000..b64b695a --- /dev/null +++ b/src/GuidesExtension/src/NodeRenderer/CodeNodeRenderer.php @@ -0,0 +1,68 @@ + + */ +final class CodeNodeRenderer implements NodeRenderer +{ + public function __construct( + private TemplateRenderer $renderer, + private SymfonyHighlighter $higlighter, + ) { + } + + #[\Override] + public function supports(string $nodeFqcn): bool + { + return CodeNode::class === $nodeFqcn || is_a($nodeFqcn, CodeNode::class, true); + } + + #[\Override] + public function render(Node $node, RenderContext $renderContext): string + { + if (!$node instanceof CodeNode) { + throw new \LogicException(\sprintf('"%s" can only render code nodes, got "%s".', __CLASS__, get_debug_type($node))); + } + + $language = $node->getLanguage() ?? 'text'; + $highlight = ($this->higlighter)($language, $node->getValue(), $renderContext->getLoggerInformation()); + + $languages = array_unique([$language, $highlight->language]); + $code = $highlight->code; + + $codeLines = preg_split('/\R/', trim($code)); + \assert(\is_array($codeLines)); + $numOfLines = \count($codeLines); + $lineNumbers = implode("\n", range(1, $numOfLines)); + + return $this->renderer->renderTemplate( + $renderContext, + 'body/code.html.twig', + [ + 'languages' => $languages, + 'code' => rtrim($code), + 'line_numbers' => $lineNumbers, + 'loc' => $numOfLines, + 'node' => $node, + ] + ); + } +} diff --git a/src/GuidesExtension/src/NodeRenderer/MenuEntryRenderer.php b/src/GuidesExtension/src/NodeRenderer/MenuEntryRenderer.php new file mode 100644 index 00000000..b3298740 --- /dev/null +++ b/src/GuidesExtension/src/NodeRenderer/MenuEntryRenderer.php @@ -0,0 +1,61 @@ + + */ +final class MenuEntryRenderer implements NodeRenderer +{ + public function __construct( + private TemplateRenderer $renderer, + private UrlGeneratorInterface $urlGenerator, + ) { + } + + #[\Override] + public function supports(string $nodeFqcn): bool + { + return MenuEntryNode::class === $nodeFqcn || is_a($nodeFqcn, MenuEntryNode::class, true); + } + + #[\Override] + public function render(Node $node, RenderContext $renderContext): string + { + if (!$node instanceof MenuEntryNode) { + throw new \LogicException(\sprintf('"%s" can only render menu entry nodes, got "%s".', __CLASS__, get_debug_type($node))); + } + + $url = $this->urlGenerator->generateCanonicalOutputUrl( + $renderContext, + $node->getUrl(), + $node instanceof SectionMenuEntryNode ? $node->getValue()?->getId() : null + ); + + return $this->renderer->renderTemplate( + $renderContext, + 'body/menu/menu-item.html.twig', + [ + 'url' => $url, + 'node' => $node, + ], + ); + } +} diff --git a/src/GuidesExtension/src/Renderer/JsonRenderer.php b/src/GuidesExtension/src/Renderer/JsonRenderer.php new file mode 100644 index 00000000..617b1c05 --- /dev/null +++ b/src/GuidesExtension/src/Renderer/JsonRenderer.php @@ -0,0 +1,79 @@ +getProjectNode(), + $renderCommand->getDocumentArray(), + $renderCommand->getOrigin(), + $renderCommand->getDestination(), + $renderCommand->getDestinationPath(), + $renderCommand->getOutputFormat(), + )->withIterator($renderCommand->getDocumentIterator()); + + foreach ($projectRenderContext->getIterator() as $documentNode) { + $context = $projectRenderContext->withDocument($documentNode); + $html = implode( + "\n", + array_map(fn (Node $node): string => $this->nodeRendererFactory->get($node)->render($node, $context), $documentNode->getChildren()) + ); + + $prevDocument = $nextDocument = null; + if (!$documentNode->isOrphan()) { + $prevDocument = $context->getIterator()->previousNode(); + $nextDocument = $context->getIterator()->nextNode(); + } + $context->getDestination()->put( + $context->getDestinationPath().'/'.$context->getCurrentFileName().'.fjson', + json_encode([ + 'parents' => [], + 'prev' => $this->getDocumentData($context, $prevDocument), + 'next' => $this->getDocumentData($context, $nextDocument), + 'title' => $documentNode->getTitle()?->toString() ?? '', + 'body' => $html, + ], \JSON_PRETTY_PRINT) + ); + } + } + + private function getDocumentData($context, ?DocumentNode $document): ?array + { + if (null === $document || $document->isOrphan()) { + return null; + } + + $url = $this->urlGenerator->createFileUrl($context, $document->getFilePath()); + + return [ + 'title' => $document->getTitle()?->toString() ?? '', + 'link' => substr($url, 0, strrpos($url, '.')).'.html', + ]; + } +} diff --git a/src/GuidesExtension/src/TextRole/ClassRole.php b/src/GuidesExtension/src/TextRole/ClassRole.php new file mode 100644 index 00000000..6ccd5c3c --- /dev/null +++ b/src/GuidesExtension/src/TextRole/ClassRole.php @@ -0,0 +1,79 @@ +replace('\\\\', '\\'); + + if (str_starts_with($fqcn, 'Symfony\\AI\\')) { + /** + * Symfony AI classes require some special handling because of its monorepo structure. Example: + * + * input: Symfony\AI\Agent\Memory\StaticMemoryProvider + * output: https://github.com/symfony/ai/blob/main/src/agent/src/Memory/StaticMemoryProvider.php + */ + $classPath = $fqcn->after('Symfony\\AI\\'); + [$monorepoSubRepository, $classRelativePath] = $classPath->split('\\', 2); + // because of monorepo structure, the first part of the classpath needs to be slugged + // 'Agent' -> 'agent', 'AiBundle' -> 'ai-bundle', etc. + $monorepoSubRepository = $monorepoSubRepository->snake()->replace('_', '-')->lower(); + $classRelativePath = $classRelativePath->replace('\\', '/'); + + $url = \sprintf('https://github.com/symfony/ai/blob/main/src/%s/src/%s.php', $monorepoSubRepository, $classRelativePath); + } elseif (str_starts_with($fqcn, 'Symfony\\UX\\')) { + /** + * Symfony UX classes require some special handling because of its monorepo structure. Example: + * + * input: Symfony\UX\Chartjs\Twig\ChartExtension + * output: https://github.com/symfony/ux/blob/2.x/src/Chartjs/src/Twig/ChartExtension.php + */ + $classPath = $fqcn->after('Symfony\\UX\\'); + [$monorepoSubRepository, $classRelativePath] = $classPath->split('\\', 2); + $classRelativePath = $classRelativePath->replace('\\', '/'); + + $url = \sprintf('https://github.com/symfony/ux/blob/2.x/src/%s/src/%s.php', $monorepoSubRepository, $classRelativePath); + } else { + $url = \sprintf($this->buildConfig->symfonyRepositoryUrl, $fqcn->replace('\\', '/').'.php'); + } + + return new ExternalLinkNode($url, (string) $fqcn->afterLast('\\'), (string) $fqcn); + } + + #[\Override] + public function getName(): string + { + return 'class'; + } + + #[\Override] + public function getAliases(): array + { + return []; + } +} diff --git a/src/GuidesExtension/src/TextRole/MethodRole.php b/src/GuidesExtension/src/TextRole/MethodRole.php new file mode 100644 index 00000000..5b4cf1ac --- /dev/null +++ b/src/GuidesExtension/src/TextRole/MethodRole.php @@ -0,0 +1,55 @@ +containsAny('::')) { + throw new \RuntimeException(sprintf('Malformed method reference "%s"', $content)); + } + [$fqcn, $method] = $content->replace('\\\\', '\\')->split('::', 2); + + $filename = \sprintf('%s.php#:~:text=%s', $fqcn->replace('\\', '/'), rawurlencode('function '.$method)); + $url = \sprintf($this->buildConfig->symfonyRepositoryUrl, $filename); + + return new ExternalLinkNode($url, $method.'()', $fqcn.'::'.$method.'()'); + } + + #[\Override] + public function getName(): string + { + return 'method'; + } + + #[\Override] + public function getAliases(): array + { + return []; + } +} diff --git a/src/GuidesExtension/src/TextRole/NamespaceRole.php b/src/GuidesExtension/src/TextRole/NamespaceRole.php new file mode 100644 index 00000000..7cde7ce4 --- /dev/null +++ b/src/GuidesExtension/src/TextRole/NamespaceRole.php @@ -0,0 +1,50 @@ +replace('\\\\', '\\'); + + $url = \sprintf($this->buildConfig->symfonyRepositoryUrl, $fqcn->replace('\\', '/')); + + return new ExternalLinkNode($url, (string) $fqcn->afterLast('\\'), (string) $fqcn); + } + + #[\Override] + public function getName(): string + { + return 'namespace'; + } + + #[\Override] + public function getAliases(): array + { + return []; + } +} diff --git a/src/GuidesExtension/src/TextRole/PhpClassRole.php b/src/GuidesExtension/src/TextRole/PhpClassRole.php new file mode 100644 index 00000000..c9bfa865 --- /dev/null +++ b/src/GuidesExtension/src/TextRole/PhpClassRole.php @@ -0,0 +1,49 @@ +lower()->replace('\\', '-'); + + return new ExternalLinkNode($url, $fqcn->afterLast('\\'), $content); + } + + #[\Override] + public function getName(): string + { + return 'phpclass'; + } + + #[\Override] + public function getAliases(): array + { + return []; + } +} diff --git a/src/GuidesExtension/src/TextRole/PhpFunctionRole.php b/src/GuidesExtension/src/TextRole/PhpFunctionRole.php new file mode 100644 index 00000000..b7dbd914 --- /dev/null +++ b/src/GuidesExtension/src/TextRole/PhpFunctionRole.php @@ -0,0 +1,47 @@ +fqcn(...), ['is_safe' => ['html']]), + ]; + } + + #[\Override] + public function getFunctions(): array + { + return [ + new TwigFunction('dump', function (mixed ...$args) { dump(...$args); }), + ]; + } + + private function fqcn(string $fqcn): string + { + // some browsers can't break long properly, so we inject a + // `` (word-break HTML tag) after some characters to help break those + // We only do this for very long (4 or more \\) to not break short + // and common `` such as App\Entity\Something + if (substr_count($fqcn, '\\') >= 4) { + // breaking before the backslask is what Firefox browser does + $fqcn = str_replace('\\', '\\', $fqcn); + } + + return $fqcn; + } +} diff --git a/src/GuidesExtension/src/Twig/UrlExtension.php b/src/GuidesExtension/src/Twig/UrlExtension.php new file mode 100644 index 00000000..5a7685e8 --- /dev/null +++ b/src/GuidesExtension/src/Twig/UrlExtension.php @@ -0,0 +1,65 @@ +isSafeUrl(...)), + ]; + } + + #[\Override] + public function getFilters(): array + { + return [ + new TwigFilter('replace_version', $this->replaceSymfonyVersion(...)), + ]; + } + + private function replaceSymfonyVersion(string $url): string + { + return u($url)->replace('{version}', $this->buildConfig->symfonyVersion)->toString();; + } + + /* + * If the URL is considered safe, it's opened in the same browser tab; + * otherwise it's opened in a new tab and with some strict security options. + */ + private function isSafeUrl(string $url): bool + { + // The following are considered Symfony URLs: + // * https://symfony.com/[...] + // * https://[...].symfony.com/ (e.g. insight.symfony.com, etc.) + // * https://symfony.wip/[...] (used for internal/local development) + $url = u($url); + $isSymfonyUrl = $url->match('{^http(s)?://(.*\.)?symfony.(com|wip)}'); + $isRelativeUrl = !$url->startsWith('http://') && !$url->startsWith('https://'); + + return $isSymfonyUrl || $isRelativeUrl; + } +} diff --git a/tests/AbstractIntegrationTest.php b/tests/AbstractIntegrationTest.php deleted file mode 100644 index 0c7819ec..00000000 --- a/tests/AbstractIntegrationTest.php +++ /dev/null @@ -1,24 +0,0 @@ -remove(__DIR__.'/_output'); - - return (new BuildConfig()) - ->setSymfonyVersion('4.0') - ->setContentDir($sourceDir) - ->disableBuildCache() - ->setOutputDir(__DIR__.'/_output') - ; - } -} diff --git a/tests/Command/BuildDocsCommandTest.php b/tests/Command/BuildDocsCommandTest.php index d1cfaa37..b5e1d057 100644 --- a/tests/Command/BuildDocsCommandTest.php +++ b/tests/Command/BuildDocsCommandTest.php @@ -21,6 +21,11 @@ class BuildDocsCommandTest extends TestCase { + protected function setUp(): void + { + $this->markTestSkipped('Old Doctrine RST based tests'); + } + public function testBuildDocsDefault() { $buildConfig = $this->createBuildConfig(); diff --git a/tests/GuidesExtension/Highlighter/SymfonyHighlighterTest.php b/tests/GuidesExtension/Highlighter/SymfonyHighlighterTest.php new file mode 100644 index 00000000..ce134c54 --- /dev/null +++ b/tests/GuidesExtension/Highlighter/SymfonyHighlighterTest.php @@ -0,0 +1,54 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyTools\DocsBuilder\Tests\GuidesExtension\Highlighter; + +use Highlight\Highlighter; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use Psr\Log\NullLogger; +use SymfonyTools\DocsBuilder\GuidesExtension\Highlighter\SymfonyHighlighter; +use phpDocumentor\Guides\Code\Highlighter\HighlightPhpHighlighter; + +class SymfonyHighlighterTest extends TestCase +{ + public static function setUpBeforeClass(): void + { + Highlighter::registerLanguage('php', dirname(__DIR__, 3).'/src/GuidesExtension/resources/highlight.php/php.json', true); + Highlighter::registerLanguage('twig', dirname(__DIR__, 3).'/src/GuidesExtension/resources/highlight.php/twig.json', true); + } + + #[DataProvider('getRenderingTests')] + public function testCustomRendering(string $language, string $inputFile, string $outputFile): void + { + $highlighter = new SymfonyHighlighter(new HighlightPhpHighlighter(new Highlighter(), new NullLogger())); + $actual = $highlighter( + $language, + file_get_contents(__DIR__.'/fixtures/'.$inputFile), + [] + )->code; + + $this->assertSame(file_get_contents(__DIR__.'/fixtures/'.$outputFile), $actual); + } + + public static function getRenderingTests(): iterable + { + yield 'php' => [ + 'php', + 'php.input.txt', + 'php.output.html', + ]; + + yield 'twig' => [ + 'twig', + 'twig.input.txt', + 'twig.output.html', + ]; + } +} diff --git a/tests/Templates/fixtures/php.input.txt b/tests/GuidesExtension/Highlighter/fixtures/php.input.txt similarity index 100% rename from tests/Templates/fixtures/php.input.txt rename to tests/GuidesExtension/Highlighter/fixtures/php.input.txt diff --git a/tests/Templates/fixtures/php.output.html b/tests/GuidesExtension/Highlighter/fixtures/php.output.html similarity index 100% rename from tests/Templates/fixtures/php.output.html rename to tests/GuidesExtension/Highlighter/fixtures/php.output.html diff --git a/tests/Templates/fixtures/twig.input.txt b/tests/GuidesExtension/Highlighter/fixtures/twig.input.txt similarity index 100% rename from tests/Templates/fixtures/twig.input.txt rename to tests/GuidesExtension/Highlighter/fixtures/twig.input.txt diff --git a/tests/Templates/fixtures/twig.output.html b/tests/GuidesExtension/Highlighter/fixtures/twig.output.html similarity index 100% rename from tests/Templates/fixtures/twig.output.html rename to tests/GuidesExtension/Highlighter/fixtures/twig.output.html diff --git a/tests/HtmlIntegrationTest.php b/tests/HtmlIntegrationTest.php new file mode 100644 index 00000000..efb0e4ef --- /dev/null +++ b/tests/HtmlIntegrationTest.php @@ -0,0 +1,120 @@ +get(DocBuilder::class)->buildString(file_get_contents($sourceFile)); + $generated = new \DOMDocument(); + $generated->loadHTML($generatedContents, \LIBXML_NOERROR); + $generated->preserveWhiteSpace = false; + $generatedHtml = self::sanitizeHTML($generated->saveHTML()); + + $expected = new \DOMDocument(); + $expectedContents = "\n\n\n".$expectedContents."\n\n"; + $expected->loadHTML($expectedContents, \LIBXML_NOERROR); + $expected->preserveWhiteSpace = false; + $expectedHtml = self::sanitizeHTML($expected->saveHTML()); + + $this->assertEquals($expectedHtml, $generatedHtml); + } catch (ExpectationFailedException $e) { + if (false !== $skip) { + $this->markTestIncomplete($skip); + } + + throw $e; + } + + $this->assertFalse($skip, 'Test passes while marked as SKIP.'); + } + + public static function provideBlocks(): iterable + { + foreach ((new Finder())->files()->in(__DIR__.'/fixtures/source/blocks') as $file) { + yield $file->getRelativePathname() => [$file->getRealPath(), __DIR__.'/fixtures/expected/blocks/'.str_replace('.rst', '.html', $file->getRelativePathname())]; + } + } + + #[DataProvider('provideProjects')] + public function testProjects(string $directory) + { + $buildEnvironment = new DynamicBuildEnvironment(new LocalFilesystemAdapter(__DIR__.'/fixtures/source/'.$directory)); + + $kernel = DocsKernel::create([new TestExtension()]); + $kernel->get(DocBuilder::class)->build($buildEnvironment); + + $expectedDirectory = __DIR__.'/fixtures/expected/'.$directory; + $skip = false; + if (file_exists($expectedDirectory.'/skip')) { + $skip = file_get_contents($expectedDirectory.'/skip'); + } + + try { + foreach ((new Finder())->files()->notName('skip')->in($expectedDirectory) as $file) { + $expected = self::sanitizeHTML($file->getContents()); + $actual = self::sanitizeHTML($buildEnvironment->getOutputFilesystem()->read($file->getRelativePathname())); + + $this->assertEquals($expected, $actual, 'File: '.$file->getRelativePathname()); + } + } catch (ExpectationFailedException $e) { + if (false !== $skip) { + $this->markTestIncomplete($skip); + } + + throw $e; + } + + $this->assertFalse($skip, 'Test passes while marked as SKIP.'); + } + + public static function provideProjects(): iterable + { + foreach ((new Finder())->directories()->in(__DIR__.'/fixtures/source')->exclude(['build-pdf', 'json'])->depth(0) as $dir) { + if ('blocks' === $dir->getBasename()) { + continue; + } + + yield $dir->getBasename() => [$dir->getBasename()]; + } + } + + private static function sanitizeHTML(string $html): string + { + $html = implode("\n", array_map('trim', explode("\n", $html))); + $html = preg_replace('# +#', ' ', $html); + $html = preg_replace('#(? (?!\w)#', '>', $html); + $html = preg_replace('#\R+#', "\n", $html); + + $html = substr($html, strpos($html, '') + 6); + $html = substr($html, 0, strpos($html, '')); + + return trim($html); + } +} diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php deleted file mode 100644 index 07f37338..00000000 --- a/tests/IntegrationTest.php +++ /dev/null @@ -1,375 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder\Tests; - -use Doctrine\RST\Builder; -use Doctrine\RST\Configuration; -use Doctrine\RST\Parser; -use Gajus\Dindent\Indenter; -use Symfony\Component\DomCrawler\Crawler; -use Symfony\Component\Finder\Finder; -use SymfonyDocsBuilder\DocBuilder; -use SymfonyDocsBuilder\KernelFactory; -use SymfonyDocsBuilder\Renderers\TitleNodeRenderer; - -class IntegrationTest extends AbstractIntegrationTest -{ - /** - * @dataProvider integrationProvider - */ - public function testIntegration(string $folder) - { - $buildConfig = $this->createBuildConfig(sprintf('%s/fixtures/source/%s', __DIR__, $folder)); - $builder = new DocBuilder(); - $builder->build($buildConfig); - - $finder = new Finder(); - $finder->in(sprintf('%s/fixtures/expected/%s', __DIR__, $folder)) - ->files() - ->depth('>=0'); - - $indenter = $this->createIndenter(); - foreach ($finder as $expectedFile) { - $relativePath = $expectedFile->getRelativePathname(); - $actualFilename = $buildConfig->getOutputDir().'/'.$relativePath; - $this->assertFileExists($actualFilename); - - $this->assertSame( - // removes odd trailing space the indenter is adding - str_replace(" \n", "\n", $indenter->indent($expectedFile->getContents())), - str_replace(" \n", "\n", $indenter->indent(file_get_contents($actualFilename))), - sprintf('File %s is not equal', $relativePath) - ); - } - - foreach ($finder as $htmlFile) { - $relativePath = $htmlFile->getRelativePathname(); - $actualFilename = $buildConfig->getOutputDir().'/'.str_replace('.html', '.fjson', $relativePath); - $this->assertFileExists($actualFilename); - - $jsonData = json_decode(file_get_contents($actualFilename), true); - $crawler = new Crawler($htmlFile->getContents()); - - $this->assertSame( - str_replace(" \n", "\n", $indenter->indent($crawler->filter('body')->html())), - str_replace(" \n", "\n", $indenter->indent($jsonData['body'])) - ); - $this->assertSame($crawler->filter('h1')->first()->text(), $jsonData['title']); - } - } - - public function integrationProvider() - { - yield 'main' => [ - 'folder' => 'main', - ]; - - yield 'toctree' => [ - 'folder' => 'toctree', - ]; - - yield 'ref-reference' => [ - 'folder' => 'ref-reference', - ]; - - yield 'doc-reference' => [ - 'folder' => 'doc-reference', - ]; - } - - /** - * @dataProvider parserUnitBlockProvider - */ - public function testParseUnitBlock(string $blockName) - { - $configuration = new Configuration(); - $configuration->setCustomTemplateDirs([__DIR__.'/Templates']); - - $kernel = KernelFactory::createKernel($this->createBuildConfig(sprintf('%s/fixtures/source/blocks', __DIR__))); - // necessary because this initializes some listeners on the kernel - $builder = new Builder($kernel); - $parser = new Parser($kernel); - - $sourceFile = sprintf('%s/fixtures/source/blocks/%s.rst', __DIR__, $blockName); - - $actualHtml = $parser->parseFile($sourceFile)->renderDocument(); - $expectedHtml = file_get_contents(sprintf('%s/fixtures/expected/blocks/%s.html', __DIR__, $blockName)); - if (false === stripos($expectedHtml, ''.$expectedHtml.''; - } - - $actualCrawler = new Crawler($actualHtml); - $expectedCrawler = new Crawler($expectedHtml); - $indenter = $this->createIndenter(); - - $expected = trim($expectedCrawler->filter('body')->html()); - // you can add notes to a test file via - // we remove them here for comparing - $expected = preg_replace('/<\!\-\- REMOVE(.)+\-\->/', '', $expected); - - $this->assertSame( - $this->normalize($indenter->indent($expected)), - $this->normalize($indenter->indent(trim($actualCrawler->filter('body')->html()))) - ); - } - - public function parserUnitBlockProvider() - { - yield 'tables' => [ - 'blockName' => 'nodes/tables', - ]; - - yield 'literal' => [ - 'blockName' => 'nodes/literal', - ]; - - yield 'list' => [ - 'blockName' => 'nodes/list', - ]; - - yield 'figure' => [ - 'blockName' => 'nodes/figure', - ]; - - yield 'span-link' => [ - 'blockName' => 'nodes/span-link', - ]; - - yield 'text' => [ - 'blockName' => 'nodes/text', - ]; - - yield 'title' => [ - 'blockName' => 'nodes/title', - ]; - - yield 'caution' => [ - 'blockName' => 'directives/caution', - ]; - - yield 'note' => [ - 'blockName' => 'directives/note', - ]; - - yield 'admonition' => [ - 'blockName' => 'directives/admonition', - ]; - - yield 'danger' => [ - 'blockName' => 'directives/danger', - ]; - - yield 'note-code-block-nested' => [ - 'blockName' => 'directives/note-code-block-nested', - ]; - - yield 'screencast' => [ - 'blockName' => 'directives/screencast', - ]; - - yield 'seealso' => [ - 'blockName' => 'directives/seealso', - ]; - - yield 'tip' => [ - 'blockName' => 'directives/tip', - ]; - - yield 'topic' => [ - 'blockName' => 'directives/topic', - ]; - - yield 'best-practice' => [ - 'blockName' => 'directives/best-practice', - ]; - - yield 'deprecated' => [ - 'blockName' => 'directives/deprecated', - ]; - - yield 'versionadded' => [ - 'blockName' => 'directives/versionadded', - ]; - - yield 'class' => [ - 'blockName' => 'directives/class', - ]; - - yield 'configuration-block' => [ - 'blockName' => 'directives/configuration-block', - ]; - - yield 'sidebar' => [ - 'blockName' => 'directives/sidebar', - ]; - - yield 'sidebar-code-block-nested' => [ - 'blockName' => 'directives/sidebar-code-block-nested', - ]; - - yield 'tabs' => [ - 'blockName' => 'directives/tabs', - ]; - - yield 'class-reference' => [ - 'blockName' => 'references/class', - ]; - - yield 'namespace-reference' => [ - 'blockName' => 'references/namespace', - ]; - - yield 'method-reference' => [ - 'blockName' => 'references/method', - ]; - - yield 'php-class-reference' => [ - 'blockName' => 'references/php-class', - ]; - - yield 'php-function-reference' => [ - 'blockName' => 'references/php-function', - ]; - - yield 'php-method-reference' => [ - 'blockName' => 'references/php-method', - ]; - - yield 'reference-and-code' => [ - 'blockName' => 'references/reference-and-code', - ]; - - yield 'code-block-caption' => [ - 'blockName' => 'code-blocks/code-caption', - ]; - - yield 'code-block-php' => [ - 'blockName' => 'code-blocks/php', - ]; - - yield 'code-block-html' => [ - 'blockName' => 'code-blocks/html', - ]; - - yield 'code-block-twig' => [ - 'blockName' => 'code-blocks/twig', - ]; - - yield 'code-block-html-twig' => [ - 'blockName' => 'code-blocks/html-twig', - ]; - - yield 'code-block-xml' => [ - 'blockName' => 'code-blocks/xml', - ]; - - yield 'code-block-yaml' => [ - 'blockName' => 'code-blocks/yaml', - ]; - - yield 'code-block-ini' => [ - 'blockName' => 'code-blocks/ini', - ]; - - yield 'code-block-bash' => [ - 'blockName' => 'code-blocks/bash', - ]; - - yield 'code-block-diff' => [ - 'blockName' => 'code-blocks/diff', - ]; - - yield 'code-block-html-php' => [ - 'blockName' => 'code-blocks/html-php', - ]; - - yield 'code-block-php-annotations' => [ - 'blockName' => 'code-blocks/php-annotations', - ]; - - yield 'code-block-php-attributes' => [ - 'blockName' => 'code-blocks/php-attributes', - ]; - - yield 'code-block-php-nested-comments' => [ - 'blockName' => 'code-blocks/php-nested-comments', - ]; - - yield 'code-block-text' => [ - 'blockName' => 'code-blocks/text', - ]; - - yield 'code-block-terminal' => [ - 'blockName' => 'code-blocks/terminal', - ]; - } - - public function testParseString() - { - $rstString = <<`_ consequat. -Duis aute irure dolor in reprehenderit in voluptate `velit esse`_. - -Cillum dolore eu fugiat nulla pariatur --------------------------------------- - -Excepteur sint occaecat cupidatat non proident, sunt in -culpa qui *officia deserunt* mollit anim id est laborum. - -.. _`velit esse`: https://github.com -RST; - - $htmlString = << -

    Lorem ipsum dolor sit amet

    -

    Consectetur adipisicing elit, sed do eiusmod -tempor incididunt ut labore et dolore magna aliqua.

    -
      -
    • Ut enim ad minim veniam
    • -
    • Quis nostrud exercitation
    • -
    • Ullamco laboris nisi ut
    • -
    -

    Aliquip ex ea commodo consequat. -Duis aute irure dolor in reprehenderit in voluptate velit esse.

    -
    -

    Cillum dolore eu fugiat nulla pariatur

    -

    Excepteur sint occaecat cupidatat non proident, sunt in -culpa qui officia deserunt mollit anim id est laborum.

    -
    - -HTML; - - $this->assertSame($htmlString, (new DocBuilder())->buildString($rstString)->getStringResult()); - } - - private function normalize(string $str): string - { - return preg_replace('/\s+$/m', '', $str); - } - - private function createIndenter(): Indenter - { - $indenter = new Indenter(); - // indent spans - easier to debug failures - $indenter->setElementType('span', Indenter::ELEMENT_TYPE_BLOCK); - - return $indenter; - } -} diff --git a/tests/JsonIntegrationTest.php b/tests/JsonIntegrationTest.php index b9ab0924..802edbc9 100644 --- a/tests/JsonIntegrationTest.php +++ b/tests/JsonIntegrationTest.php @@ -1,41 +1,40 @@ - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ +namespace SymfonyTools\DocsBuilder\GuidesExtension\Tests; -namespace SymfonyDocsBuilder\Tests; +use League\Flysystem\Local\LocalFilesystemAdapter; +use PHPUnit\Framework\Attributes\DataProvider; +use PHPUnit\Framework\TestCase; +use SymfonyTools\DocsBuilder\GuidesExtension\Build\BuildConfig; +use SymfonyTools\DocsBuilder\GuidesExtension\Build\DynamicBuildEnvironment; +use SymfonyTools\DocsBuilder\GuidesExtension\DocBuilder; +use SymfonyTools\DocsBuilder\GuidesExtension\DocsKernel; +use phpDocumentor\Guides\DependencyInjection\TestExtension; -use SymfonyDocsBuilder\DocBuilder; -use SymfonyDocsBuilder\Renderers\TitleNodeRenderer; - -class JsonIntegrationTest extends AbstractIntegrationTest +class JsonIntegrationTest extends TestCase { - /** - * @dataProvider getJsonTests - */ + #[DataProvider('getJsonTests')] public function testJsonGeneration(string $filename, array $expectedData) { - $buildConfig = $this->createBuildConfig(__DIR__ . '/fixtures/source/json'); - $builder = new DocBuilder(); - $buildResult = $builder->build($buildConfig); - $fJsons = $buildResult->getJsonResults(); + $kernel = DocsKernel::create([new TestExtension()]); + + $kernel->get(BuildConfig::class)->outputFormat = 'json'; - $actualFileData = $fJsons[$filename]; + $buildEnvironment = new DynamicBuildEnvironment(new LocalFilesystemAdapter(__DIR__.'/fixtures/source/json')); + $kernel->get(DocBuilder::class)->build($buildEnvironment); + + $actualFileData = json_decode($buildEnvironment->getOutputFilesystem()->read($filename.'.fjson'), true); + $this->assertSame($expectedData, array_intersect_key($actualFileData, $expectedData), sprintf('Invalid data in file "%s"', $filename)); foreach ($expectedData as $key => $expectedKeyData) { $this->assertArrayHasKey($key, $actualFileData, sprintf('Missing key "%s" in file "%s"', $key, $filename)); - $this->assertSame($expectedKeyData, $actualFileData[$key], sprintf('Invalid data for key "%s" in file "%s"', $key, $filename)); } } - public function getJsonTests() + public static function getJsonTests() { yield 'index' => [ - 'file' => 'index', - 'data' => [ + 'filename' => 'index', + 'expectedData' => [ 'parents' => [], 'prev' => null, 'next' => [ @@ -47,8 +46,8 @@ public function getJsonTests() ]; yield 'dashboards' => [ - 'file' => 'dashboards', - 'data' => [ + 'filename' => 'dashboards', + 'expectedData' => [ 'parents' => [], 'prev' => [ 'title' => 'JSON Generation Test', @@ -63,8 +62,8 @@ public function getJsonTests() ]; yield 'design' => [ - 'file' => 'design', - 'data' => [ + 'filename' => 'design', + 'expectedData' => [ 'parents' => [], 'prev' => [ 'title' => 'Dashboards', @@ -128,54 +127,41 @@ public function getJsonTests() ]; yield 'crud' => [ - 'file' => 'crud', - 'data' => [ - 'parents' => [ - [ - 'title' => 'Design', - 'link' => 'design.html', - ], - ], + 'filename' => 'crud', + 'expectedData' => [ + 'parents' => [], 'prev' => [ 'title' => 'Design', 'link' => 'design.html', ], 'next' => [ - 'title' => 'Design Sub-Page', - 'link' => 'design/sub-page.html', + 'title' => 'Fields', + 'link' => 'fields.html', ], 'title' => 'CRUD', ] ]; yield 'design/sub-page' => [ - 'file' => 'design/sub-page', - 'data' => [ + 'filename' => 'design/sub-page', + 'expectedData' => [ 'parents' => [ [ 'title' => 'Design', 'link' => '../design.html', ], ], - 'prev' => [ - 'title' => 'CRUD', - 'link' => '../crud.html', - ], - 'next' => [ - 'title' => 'Fields', - 'link' => '../fields.html', - ], 'title' => 'Design Sub-Page', ] ]; yield 'fields' => [ - 'file' => 'fields', - 'data' => [ + 'filename' => 'fields', + 'expectedData' => [ 'parents' => [], 'prev' => [ - 'title' => 'Design Sub-Page', - 'link' => 'design/sub-page.html', + 'title' => 'CRUD', + 'link' => 'crud.html', ], 'next' => null, 'title' => 'Fields', @@ -183,8 +169,8 @@ public function getJsonTests() ]; yield 'orphan' => [ - 'file' => 'orphan', - 'data' => [ + 'filename' => 'orphan', + 'expectedData' => [ 'parents' => [], 'prev' => null, 'next' => null, diff --git a/tests/Renderers/CodeNodeRendererTest.php b/tests/Renderers/CodeNodeRendererTest.php index 1606463b..b79274d6 100644 --- a/tests/Renderers/CodeNodeRendererTest.php +++ b/tests/Renderers/CodeNodeRendererTest.php @@ -14,6 +14,11 @@ class CodeNodeRendererTest extends TestCase { + protected function setUp(): void + { + $this->markTestSkipped('Old Doctrine RST based tests'); + } + /** * @dataProvider getSupportedLanguageTests */ diff --git a/tests/Templates/HighlightJsIntegrationTest.php b/tests/Templates/HighlightJsIntegrationTest.php deleted file mode 100644 index 0e25a815..00000000 --- a/tests/Templates/HighlightJsIntegrationTest.php +++ /dev/null @@ -1,51 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder\Tests\Templates; - -use Highlight\Highlighter; -use PHPUnit\Framework\TestCase; - -class HighlightJsIntegrationTest extends TestCase -{ - public static function setUpBeforeClass(): void - { - Highlighter::registerLanguage('php', __DIR__.'/../../src/Templates/highlight.php/php.json', true); - Highlighter::registerLanguage('twig', __DIR__.'/../../src/Templates/highlight.php/twig.json', true); - } - - /** - * @dataProvider getRenderingTests - */ - public function testCustomRendering(string $language, string $inputFile, string $outputFile) - { - $highlighter = new Highlighter(); - $actual = $highlighter->highlight( - $language, - file_get_contents(__DIR__.'/fixtures/'.$inputFile) - )->value; - - $this->assertSame(file_get_contents(__DIR__.'/fixtures/'.$outputFile), $actual); - } - - public function getRenderingTests() - { - yield 'php' => [ - 'php', - 'php.input.txt', - 'php.output.html', - ]; - - yield 'twig' => [ - 'twig', - 'twig.input.txt', - 'twig.output.html', - ]; - } -} diff --git a/tests/fixtures/expected/blocks/code-blocks/code-caption.html b/tests/fixtures/expected/blocks/code-blocks/code-caption.html index a4eaf9d7..24d5b798 100644 --- a/tests/fixtures/expected/blocks/code-blocks/code-caption.html +++ b/tests/fixtures/expected/blocks/code-blocks/code-caption.html @@ -2,14 +2,7 @@
    config/routes.php
    1
    -
    -       
    -           
    -               $
    -               foo
    -           =
    -           'bar';
    -   
    +
    $foo = 'bar';
    @@ -17,28 +10,18 @@
    config/routes.php
    1
    -
    -       
    -           
    -               $
    -               foo
    -           =
    -           'bar';
    -   
    +
    $foo = 'bar';
    -
    -
    -
    1
    +
    +
    patch_file
    +
    +
    1
     2
     3
    -
    -       
    -           --- a/src/Controller/DefaultController.php
    +       
    --- a/src/Controller/DefaultController.php
                +++ b/src/Controller/DefaultController.php
    -           @@ -2,7 +2,9 @@
    -       
    -   
    + @@ -2,7 +2,9 @@
    diff --git a/tests/fixtures/expected/blocks/code-blocks/diff.html b/tests/fixtures/expected/blocks/code-blocks/diff.html index 0a020cf4..f00f497a 100644 --- a/tests/fixtures/expected/blocks/code-blocks/diff.html +++ b/tests/fixtures/expected/blocks/code-blocks/diff.html @@ -5,15 +5,11 @@ 3 4 5
    -
    -            
    -                + Added line
    -                - Removed line
    -                Normal line
    -- Removed line
    -                + Added line
    -            
    -        
    +
    + Added line
    +            - Removed line
    +            Normal line
    +            - Removed line
    +            + Added line
    @@ -25,15 +21,11 @@ 4 5 6 -
    -            
    -                Normal line
    -+ Added line
    -                - Removed line
    -                Normal line
    -- Removed line
    -                + Added line
    -            
    -        
    +
     Normal line
    +            + Added line
    +            - Removed line
    +            Normal line
    +            - Removed line
    +            + Added line
    diff --git a/tests/fixtures/expected/blocks/code-blocks/html-php.html b/tests/fixtures/expected/blocks/code-blocks/html-php.html index d77a92ef..e7b564e0 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html-php.html +++ b/tests/fixtures/expected/blocks/code-blocks/html-php.html @@ -1,4 +1,4 @@ -
    +
    1
     2
    @@ -23,6 +23,5 @@
         <body>
             <?php echo 'body'; ?>
         </body>
    -</html>
    -
    +</html>
    diff --git a/tests/fixtures/expected/blocks/code-blocks/html-twig.html b/tests/fixtures/expected/blocks/code-blocks/html-twig.html index dbe31018..6e17a7d9 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html-twig.html +++ b/tests/fixtures/expected/blocks/code-blocks/html-twig.html @@ -3,7 +3,6 @@
    1
     2
    {# some code #}
    -<!-- some code -->
    -
    +<!-- some code -->
    diff --git a/tests/fixtures/expected/blocks/code-blocks/html.html b/tests/fixtures/expected/blocks/code-blocks/html.html index f35e7e3e..09d9b41d 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html.html +++ b/tests/fixtures/expected/blocks/code-blocks/html.html @@ -1,6 +1,5 @@ -
    +
    1
    -
    <!-- some code -->
    -
    +
    <!-- some code -->
    diff --git a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html index 1d6bce0b..d6f7135b 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html @@ -65,97 +65,70 @@ 63 64 65 -
    -            
    -                // src/SomePath/SomeClass.php
    +
    // src/SomePath/SomeClass.php
     namespace App\SomePath;
    -                useSymfony\Component\Validator\Constraints as Assert;
    -                
    -                    class
    -                    SomeClass
    -                
    -                {
    -                #[AttributeName]
    -                private
    -                
    -                    $ property1
    -                ;
    -                #[AttributeName()]
    -                private
    -                
    -                    $ property2
    -                ;
    -                #[AttributeName('value')]
    -                private
    -                
    -                    $ property3
    -                ;
    -                #[AttributeName('value', option: 'value')]
    -                private
    -                
    -                    $ property4
    -                ;
    -                #[AttributeName(['value' => 'value'])]
    -                private
    -                
    -                    $ property5
    -                ;
    -                #[AttributeName(
    -                    'value',
    -                    option: 'value'
    -                )]
    -                private
    -                
    -                    $ property6
    -                ;
    -                #[Assert\AttributeName('value')]
    -                private
    -                
    -                    $ property7
    -                ;
    -                #[Assert\AttributeName(
    -                    'value',
    -                    option: 'value'
    -                )]
    -                private
    -                
    -                    $ property8
    -                ;
    -                #[Route('/blog/{page<\d+>}', name: 'blog_list')]
    -                private
    -                
    -                $ property9
    -                ;
    -                #[Assert\GreaterThanOrEqual(
    -                    value: 18,
    -                )]
    -                private
    -                
    -                $ property10
    -                ;
    -                #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    -                private
    -                
    -                $ property11
    -                ;
    -                #[Assert\AtLeastOneOf([
    -                    new Assert\Regex('/#/'),
    -                    new Assert\Length(min: 10),
    -                ])]
    -                private
    -                
    -                $ property12
    -                ;
    -                public function __construct(
    -                        #[TaggedIterator('app.handlers')]
    -                        iterable $handlers,
    -                ){
    -                }
     
    -                #[AsController]
    -                public functionsomeAction(#[CurrentUser, AttributeName('value')] User $user)
    -                {
    -                }
    +useSymfony\Component\Validator\Constraints as Assert;
    +
    +classSomeClass
    +{
    +    #[AttributeName]
    +    private $property1;
    +
    +    #[AttributeName()]
    +    private $property2;
    +
    +    #[AttributeName('value')]
    +    private $property3;
    +
    +    #[AttributeName('value', option: 'value')]
    +    private $property4;
    +
    +    #[AttributeName(['value' => 'value'])]
    +    private $property5;
    +
    +    #[AttributeName(
    +        'value',
    +        option: 'value'
    +    )]
    +    private $property6;
    +
    +    #[Assert\AttributeName('value')]
    +    private $property7;
    +
    +    #[Assert\AttributeName(
    +        'value',
    +        option: 'value'
    +    )]
    +    private $property8;
    +
    +    #[Route('/blog/{page<\d+>}', name: 'blog_list')]
    +    private $property9;
    +
    +    #[Assert\GreaterThanOrEqual(
    +        value: 18,
    +    )]
    +    private $property10;
    +
    +    #[ORM\CustomIdGenerator(class: 'doctrine.uuid_generator')]
    +    private $property11;
    +
    +    #[Assert\AtLeastOneOf([
    +        new Assert\Regex('/#/'),
    +        new Assert\Length(min: 10),
    +    ])]
    +    private $property12;
    +
    +    public function __construct(
    +            #[TaggedIterator('app.handlers')]
    +            iterable $handlers,
    +    ){
    +    }
    +
    +    #[AsController]
    +    public functionsomeAction(#[CurrentUser, AttributeName('value')] User $user)
    +    {
    +    }
     }
    diff --git a/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html b/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html index e1f7078f..ee55b522 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html @@ -11,62 +11,15 @@ 8 9 10 -
    -            
    -                use
    -                Symfony
    -                \
    -                Component
    -                \
    -                Messenger
    -                \
    -                MessageBusInterface
    -                ;
    -                use
    -                Symfony
    -                \
    -                Component
    -                \
    -                Messenger
    -                \
    -                Stamp
    -                \
    -                DelayStamp
    -                ;
    -                public
    -                
    -                    function
    -                    index
    -                    
    -                        (MessageBusInterface
    -                        
    -                            $
    -                            bus
    -                        
    -                        )
    -                    
    -                
    -                {
    -                
    -                    $
    -                    bus
    -                
    -                ->
    -                dispatch
    -                (
    -                new
    -                SmsNotification
    -                (
    -                '...'
    -                ), [
    -                // wait 5 seconds before processing
    -                new
    -                DelayStamp
    -                (
    -                5000
    -                ), ]);
    -}
    -            
    -        
    +
    use Symfony\Component\Messenger\MessageBusInterface;
    +use Symfony\Component\Messenger\Stamp\DelayStamp;
    +
    +public function index(MessageBusInterface $bus)
    +{
    +    $bus->dispatch(new SmsNotification('...'), [
    +        // wait 5 seconds before processing
    +        new DelayStamp(5000),
    +    ]);
    +}
    diff --git a/tests/fixtures/expected/blocks/code-blocks/php.html b/tests/fixtures/expected/blocks/code-blocks/php.html index 163de59a..71e79f0f 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php.html +++ b/tests/fixtures/expected/blocks/code-blocks/php.html @@ -12,9 +12,9 @@
    // config/routes.php
     namespace Symfony\Component\Routing\Loader\Configurator;
     
    -    use App\Controller\CompanyController;
    +use App\Controller\CompanyController;
     
    -return static function (RoutingConfigurator $routes): void {
    +return static function (RoutingConfigurator $routes):void {
         $routes->add('about_us', ['nl' => '/over-ons', 'en' => '/about-us'])
             ->controller(CompanyController::class.'::about');
     };
    @@ -38,15 +38,14 @@ 14 15 16 -
    
    -enum TextAlign: string implements TranslatableInterface
    -{
    +        
    enum TextAlign: string implements TranslatableInterface
    +{
         case Left = 'Left aligned';
         case Center = 'Center aligned';
         case Right = 'Right aligned';
     
    -    public function trans(TranslatorInterface $translator, ?string $locale = null): string
    -    {
    +    public function trans(TranslatorInterface $translator, ?string $locale = null): string
    +    {
             // Translate enum using custom labels
             return match ($this) {
                 self::Left => $translator->trans('text_align.left.label', locale: $locale),
    @@ -54,9 +53,7 @@
                 self::Right => $translator->trans('text_align.right.label', locale: $locale),
             };
         }
    -}
    -            
    -        
    +}
    @@ -77,9 +74,8 @@ 14 15 16 -
    
    -public function getUserBadgeFrom(string $accessToken): UserBadge
    -{
    +        
    public function getUserBadgeFrom(string $accessToken): UserBadge
    +{
         // get the data from the token
         $payload = ...;
     
    @@ -93,8 +89,7 @@
             $payload->getUserId(),
             $this->loadUser(...)
         );
    -}
    -        
    +}
    @@ -103,11 +98,9 @@ 2 3 4 -
    
    -public function __construct(
    +        
    public function __construct(
         private string $username
     ) {
    -}
    -        
    +}
    diff --git a/tests/fixtures/expected/blocks/code-blocks/terminal.html b/tests/fixtures/expected/blocks/code-blocks/terminal.html index 4567fdb7..548e3078 100644 --- a/tests/fixtures/expected/blocks/code-blocks/terminal.html +++ b/tests/fixtures/expected/blocks/code-blocks/terminal.html @@ -1,22 +1,15 @@
    1
    -
    git clone git@github.com:symfony/symfony.git
    +
    git clone git@github.com:symfony/symfony.git
    1
     2
    -
    -           
    -               $ cowsay
    -               'eat more chicken'
    -               $ cowsay
    -               'mmmm'
    -           
    -       
    +
    $ cowsay 'eat more chicken'
    +               $ cowsay 'mmmm'
    @@ -25,24 +18,14 @@
    1
     2
     3
    -
    -           
    -               C:\> CIV
    -               # Civilization for DOS - my first computer game!
    -           
    -       
    +
    C:\> CIV
    +               # Civilization for DOS - my first computer game!
    1
    -
    -            
    -                $ 
    -                git branch -D sessions-in-db ||
    -               true
    -           
    -        
    +
    $ git branch -D sessions-in-db || true
    diff --git a/tests/fixtures/expected/blocks/code-blocks/twig.html b/tests/fixtures/expected/blocks/code-blocks/twig.html index 83c841cf..31404752 100644 --- a/tests/fixtures/expected/blocks/code-blocks/twig.html +++ b/tests/fixtures/expected/blocks/code-blocks/twig.html @@ -9,22 +9,14 @@ 7 8 9 -
    -            
    -                {# some code #}
    -                
    -                
    -                    {%
    -                        set some_var = 'some value' # some inline comment
    -                    %}
    -                
    -                
    -                {{
    -                    # another inline comment
    -                    'Lorem Ipsum'|uppercase
    -                    # final inline comment
    -                }}
    -            
    -        
    +
    {# some code #}
    +{%
    +    set some_var = 'some value' # some inline comment
    +%}
    +{{
    +    # another inline comment
    +    'Lorem Ipsum'|uppercase
    +    # final inline comment
    +}}
    diff --git a/tests/fixtures/expected/blocks/code-blocks/xml.html b/tests/fixtures/expected/blocks/code-blocks/xml.html index 47722577..02ffe4e8 100644 --- a/tests/fixtures/expected/blocks/code-blocks/xml.html +++ b/tests/fixtures/expected/blocks/code-blocks/xml.html @@ -1,7 +1,6 @@
    1
    -
    <!-- some code -->
    -
    +
    <!-- some code -->
    diff --git a/tests/fixtures/expected/blocks/code-blocks/yaml.html b/tests/fixtures/expected/blocks/code-blocks/yaml.html index 2de3b7b2..02e1a86f 100644 --- a/tests/fixtures/expected/blocks/code-blocks/yaml.html +++ b/tests/fixtures/expected/blocks/code-blocks/yaml.html @@ -10,27 +10,15 @@ 8 9 10 -
    -            
    -                # some code
    -                parameters:
    -                credit_card_number:
    -                1234_5678_9012_3456
    -                long_number:
    -                10_000_000_000
    -                pi:
    -                3.14159_26535_89793
    -                hex_words:
    -                0x_CAFE_F00D
    -                canonical:
    -                2001-12-15T02:59:43.1Z
    -                iso8601:
    -                2001-12-14t21:59:43.10-05:00
    -                spaced:
    -                2001-12-14 21:59:43.10 -5
    -                date:
    -                2002-12-14
    -            
    -        
    +
    # some code
    +parameters:
    +    credit_card_number: 1234_5678_9012_3456
    +    long_number: 10_000_000_000
    +    pi: 3.14159_26535_89793
    +    hex_words: 0x_CAFE_F00D
    +    canonical: 2001-12-15T02:59:43.1Z
    +    iso8601: 2001-12-14t21:59:43.10-05:00
    +    spaced: 2001-12-14 21:59:43.10 -5
    +    date: 2002-12-14
    diff --git a/tests/fixtures/expected/blocks/directives/admonition.html b/tests/fixtures/expected/blocks/directives/admonition.html index f8f22804..ebdef057 100644 --- a/tests/fixtures/expected/blocks/directives/admonition.html +++ b/tests/fixtures/expected/blocks/directives/admonition.html @@ -1,5 +1,6 @@ -
    +

    Some Admonition -

    Do you prefer admonitions? Well then... enjoy this one!

    +

    +

    Do you prefer admonitions? Well then... enjoy this one!

    diff --git a/tests/fixtures/expected/blocks/directives/best-practice.html b/tests/fixtures/expected/blocks/directives/best-practice.html index a23604d2..cb2bfdb8 100644 --- a/tests/fixtures/expected/blocks/directives/best-practice.html +++ b/tests/fixtures/expected/blocks/directives/best-practice.html @@ -1,5 +1,6 @@ -
    +

    Best Practice -

    Use the bcrypt encoder for hashing your users' passwords.

    +

    +

    Use the bcrypt encoder for hashing your users' passwords.

    diff --git a/tests/fixtures/expected/blocks/directives/caution.html b/tests/fixtures/expected/blocks/directives/caution.html index 717c09f2..09712a67 100644 --- a/tests/fixtures/expected/blocks/directives/caution.html +++ b/tests/fixtures/expected/blocks/directives/caution.html @@ -1,6 +1,7 @@ -
    +

    Caution -

    Using too many sidebars or caution directives can be distracting!

    +

    +

    Using too many sidebars or caution directives can be distracting!

    diff --git a/tests/fixtures/expected/blocks/directives/class.html b/tests/fixtures/expected/blocks/directives/class.html index 22c16352..9d8466c9 100644 --- a/tests/fixtures/expected/blocks/directives/class.html +++ b/tests/fixtures/expected/blocks/directives/class.html @@ -1,6 +1,7 @@ -
    • list-item-1
    • -
    • list-item-2
    • -
    • list-item-3
    • +
        +
      • list-item-1
      • +
      • list-item-2
      • +
      • list-item-3

      some text

      diff --git a/tests/fixtures/expected/blocks/directives/configuration-block.html b/tests/fixtures/expected/blocks/directives/configuration-block.html index 4b940ce5..aa101992 100644 --- a/tests/fixtures/expected/blocks/directives/configuration-block.html +++ b/tests/fixtures/expected/blocks/directives/configuration-block.html @@ -1,10 +1,14 @@
      - - + +
      -
      +
      1
      @@ -13,7 +17,7 @@
      -