From 2c8acf813508a532b0e8878ed3ae8d5ed5f6bf43 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Sun, 1 Mar 2026 10:45:02 +0100 Subject: [PATCH 1/6] Migration --- README.md | 217 ++++- composer.json | 23 +- src/BuildConfig.php | 3 +- src/BuildResult.php | 50 +- src/Command/BuildDocsCommand.php | 81 +- src/Compiler/CopyImagesTransformer.php | 77 ++ src/Compiler/UrlNodeTransformer.php | 116 +++ src/Directive/AbstractAdmonitionDirective.php | 43 +- src/Directive/AdmonitionDirective.php | 34 +- src/Directive/AttentionDirective.php | 7 +- src/Directive/BestPracticeDirective.php | 7 +- src/Directive/CautionDirective.php | 7 +- src/Directive/CodeBlockDirective.php | 75 +- src/Directive/ConfigurationBlockDirective.php | 76 +- src/Directive/DangerDirective.php | 7 +- src/Directive/DeprecatedDirective.php | 26 +- src/Directive/ErrorDirective.php | 7 +- src/Directive/FigureDirective.php | 72 +- src/Directive/GlossaryDirective.php | 8 +- src/Directive/HintDirective.php | 7 +- src/Directive/ImportantDirective.php | 7 +- src/Directive/IndexDirective.php | 8 +- src/Directive/NoteDirective.php | 7 +- src/Directive/RoleDirective.php | 8 +- src/Directive/RstClassDirective.php | 84 +- src/Directive/ScreencastDirective.php | 7 +- src/Directive/SeeAlsoDirective.php | 7 +- src/Directive/SidebarDirective.php | 28 +- src/Directive/TabDirective.php | 20 +- src/Directive/TabsDirective.php | 47 +- src/Directive/TipDirective.php | 7 +- src/Directive/TopicDirective.php | 26 +- src/Directive/VersionAddedDirective.php | 26 +- src/Directive/WarningDirective.php | 7 +- src/DocBuilder.php | 67 +- src/DocsKernel.php | 69 +- src/Generator/HtmlForPdfGenerator.php | 26 +- src/Generator/JsonGenerator.php | 51 +- src/GuidesContainerFactory.php | 252 ++++++ src/KernelFactory.php | 116 +-- src/Listener/AdmonitionListener.php | 4 +- src/Listener/AssetsCopyListener.php | 9 +- src/Listener/BuildProgressListener.php | 58 +- src/Listener/CopyImagesListener.php | 63 -- src/Listener/DuplicatedHeaderIdListener.php | 4 +- src/Node/TabNode.php | 22 +- src/Node/TabsNode.php | 33 + src/Reference/ClassReference.php | 37 +- src/Reference/DeciderReference.php | 26 +- src/Reference/LeaderReference.php | 26 +- src/Reference/MergerReference.php | 26 +- src/Reference/MethodReference.php | 37 +- src/Reference/NamespaceReference.php | 36 +- src/Reference/PhpClassReference.php | 36 +- src/Reference/PhpFunctionReference.php | 36 +- src/Reference/PhpMethodReference.php | 36 +- src/Reference/TermReference.php | 26 +- src/Renderers/CodeNodeRenderer.php | 47 +- src/Renderers/SpanNodeRenderer.php | 168 +--- src/Renderers/TabsNodeRenderer.php | 73 ++ src/Renderers/TitleNodeRenderer.php | 67 +- src/SymfonyHTMLFormat.php | 89 +-- .../default/html/directives/tabs.html.twig | 17 + .../default/html/header-title.html.twig | 2 +- .../default/html/structure/layout.html.twig | 14 + src/Templates/rtd/html/header-title.html.twig | 2 +- tests/AbstractIntegrationTest.php | 1 - tests/IntegrationTest.php | 78 +- tests/JsonIntegrationTest.php | 32 +- .../expected/blocks/code-blocks/bash.html | 4 +- .../blocks/code-blocks/code-caption.html | 54 +- .../expected/blocks/code-blocks/diff.html | 31 +- .../expected/blocks/code-blocks/html-php.html | 6 +- .../blocks/code-blocks/html-twig.html | 7 +- .../expected/blocks/code-blocks/html.html | 6 +- .../expected/blocks/code-blocks/ini.html | 4 +- .../blocks/code-blocks/php-annotations.html | 4 +- .../blocks/code-blocks/php-attributes.html | 155 ++-- .../code-blocks/php-nested-comments.html | 72 +- .../expected/blocks/code-blocks/php.html | 41 +- .../expected/blocks/code-blocks/terminal.html | 50 +- .../expected/blocks/code-blocks/text.html | 4 +- .../expected/blocks/code-blocks/twig.html | 30 +- .../expected/blocks/code-blocks/xml.html | 7 +- .../expected/blocks/code-blocks/yaml.html | 36 +- .../blocks/directives/admonition.html | 10 +- .../blocks/directives/best-practice.html | 10 +- .../expected/blocks/directives/caution.html | 11 +- .../expected/blocks/directives/class.html | 10 +- .../directives/configuration-block.html | 37 +- .../expected/blocks/directives/danger.html | 11 +- .../blocks/directives/deprecated.html | 12 +- .../directives/note-code-block-nested.html | 15 +- .../expected/blocks/directives/note.html | 11 +- .../blocks/directives/screencast.html | 26 +- .../expected/blocks/directives/seealso.html | 12 +- .../directives/sidebar-code-block-nested.html | 16 +- .../expected/blocks/directives/sidebar.html | 17 +- .../expected/blocks/directives/tabs.html | 22 +- .../expected/blocks/directives/tip.html | 11 +- .../expected/blocks/directives/topic.html | 7 +- .../blocks/directives/versionadded.html | 12 +- .../expected/blocks/nodes/figure.html | 38 +- .../fixtures/expected/blocks/nodes/list.html | 11 +- .../expected/blocks/nodes/literal.html | 10 +- .../expected/blocks/nodes/span-link.html | 5 +- .../expected/blocks/nodes/tables.html | 244 +++--- .../fixtures/expected/blocks/nodes/text.html | 7 +- .../fixtures/expected/blocks/nodes/title.html | 28 +- .../expected/blocks/references/class.html | 11 +- .../expected/blocks/references/method.html | 2 +- .../expected/blocks/references/namespace.html | 2 +- .../expected/blocks/references/php-class.html | 6 +- .../blocks/references/php-function.html | 2 +- .../blocks/references/php-method.html | 6 +- .../blocks/references/reference-and-code.html | 8 +- tests/fixtures/expected/build-pdf/book.html | 49 +- .../fixtures/expected/doc-reference/file.html | 12 + .../expected/doc-reference/index.html | 23 +- tests/fixtures/expected/main/datetime.html | 749 ++++++++---------- .../expected/main/form/form_type.html | 28 +- tests/fixtures/expected/main/index.html | 116 ++- .../fixtures/expected/ref-reference/file.html | 13 + .../expected/ref-reference/index.html | 23 +- .../toctree/directory/another_file.html | 19 + tests/fixtures/expected/toctree/file.html | 15 + tests/fixtures/expected/toctree/index.html | 83 +- 127 files changed, 2787 insertions(+), 2402 deletions(-) create mode 100644 src/Compiler/CopyImagesTransformer.php create mode 100644 src/Compiler/UrlNodeTransformer.php create mode 100644 src/GuidesContainerFactory.php delete mode 100644 src/Listener/CopyImagesListener.php create mode 100644 src/Node/TabsNode.php create mode 100644 src/Renderers/TabsNodeRenderer.php create mode 100644 src/Templates/default/html/directives/tabs.html.twig create mode 100644 src/Templates/default/html/structure/layout.html.twig create mode 100644 tests/fixtures/expected/doc-reference/file.html create mode 100644 tests/fixtures/expected/ref-reference/file.html create mode 100644 tests/fixtures/expected/toctree/directory/another_file.html create mode 100644 tests/fixtures/expected/toctree/file.html diff --git a/README.md b/README.md index 1ab6f116..b6f93fdb 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Symfony Docs Builder ==================== -This project is used to build the [Symfony Documentation][1] both in +This project is used to build the [Symfony Documentation][1] both on https://symfony.com and the Continuous Integration service used in the Symfony Docs repository. @@ -11,8 +11,217 @@ Symfony projects, this repository doesn't provide any support and it doesn't guarantee backward compatibility either. Any or the entire project can change, or even disappear, at any moment without prior notice. -If you are looking for a project to build your reStructuredText documentation -in PHP, use [Doctrine RST parser][2] (which is also used by this project internally). +Internally, this project uses the [phpDocumentor Guides][2] library to parse +reStructuredText files and render them as HTML. + +Usage +----- + +### CLI Usage + +The `build:docs` command builds a directory of `.rst` files into HTML: + +```bash +bin/docs-builder build:docs /path/to/rst-docs /path/to/output --symfony-version=7.2 +``` + +**Arguments:** + +| Argument | Description | Default | +|----------------|---------------------------------|--------------------------| +| `source-dir` | Directory containing `.rst` files | Current working directory | +| `output-dir` | Directory for HTML output | `{source-dir}/_output` | + +**Options:** + +| Option | Description | +|----------------------|----------------------------------------------------------| +| `--parse-sub-path` | Build only a subdirectory (generates single HTML for PDF) | +| `--output-json` | Generate `.fjson` metadata files (Sphinx compatible) | +| `--disable-cache` | Clear cache before building | +| `--save-errors` | Save error log to a file | +| `--no-theme` | Use `default` theme instead of `rtd` | +| `--fail-on-errors` | Return exit code 1 if there are warnings/errors | + +The `--symfony-version` option (or the `SYMFONY_VERSION` environment variable) is required. + +If a `docs.json` file exists in the source directory, it is read automatically: + +```json +{ + "exclude": ["_build", "vendor"] +} +``` + +### Library Usage + +You can use the builder programmatically without invoking a console command. +The two main classes are `DocBuilder` (the builder) and `BuildConfig` (the +configuration): + +#### Build a Directory + +```php +use SymfonyDocsBuilder\DocBuilder; +use SymfonyDocsBuilder\BuildConfig; + +$config = (new BuildConfig()) + ->setContentDir('/path/to/rst-docs') + ->setOutputDir('/path/to/output') + ->setSymfonyVersion('7.2'); + +$builder = new DocBuilder(); +$result = $builder->build($config); + +if (!$result->isSuccessful()) { + foreach ($result->getErrors() as $error) { + echo $error . "\n"; + } +} +``` + +#### Build a Single RST String + +```php +use SymfonyDocsBuilder\DocBuilder; + +$rst = <<buildString($rst); + +$html = $result->getStringResult(); +``` + +#### Advanced Configuration + +```php +$config = (new BuildConfig()) + ->setContentDir($sourceDir) + ->setOutputDir($outputDir) + ->setCacheDir('/tmp/docs-cache') // Default: {outputDir}/.cache + ->setImagesDir('/path/to/public/images') // Default: {outputDir}/_images + ->setImagesPublicPrefix('/assets/images/') // URL prefix for + ->setSymfonyVersion('7.2') + ->setTheme('rtd') // 'default' or 'rtd' + ->setExcludedPaths(['_build', 'vendor']) + ->disableBuildCache() + ->disableJsonFileGeneration(); + +$builder = new DocBuilder(); +$result = $builder->build($config); +``` + +#### With Progress Output + +```php +use Symfony\Component\Console\Style\SymfonyStyle; +use Symfony\Component\Console\Input\ArrayInput; +use Symfony\Component\Console\Output\ConsoleOutput; + +$io = new SymfonyStyle(new ArrayInput([]), new ConsoleOutput()); + +$result = $builder->build($config, $io); +``` + +### BuildConfig API + +| Method | Description | +|----------------------------------|----------------------------------------------| +| `setContentDir(string)` | Source directory containing `.rst` files | +| `setOutputDir(string)` | Output directory for generated HTML | +| `setCacheDir(string)` | Cache directory (default: `{outputDir}/.cache`) | +| `setImagesDir(string)` | Target directory for copied images | +| `setImagesPublicPrefix(string)` | URL prefix for image `src` attributes | +| `setSymfonyVersion(string)` | Symfony version (used in URLs and `{version}` placeholders) | +| `setTheme(string)` | `'default'` or `'rtd'` | +| `setSubdirectoryToBuild(string)` | Build only a subdirectory (for PDF generation)| +| `setExcludedPaths(array)` | Paths to exclude from parsing | +| `disableBuildCache()` | Disable output caching | +| `disableJsonFileGeneration()` | Don't generate `.fjson` files | + +### BuildResult API + +| Method | Description | +|---------------------|-----------------------------------------------------| +| `isSuccessful()` | Returns `true` if there are no errors | +| `getErrors()` | Returns an array of error message strings | +| `getErrorTrace()` | Returns errors formatted as a single string | +| `getProjectNode()` | Returns the phpDocumentor `ProjectNode` (full AST) | +| `getJsonResults()` | Returns generated JSON metadata | +| `getStringResult()` | Returns the HTML output (only for `buildString()`) | + +### Integration in a Symfony Application + +`DocBuilder` creates its own internal DI container (via `GuidesContainerFactory`), +so you don't need to register any of its internal services in your Symfony +application. Simply instantiate `DocBuilder` and call `build()` or `buildString()`: + +```php +// src/Service/DocumentationBuilder.php +namespace App\Service; + +use SymfonyDocsBuilder\DocBuilder; +use SymfonyDocsBuilder\BuildConfig; +use SymfonyDocsBuilder\BuildResult; + +class DocumentationBuilder +{ + public function __construct( + private string $docsSourceDir, + private string $docsOutputDir, + private string $symfonyVersion = '7.2', + ) {} + + public function buildAll(): BuildResult + { + $config = (new BuildConfig()) + ->setContentDir($this->docsSourceDir) + ->setOutputDir($this->docsOutputDir) + ->setSymfonyVersion($this->symfonyVersion) + ->setTheme('default') + ->disableJsonFileGeneration(); + + return (new DocBuilder())->build($config); + } + + public function renderRstString(string $rstContent): string + { + $result = (new DocBuilder())->buildString($rstContent); + + if (!$result->isSuccessful()) { + throw new \RuntimeException( + 'RST rendering failed: ' . $result->getErrorTrace() + ); + } + + return $result->getStringResult(); + } +} +``` + +```yaml +# config/services.yaml +services: + App\Service\DocumentationBuilder: + arguments: + $docsSourceDir: '%kernel.project_dir%/docs' + $docsOutputDir: '%kernel.project_dir%/public/docs' + $symfonyVersion: '7.2' +``` [1]: https://github.com/symfony/symfony-docs -[2]: https://github.com/doctrine/rst-parser +[2]: https://github.com/phpDocumentor/guides diff --git a/composer.json b/composer.json index aceb20a5..61b545e4 100644 --- a/composer.json +++ b/composer.json @@ -17,20 +17,23 @@ "php": ">=8.3", "ext-json": "*", "ext-curl": "*", - "doctrine/rst-parser": "^0.5", + "phpdocumentor/guides-restructured-text": "^1.0", "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" + "symfony/config": "^6.4 || ^7.0 || ^8.0", + "symfony/console": "^6.4 || ^7.0 || ^8.0", + "symfony/css-selector": "^6.4 || ^7.0 || ^8.0", + "symfony/dependency-injection": "^6.4 || ^7.0 || ^8.0", + "symfony/dom-crawler": "^6.4 || ^7.0 || ^8.0", + "symfony/filesystem": "^6.4 || ^7.0 || ^8.0", + "symfony/finder": "^6.4 || ^7.0 || ^8.0", + "symfony/http-client": "^6.4 || ^7.0 || ^8.0", + "twig/twig": "^3.3", + "symfony/event-dispatcher": "^6.4 || ^7.0 || ^8.0" }, "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", + "symfony/phpunit-bridge": "^6.4 || ^7.0 || ^8.0", + "symfony/process": "^6.4 || ^7.0 || ^8.0", "masterminds/html5": "^2.7" }, "bin": ["bin/docs-builder"] diff --git a/src/BuildConfig.php b/src/BuildConfig.php index b7281c3e..4f173efe 100644 --- a/src/BuildConfig.php +++ b/src/BuildConfig.php @@ -9,7 +9,6 @@ namespace SymfonyDocsBuilder; -use Doctrine\RST\Configuration; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -38,7 +37,7 @@ class BuildConfig public function __construct() { $this->useBuildCache = true; - $this->theme = Configuration::THEME_DEFAULT; + $this->theme = 'default'; $this->symfonyVersion = '4.4'; $this->excludedPaths = []; $this->imagesPublicPrefix = ''; diff --git a/src/BuildResult.php b/src/BuildResult.php index 322d6ce1..ad76f730 100644 --- a/src/BuildResult.php +++ b/src/BuildResult.php @@ -2,33 +2,16 @@ namespace SymfonyDocsBuilder; -use Doctrine\RST\Builder; -use Doctrine\RST\Meta\Metas; -use Symfony\Component\Console\Output\NullOutput; -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 phpDocumentor\Guides\Nodes\ProjectNode; class BuildResult { - private $builder; - private $errors; - // only defined when using build() method - private $jsonResults = []; - // only defined when using buildString() method - private $stringResult = null; + private array $errors = []; + private array $jsonResults = []; + private ?string $stringResult = null; - public function __construct(Builder $builder) + public function __construct(private readonly ProjectNode $projectNode) { - $this->builder = $builder; - $this->errors = []; - foreach ($builder->getErrorManager()->getErrors() as $error) { - $this->errors[] = $error->asString(); - } } public function appendError(string $errorMessage): void @@ -43,7 +26,7 @@ public function prependError(string $errorMessage): void public function isSuccessful(): bool { - return null === $this->errors || 0 === \count($this->errors); + return 0 === \count($this->errors); } public function getErrors(): array @@ -56,24 +39,11 @@ public function getErrorTrace(): string return implode("\n", $this->errors); } - public function getMetadata(): Metas + public function getProjectNode(): ProjectNode { - return $this->builder->getMetas(); + return $this->projectNode; } - /** - * Returns the "master document": the first file whose toctree is parsed. - * - * Unless customized, this is "index" (i.e. file index.rst). - */ - public function getMasterDocumentFilename(): string - { - return $this->builder->getIndexName(); - } - - /** - * Returns the JSON array data generated for each file, keyed by the source filename. - */ public function getJsonResults(): array { return $this->jsonResults; @@ -84,10 +54,6 @@ public function setJsonResults(array $jsonResults): void $this->jsonResults = $jsonResults; } - /** - * Returns the HTML result of building some string contents - * using the buildString() builder method - */ public function getStringResult(): ?string { return $this->stringResult; diff --git a/src/Command/BuildDocsCommand.php b/src/Command/BuildDocsCommand.php index def179eb..c22c3543 100644 --- a/src/Command/BuildDocsCommand.php +++ b/src/Command/BuildDocsCommand.php @@ -11,10 +11,6 @@ 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; @@ -23,19 +19,14 @@ 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; +use SymfonyDocsBuilder\DocBuilder; class BuildDocsCommand extends Command { protected static $defaultName = 'build:docs'; private $buildConfig; - private $missingFilesChecker; /** @var SymfonyStyle */ private $io; @@ -44,7 +35,6 @@ public function __construct(BuildConfig $buildConfig) parent::__construct(self::$defaultName); $this->buildConfig = $buildConfig; - $this->missingFilesChecker = new MissingFilesChecker($buildConfig); } protected function configure(): void @@ -79,13 +69,6 @@ protected function configure(): void 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, @@ -132,7 +115,11 @@ protected function initialize(InputInterface $input, OutputInterface $output): v $this->buildConfig->disableBuildCache(); } - $this->buildConfig->setTheme($input->getOption('no-theme') ? Configuration::THEME_DEFAULT : 'rtd'); + if (!$input->getOption('output-json')) { + $this->buildConfig->disableJsonFileGeneration(); + } + + $this->buildConfig->setTheme($input->getOption('no-theme') ? 'default' : 'rtd'); $configFileParser = new ConfigFileParser($this->buildConfig, $output); $configFileParser->processConfigFile($sourceDir); @@ -140,26 +127,12 @@ protected function initialize(InputInterface $input, OutputInterface $output): v 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); + $builder = new DocBuilder(); + $buildResult = $builder->build($this->buildConfig, $this->io); + + $buildErrors = $buildResult->getErrors(); + foreach ($buildErrors as $error) { + $this->io->warning($error); } if ($logPath = $input->getOption('save-errors')) { @@ -168,14 +141,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int } $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); + $filesystem->dumpFile($logPath, implode("\n", $buildErrors)); } $this->io->newLine(2); @@ -192,25 +158,4 @@ protected function execute(InputInterface $input, OutputInterface $output): int 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/Compiler/CopyImagesTransformer.php b/src/Compiler/CopyImagesTransformer.php new file mode 100644 index 00000000..529f8222 --- /dev/null +++ b/src/Compiler/CopyImagesTransformer.php @@ -0,0 +1,77 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyDocsBuilder\Compiler; + +use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Nodes\ImageNode; +use phpDocumentor\Guides\Nodes\Node; +use Psr\Log\LoggerInterface; +use Symfony\Component\Filesystem\Filesystem; +use SymfonyDocsBuilder\BuildConfig; + +class CopyImagesTransformer implements NodeTransformer +{ + public function __construct( + private readonly BuildConfig $buildConfig, + private readonly ?LoggerInterface $logger = null, + ) { + } + + public function enterNode(Node $node, CompilerContext $compilerContext): Node + { + if (!$node instanceof ImageNode) { + return $node; + } + + $sourceImage = $this->buildConfig->getContentDir().'/'.$node->getValue(); + + if (!file_exists($sourceImage)) { + $this->logger?->error(sprintf( + 'Missing image file "%s"', + $node->getValue(), + )); + + return $node; + } + + $fileInfo = new \SplFileInfo($sourceImage); + $fs = new Filesystem(); + + $newAbsoluteFilePath = $this->buildConfig->getImagesDir().'/'.$fileInfo->getFilename(); + $fs->copy($sourceImage, $newAbsoluteFilePath, true); + + if ('' === $this->buildConfig->getImagesPublicPrefix()) { + $newUrlPath = '_images/'.$fileInfo->getFilename(); + } else { + $newUrlPath = $this->buildConfig->getImagesPublicPrefix().'/'.$fileInfo->getFilename(); + } + $node->setValue($newUrlPath); + + return $node; + } + + public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + { + return $node; + } + + public function supports(Node $node): bool + { + return $node instanceof ImageNode; + } + + public function getPriority(): int + { + return 1000; + } +} diff --git a/src/Compiler/UrlNodeTransformer.php b/src/Compiler/UrlNodeTransformer.php new file mode 100644 index 00000000..cd04e933 --- /dev/null +++ b/src/Compiler/UrlNodeTransformer.php @@ -0,0 +1,116 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyDocsBuilder\Compiler; + +use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Compiler\NodeTransformer; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Node; +use SymfonyDocsBuilder\CI\UrlChecker; + +use function str_contains; +use function str_replace; +use function str_starts_with; + +/** + * Transforms HyperLinkNode URLs during compilation: + * - Replaces {version} placeholders with the configured Symfony version + * - Checks external URLs for validity (when UrlChecker is configured) + * - Adds security attributes (rel, target) for external non-Symfony URLs + * + * This replaces the URL-related functionality that was previously in SpanNodeRenderer + * (which was part of the doctrine/rst-parser integration). + * + * @implements NodeTransformer + */ +class UrlNodeTransformer implements NodeTransformer +{ + public function __construct( + private readonly ?UrlChecker $urlChecker = null, + private readonly ?string $symfonyVersion = null, + ) { + } + + public function enterNode(Node $node, CompilerContext $compilerContext): Node + { + if (!$node instanceof HyperLinkNode) { + return $node; + } + + $url = $node->getUrl(); + if ('' === $url) { + $url = $node->getTargetReference(); + } + + // Replace {version} placeholders with the configured Symfony version + if (null !== $this->symfonyVersion && str_contains($url, '{version}')) { + $url = str_replace('{version}', $this->symfonyVersion, $url); + $node->setUrl($url); + } + + // Check external URLs for validity + if ( + null !== $this->urlChecker + && $this->isExternalUrl($url) + && !str_starts_with($url, 'http://localhost') + && !str_starts_with($url, 'http://192.168') + ) { + $this->urlChecker->checkUrl($url); + } + + // Add security attributes for external non-Symfony URLs + if (!$this->isSafeUrl($url)) { + $node = $node->withOptions([ + 'rel' => 'external noopener noreferrer', + 'target' => '_blank', + ]); + } + + return $node; + } + + public function leaveNode(Node $node, CompilerContext $compilerContext): Node|null + { + return $node; + } + + public function supports(Node $node): bool + { + return $node instanceof HyperLinkNode; + } + + public function getPriority(): int + { + return 1000; + } + + private function isExternalUrl(string $url): bool + { + return str_contains($url, '://'); + } + + /** + * 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) + $isSymfonyUrl = (bool) preg_match('{^http(s)?://(.*\.)?symfony.(com|wip)}', $url); + $isRelativeUrl = !str_starts_with($url, 'http://') && !str_starts_with($url, 'https://'); + + return $isSymfonyUrl || $isRelativeUrl; + } +} diff --git a/src/Directive/AbstractAdmonitionDirective.php b/src/Directive/AbstractAdmonitionDirective.php index cdc366d8..5e720920 100644 --- a/src/Directive/AbstractAdmonitionDirective.php +++ b/src/Directive/AbstractAdmonitionDirective.php @@ -9,44 +9,11 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective as BaseAbstractAdmonitionDirective; -abstract class AbstractAdmonitionDirective extends SubDirective +/** + * @deprecated Use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective directly. + */ +abstract class AbstractAdmonitionDirective extends BaseAbstractAdmonitionDirective { - /** @var string */ - private $name; - - /** @var string */ - private $text; - - public function __construct(string $name, string $text) - { - $this->name = $name; - $this->text = $text; - } - - final public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node - { - if (null === $document) { - throw new \RuntimeException('Content expected, none found.'); - } - - $wrapperDiv = $parser->renderTemplate( - 'directives/admonition.html.twig', - [ - 'name' => $this->name, - 'text' => $this->text, - 'class' => $options['class'] ?? null, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); - } - - final public function getName(): string - { - return $this->name; - } } diff --git a/src/Directive/AdmonitionDirective.php b/src/Directive/AdmonitionDirective.php index 75061fde..6bb7e4da 100644 --- a/src/Directive/AdmonitionDirective.php +++ b/src/Directive/AdmonitionDirective.php @@ -9,28 +9,34 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\AdmonitionNode; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; class AdmonitionDirective extends SubDirective { - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node + public function __construct(protected Rule $startingRule) { - $wrapperDiv = $parser->renderTemplate( - 'directives/admonition.html.twig', - [ - 'name' => 'default', - 'text' => $data, - 'class' => $options['class'] ?? null, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); + parent::__construct($startingRule); } public function getName(): string { return 'admonition'; } + + protected function processSub(BlockContext $blockContext, CollectionNode $collectionNode, Directive $directive): Node|null + { + return new AdmonitionNode( + 'default', + $directive->getDataNode(), + $directive->getData(), + $collectionNode->getChildren(), + true, + ); + } } diff --git a/src/Directive/AttentionDirective.php b/src/Directive/AttentionDirective.php index 10c400f9..69d20a44 100644 --- a/src/Directive/AttentionDirective.php +++ b/src/Directive/AttentionDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class AttentionDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('attention', 'Attention'); + parent::__construct($startingRule, 'attention', 'Attention'); } } diff --git a/src/Directive/BestPracticeDirective.php b/src/Directive/BestPracticeDirective.php index d443b577..4873d7ed 100644 --- a/src/Directive/BestPracticeDirective.php +++ b/src/Directive/BestPracticeDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class BestPracticeDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('best-practice', 'Best Practice'); + parent::__construct($startingRule, 'best-practice', 'Best Practice'); } } diff --git a/src/Directive/CautionDirective.php b/src/Directive/CautionDirective.php index c0a9dfc4..e17f9acb 100644 --- a/src/Directive/CautionDirective.php +++ b/src/Directive/CautionDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class CautionDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('caution', 'Caution'); + parent::__construct($startingRule, 'caution', 'Caution'); } } diff --git a/src/Directive/CodeBlockDirective.php b/src/Directive/CodeBlockDirective.php index 1ccd4b2b..2dd7d362 100644 --- a/src/Directive/CodeBlockDirective.php +++ b/src/Directive/CodeBlockDirective.php @@ -9,47 +9,72 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\Directive; -use Doctrine\RST\Nodes\CodeNode; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CodeNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\BaseDirective; +use phpDocumentor\Guides\RestructuredText\Directives\OptionMapper\CodeNodeOptionMapper; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use Psr\Log\LoggerInterface; use SymfonyDocsBuilder\Renderers\CodeNodeRenderer; -class CodeBlockDirective extends Directive +use function trim; + +class CodeBlockDirective extends BaseDirective { + public function __construct( + private readonly LoggerInterface $logger, + private readonly CodeNodeOptionMapper $codeNodeOptionMapper, + ) { + } + public function getName(): string { return 'code-block'; } - public function process(Parser $parser, ?Node $node, string $variable, string $data, array $options): void + /** @inheritDoc */ + public function getAliases(): array { - if (!$node instanceof CodeNode) { - return; - } + return ['code', 'parsed-literal']; + } + + /** @inheritDoc */ + public function process( + BlockContext $blockContext, + Directive $directive, + ): Node|null { + if ($blockContext->getDocumentIterator()->isEmpty()) { + $this->logger->warning('The code-block has no content. Did you properly indent the code? ', $blockContext->getLoggerInformation()); - if (!CodeNodeRenderer::isLanguageSupported($data)) { - throw new \Exception(sprintf('Unsupported code block language "%s". Added it in %s', $data, CodeNodeRenderer::class)); + return null; } - $node->setLanguage($data); - // grab the "class" option and forward it onto the Node - // CodeNodeRenderer can then use it when rendering - $node->setClasses(isset($options['class']) ? explode(' ', $options['class']) : []); + $language = trim($directive->getData()); + + if ($language !== '' && !CodeNodeRenderer::isLanguageSupported($language)) { + throw new \Exception(sprintf('Unsupported code block language "%s". Add it in %s', $language, CodeNodeRenderer::class)); + } - $node->setOptions(array_merge($node->getOptions(), ['caption' => $options['caption'] ?? null])); + $node = new CodeNode( + $blockContext->getDocumentIterator()->toArray(), + ); - if ('' !== $variable) { - $environment = $parser->getEnvironment(); - $environment->setVariable($variable, $node); + if ($language !== '') { + $node->setLanguage($language); } else { - $document = $parser->getDocument(); - $document->addNode($node); + $node->setLanguage($blockContext->getDocumentParserContext()->getCodeBlockDefaultLanguage()); } - } - public function wantCode(): bool - { - return true; + $this->codeNodeOptionMapper->apply($node, $directive->getOptions(), $blockContext); + + if ($directive->getVariable() !== '') { + $document = $blockContext->getDocumentParserContext()->getDocument(); + $document->addVariable($directive->getVariable(), $node); + + return null; + } + + return $node; } } diff --git a/src/Directive/ConfigurationBlockDirective.php b/src/Directive/ConfigurationBlockDirective.php index 83850ce4..b8076ce7 100644 --- a/src/Directive/ConfigurationBlockDirective.php +++ b/src/Directive/ConfigurationBlockDirective.php @@ -9,11 +9,22 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\CodeNode; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; -use function strtoupper; +use phpDocumentor\Guides\Nodes\CodeNode; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\Configuration\ConfigurationBlockNode; +use phpDocumentor\Guides\Nodes\Configuration\ConfigurationTab; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; +use Psr\Log\LoggerInterface; +use Symfony\Component\String\Slugger\AsciiSlugger; +use Symfony\Component\String\Slugger\SluggerInterface; + +use function assert; +use function get_debug_type; +use function sprintf; class ConfigurationBlockDirective extends SubDirective { @@ -39,37 +50,50 @@ class ConfigurationBlockDirective extends SubDirective 'yaml' => 'YAML', ]; + private SluggerInterface $slugger; + + public function __construct( + private readonly LoggerInterface $logger, + protected Rule $startingRule, + ) { + parent::__construct($startingRule); + + $this->slugger = new AsciiSlugger(); + } + public function getName(): string { return 'configuration-block'; } - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node - { - $blocks = []; - foreach ($document->getNodes() as $node) { - if (!$node instanceof CodeNode) { + protected function processSub( + BlockContext $blockContext, + CollectionNode $collectionNode, + Directive $directive, + ): Node|null { + $tabs = []; + foreach ($collectionNode->getValue() as $child) { + if (!$child instanceof CodeNode) { + $this->logger->warning( + sprintf('The ".. configuration-block::" directive only supports code blocks, "%s" given.', get_debug_type($child)), + $blockContext->getLoggerInformation(), + ); + continue; } - $language = $node->getLanguage() ?? 'Unknown'; + $language = $child->getLanguage(); + assert($language !== null); - $blocks[] = [ - 'hash' => hash('sha1', $node->getValue()), - 'language_label' => self::LANGUAGE_LABELS[$language] ?? ucfirst(str_replace('-', ' ', $language)), - 'language' => $language, - 'code' => $node->render(), - ]; - } + $label = self::LANGUAGE_LABELS[$language] ?? $this->slugger->slug($language, ' ')->title()->toString(); - $wrapperDiv = $parser->renderTemplate( - 'directives/configuration-block.html.twig', - [ - 'blocks' => $blocks, - 'title' => 'Configuration formats', - ] - ); + $tabs[] = new ConfigurationTab( + $label, + $this->slugger->slug($label)->lower()->toString(), + $child, + ); + } - return $parser->getNodeFactory()->createWrapperNode(null, $wrapperDiv, ''); + return new ConfigurationBlockNode($tabs); } } diff --git a/src/Directive/DangerDirective.php b/src/Directive/DangerDirective.php index 8c38a877..c05437de 100644 --- a/src/Directive/DangerDirective.php +++ b/src/Directive/DangerDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class DangerDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('danger', 'Danger'); + parent::__construct($startingRule, 'danger', 'Danger'); } } diff --git a/src/Directive/DeprecatedDirective.php b/src/Directive/DeprecatedDirective.php index b311a49c..eb162268 100644 --- a/src/Directive/DeprecatedDirective.php +++ b/src/Directive/DeprecatedDirective.php @@ -9,29 +9,13 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractVersionChangeDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; -class DeprecatedDirective extends SubDirective +class DeprecatedDirective extends AbstractVersionChangeDirective { - public function getName(): string + public function __construct(protected Rule $startingRule) { - return 'deprecated'; - } - - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node - { - $wrapperDiv = $parser->renderTemplate( - 'directives/admonition.html.twig', - [ - 'name' => 'deprecated', - 'text' => $data, - 'class' => $options['class'] ?? null, - 'version' => $data, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); + parent::__construct($startingRule, 'deprecated', 'Deprecated since version %s'); } } diff --git a/src/Directive/ErrorDirective.php b/src/Directive/ErrorDirective.php index c1a48bfa..8c94c05d 100644 --- a/src/Directive/ErrorDirective.php +++ b/src/Directive/ErrorDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class ErrorDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('error', 'Error'); + parent::__construct($startingRule, 'error', 'Error'); } } diff --git a/src/Directive/FigureDirective.php b/src/Directive/FigureDirective.php index 43cfdd20..18c1b37e 100644 --- a/src/Directive/FigureDirective.php +++ b/src/Directive/FigureDirective.php @@ -9,53 +9,67 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\FigureNode; +use phpDocumentor\Guides\Nodes\ImageNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\ReferenceResolvers\DocumentNameResolverInterface; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + +use function dirname; /** * Overridden to handle "figclass" properly. */ class FigureDirective extends SubDirective { + public function __construct( + private readonly DocumentNameResolverInterface $documentNameResolver, + protected Rule $startingRule, + ) { + parent::__construct($startingRule); + } + public function getName(): string { return 'figure'; } - /** - * @param string[] $options - */ - public function processSub( - Parser $parser, - ?Node $document, - string $variable, - string $data, - array $options - ): ?Node { - $environment = $parser->getEnvironment(); - - $url = $environment->relativeUrl($data); - - if ($url === null) { - throw new \Exception(sprintf('Could not get relative url for %s', $data)); - } - - $nodeFactory = $parser->getNodeFactory(); + protected function processSub( + BlockContext $blockContext, + CollectionNode $collectionNode, + Directive $directive, + ): Node|null { + $image = new ImageNode($this->documentNameResolver->absoluteUrl( + dirname($blockContext->getDocumentParserContext()->getContext()->getCurrentAbsolutePath()), + $directive->getData(), + )); + $scalarOptions = $this->optionsToArray($directive->getOptions()); /* Start Custom Code */ - $figClass = $options['figclass'] ?? null; - unset($options['figclass']); + $figClass = $scalarOptions['figclass'] ?? null; + unset($scalarOptions['figclass']); /* End Custom Code */ - $figureNode = $parser->getNodeFactory()->createFigureNode( - $nodeFactory->createImageNode($url, $options), - $document - ); + $image = $image->withOptions([ + 'width' => $scalarOptions['width'] ?? null, + 'height' => $scalarOptions['height'] ?? null, + 'alt' => $scalarOptions['alt'] ?? null, + 'scale' => $scalarOptions['scale'] ?? null, + 'target' => $scalarOptions['target'] ?? null, + 'class' => $scalarOptions['class'] ?? null, + 'name' => $scalarOptions['name'] ?? null, + 'align' => $scalarOptions['align'] ?? null, + ]); + + $figureNode = new FigureNode($image, new CollectionNode($collectionNode->getChildren())); /* Start Custom Code */ if ($figClass) { - $figureNode->setClasses(explode(' ', $figClass)); + $figureNode->setClasses(explode(' ', (string) $figClass)); } /* End Custom Code */ diff --git a/src/Directive/GlossaryDirective.php b/src/Directive/GlossaryDirective.php index 8d43f999..57668380 100644 --- a/src/Directive/GlossaryDirective.php +++ b/src/Directive/GlossaryDirective.php @@ -9,13 +9,19 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; /** * @deprecated */ class GlossaryDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'glossary'; diff --git a/src/Directive/HintDirective.php b/src/Directive/HintDirective.php index 6f381969..57dfa767 100644 --- a/src/Directive/HintDirective.php +++ b/src/Directive/HintDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class HintDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('hint', 'Hint'); + parent::__construct($startingRule, 'hint', 'Hint'); } } diff --git a/src/Directive/ImportantDirective.php b/src/Directive/ImportantDirective.php index 3c880867..596f02dc 100644 --- a/src/Directive/ImportantDirective.php +++ b/src/Directive/ImportantDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class ImportantDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('important', 'Important'); + parent::__construct($startingRule, 'important', 'Important'); } } diff --git a/src/Directive/IndexDirective.php b/src/Directive/IndexDirective.php index 86e75395..9015e6bd 100644 --- a/src/Directive/IndexDirective.php +++ b/src/Directive/IndexDirective.php @@ -9,10 +9,16 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; class IndexDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'index'; diff --git a/src/Directive/NoteDirective.php b/src/Directive/NoteDirective.php index 93a4b874..91d19bb8 100644 --- a/src/Directive/NoteDirective.php +++ b/src/Directive/NoteDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class NoteDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('note', 'Note'); + parent::__construct($startingRule, 'note', 'Note'); } } diff --git a/src/Directive/RoleDirective.php b/src/Directive/RoleDirective.php index e2bc721f..b9aa80ef 100644 --- a/src/Directive/RoleDirective.php +++ b/src/Directive/RoleDirective.php @@ -9,10 +9,16 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; class RoleDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'role'; diff --git a/src/Directive/RstClassDirective.php b/src/Directive/RstClassDirective.php index 74d4ba1c..a42dbb21 100644 --- a/src/Directive/RstClassDirective.php +++ b/src/Directive/RstClassDirective.php @@ -2,43 +2,81 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\HTML\Directives\ClassDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\ClassNode; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\DocumentNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; +use Symfony\Component\String\Slugger\AsciiSlugger; + +use function array_map; +use function array_merge; +use function explode; /** * Allows you to add custom classes to the next directive. */ class RstClassDirective extends SubDirective { - private $classDirective; - - public function __construct(ClassDirective $classDirective) + public function __construct(protected Rule $startingRule) { - $this->classDirective = $classDirective; + parent::__construct($startingRule); } - /** - * @param string[] $options - */ - public function processSub( - Parser $parser, - ?Node $document, - string $variable, - string $data, - array $options - ): ?Node { - return $this->classDirective->processSub($parser, $document, $variable, $data, $options); + public function getName(): string + { + return 'rst-class'; } - public function appliesToNonBlockContent(): bool + /** @return string[] */ + public function getAliases(): array { - return $this->classDirective->appliesToNonBlockContent(); + return ['class']; } - public function getName() : string + protected function processSub( + BlockContext $blockContext, + CollectionNode $collectionNode, + Directive $directive, + ): Node|null { + $classes = explode(' ', $directive->getData()); + + $normalizedClasses = array_map( + static fn (string $class): string => (new AsciiSlugger())->slug($class)->lower()->toString(), + $classes, + ); + + $collectionNode->setClasses($normalizedClasses); + + if ($collectionNode->getChildren() === []) { + $classNode = new ClassNode($directive->getData()); + $classNode->setClasses($classes); + + return $classNode; + } + + $this->setNodesClasses($collectionNode->getChildren(), $classes); + + return new CollectionNode($collectionNode->getChildren()); + } + + /** + * @param Node[] $nodes + * @param string[] $classes + */ + private function setNodesClasses(array $nodes, array $classes): void { - return 'rst-class'; + foreach ($nodes as $node) { + $node->setClasses(array_merge($node->getClasses(), $classes)); + + if (!($node instanceof DocumentNode)) { + continue; + } + + $this->setNodesClasses($node->getNodes(), $classes); + } } } diff --git a/src/Directive/ScreencastDirective.php b/src/Directive/ScreencastDirective.php index cd2a795c..d6de9cbd 100644 --- a/src/Directive/ScreencastDirective.php +++ b/src/Directive/ScreencastDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class ScreencastDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('screencast', 'Screencast'); + parent::__construct($startingRule, 'screencast', 'Screencast'); } } diff --git a/src/Directive/SeeAlsoDirective.php b/src/Directive/SeeAlsoDirective.php index cd81a76d..159e83b0 100644 --- a/src/Directive/SeeAlsoDirective.php +++ b/src/Directive/SeeAlsoDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class SeeAlsoDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('seealso', 'See also'); + parent::__construct($startingRule, 'seealso', 'See also'); } } diff --git a/src/Directive/SidebarDirective.php b/src/Directive/SidebarDirective.php index f4674e23..5ae3697f 100644 --- a/src/Directive/SidebarDirective.php +++ b/src/Directive/SidebarDirective.php @@ -9,26 +9,32 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\InlineCompoundNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Nodes\SidebarNode; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; class SidebarDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'sidebar'; } - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node + protected function processSub(BlockContext $blockContext, CollectionNode $collectionNode, Directive $directive): Node|null { - $wrapperDiv = $parser->renderTemplate( - 'directives/sidebar.html.twig', - [ - 'title' => $parser->createSpanNode($data)->render(), - ] + return new SidebarNode( + $directive->getDataNode() ?? InlineCompoundNode::getPlainTextInlineNode($directive->getData()), + $collectionNode->getChildren(), ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); } } diff --git a/src/Directive/TabDirective.php b/src/Directive/TabDirective.php index 81e1c27f..284684b6 100644 --- a/src/Directive/TabDirective.php +++ b/src/Directive/TabDirective.php @@ -9,9 +9,12 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; use SymfonyDocsBuilder\Node\TabNode; /** @@ -19,18 +22,23 @@ */ class TabDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'tab'; } - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node + protected function processSub(BlockContext $blockContext, CollectionNode $collectionNode, Directive $directive): Node|null { - $tabName = $data; + $tabName = $directive->getData(); if (!$tabName) { throw new \RuntimeException(sprintf('The "tab" directive requires a tab name: ".. tab:: Tab Name".')); } - return new TabNode($document->getNodes(), $data); + return new TabNode($collectionNode->getChildren(), $tabName); } } diff --git a/src/Directive/TabsDirective.php b/src/Directive/TabsDirective.php index 0a8c25e6..4086840e 100644 --- a/src/Directive/TabsDirective.php +++ b/src/Directive/TabsDirective.php @@ -9,52 +9,43 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; use SymfonyDocsBuilder\Node\TabNode; +use SymfonyDocsBuilder\Node\TabsNode; class TabsDirective extends SubDirective { + public function __construct(protected Rule $startingRule) + { + parent::__construct($startingRule); + } + public function getName(): string { return 'tabs'; } - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node + protected function processSub(BlockContext $blockContext, CollectionNode $collectionNode, Directive $directive): Node|null { - $tabsTitle = $data; + $tabsTitle = $directive->getData(); if (!$tabsTitle) { throw new \RuntimeException(sprintf('The "tabs" directive requires a title: ".. tabs:: Title".')); } - $blocks = []; - foreach ($document->getNodes() as $tabNode) { + $tabs = []; + foreach ($collectionNode->getChildren() as $tabNode) { if (!$tabNode instanceof TabNode) { throw new \RuntimeException(sprintf('Only ".. tab::" content can appear within the "tabs" directive.')); } - $content = ''; - foreach ($tabNode->getNodes() as $node) { - $content .= $node->render(); - } + $tabs[] = $tabNode; + } - $blocks[] = [ - 'hash' => hash('sha1', $tabNode->getTabName()), - 'language_label' => $tabNode->getTabName(), - 'language' => $tabNode->getSluggedTabName(), - 'code' => $content, - ]; - } - - $wrapperDiv = $parser->renderTemplate( - 'directives/configuration-block.html.twig', - [ - 'blocks' => $blocks, - 'title' => $tabsTitle, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode(null, $wrapperDiv, ''); + return new TabsNode($tabsTitle, $tabs); } } diff --git a/src/Directive/TipDirective.php b/src/Directive/TipDirective.php index 41ba8969..19900e6e 100644 --- a/src/Directive/TipDirective.php +++ b/src/Directive/TipDirective.php @@ -9,10 +9,13 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class TipDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { - parent::__construct('tip', 'Tip'); + parent::__construct($startingRule, 'tip', 'Tip'); } } diff --git a/src/Directive/TopicDirective.php b/src/Directive/TopicDirective.php index 42e571bc..431cafc0 100644 --- a/src/Directive/TopicDirective.php +++ b/src/Directive/TopicDirective.php @@ -9,26 +9,28 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\Nodes\CollectionNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RestructuredText\Directives\SubDirective; +use phpDocumentor\Guides\RestructuredText\Nodes\TopicNode; +use phpDocumentor\Guides\RestructuredText\Parser\BlockContext; +use phpDocumentor\Guides\RestructuredText\Parser\Directive; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; class TopicDirective extends SubDirective { - final public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node + public function __construct(protected Rule $startingRule) { - $wrapperDiv = $parser->renderTemplate( - 'directives/topic.html.twig', - [ - 'name' => $data, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); + parent::__construct($startingRule); } public function getName(): string { return 'topic'; } + + protected function processSub(BlockContext $blockContext, CollectionNode $collectionNode, Directive $directive): Node|null + { + return new TopicNode($directive->getData(), $collectionNode->getChildren()); + } } diff --git a/src/Directive/VersionAddedDirective.php b/src/Directive/VersionAddedDirective.php index 3568bacc..720d8b74 100644 --- a/src/Directive/VersionAddedDirective.php +++ b/src/Directive/VersionAddedDirective.php @@ -9,29 +9,13 @@ namespace SymfonyDocsBuilder\Directive; -use Doctrine\RST\Directives\SubDirective; -use Doctrine\RST\Nodes\Node; -use Doctrine\RST\Parser; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractVersionChangeDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; -class VersionAddedDirective extends SubDirective +class VersionAddedDirective extends AbstractVersionChangeDirective { - public function getName(): string + public function __construct(protected Rule $startingRule) { - return 'versionadded'; - } - - public function processSub(Parser $parser, ?Node $document, string $variable, string $data, array $options): ?Node - { - $wrapperDiv = $parser->renderTemplate( - 'directives/admonition.html.twig', - [ - 'name' => 'versionadded', - 'text' => $data, - 'class' => $options['class'] ?? null, - 'version' => $data, - ] - ); - - return $parser->getNodeFactory()->createWrapperNode($document, $wrapperDiv, ''); + parent::__construct($startingRule, 'versionadded', 'New in version %s'); } } diff --git a/src/Directive/WarningDirective.php b/src/Directive/WarningDirective.php index c849d337..4bc68d65 100644 --- a/src/Directive/WarningDirective.php +++ b/src/Directive/WarningDirective.php @@ -9,11 +9,14 @@ namespace SymfonyDocsBuilder\Directive; +use phpDocumentor\Guides\RestructuredText\Directives\AbstractAdmonitionDirective; +use phpDocumentor\Guides\RestructuredText\Parser\Productions\Rule; + class WarningDirective extends AbstractAdmonitionDirective { - public function __construct() + public function __construct(protected Rule $startingRule) { // we render warning and caution the same - parent::__construct('warning', 'Warning'); + parent::__construct($startingRule, 'warning', 'Warning'); } } diff --git a/src/DocBuilder.php b/src/DocBuilder.php index 46ce8064..d3ea8321 100644 --- a/src/DocBuilder.php +++ b/src/DocBuilder.php @@ -2,17 +2,25 @@ namespace SymfonyDocsBuilder; -use Doctrine\RST\Builder; +use League\Tactician\CommandBus; +use phpDocumentor\FileSystem\FlySystemAdapter; +use phpDocumentor\Guides\Compiler\CompilerContext; +use phpDocumentor\Guides\Handlers\CompileDocumentsCommand; +use phpDocumentor\Guides\Handlers\ParseDirectoryCommand; +use phpDocumentor\Guides\Handlers\RenderCommand; +use phpDocumentor\Guides\Nodes\ProjectNode; use Symfony\Component\Console\Output\NullOutput; +use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; use SymfonyDocsBuilder\CI\MissingFilesChecker; +use SymfonyDocsBuilder\CI\UrlChecker; use SymfonyDocsBuilder\Generator\HtmlForPdfGenerator; use SymfonyDocsBuilder\Generator\JsonGenerator; final class DocBuilder { - public function build(BuildConfig $config): BuildResult + public function build(BuildConfig $config, ?SymfonyStyle $io = null, ?UrlChecker $urlChecker = null): BuildResult { $filesystem = new Filesystem(); if (!$config->isBuildCacheEnabled() && $filesystem->exists($config->getOutputDir())) { @@ -23,10 +31,45 @@ public function build(BuildConfig $config): BuildResult $configFileParser = new ConfigFileParser($config, new NullOutput()); $configFileParser->processConfigFile($config->getContentDir()); - $builder = new Builder(KernelFactory::createKernel($config)); - $builder->build($config->getContentDir(), $config->getOutputDir()); + // Create the DI container with all services + $container = GuidesContainerFactory::createContainer($config, $urlChecker, $io); + $commandBus = $container->get(CommandBus::class); - $buildResult = new BuildResult($builder); + // Create filesystems for source and output + $sourceFilesystem = FlySystemAdapter::createForPath($config->getContentDir()); + $outputFilesystem = FlySystemAdapter::createForPath($config->getOutputDir()); + + // Create project node + $projectNode = new ProjectNode(); + + // Phase 1: Parse + $documents = $commandBus->handle( + new ParseDirectoryCommand( + $sourceFilesystem, + '', + 'rst', + $projectNode, + ) + ); + + // Phase 2: Compile + $compilerContext = new CompilerContext($projectNode); + $documents = $commandBus->handle( + new CompileDocumentsCommand($documents, $compilerContext) + ); + + // Phase 3: Render + $commandBus->handle( + new RenderCommand( + 'html', + $documents, + $sourceFilesystem, + $outputFilesystem, + $projectNode, + ) + ); + + $buildResult = new BuildResult($projectNode); $missingFilesChecker = new MissingFilesChecker($config); $missingFiles = $missingFilesChecker->getMissingFiles(); @@ -35,26 +78,22 @@ public function build(BuildConfig $config): BuildResult } 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())); + $errorLog = sprintf("Build errors from \"%s\"\n%s", date('Y-m-d h:i:s'), implode("\n", $buildResult->getErrors())); + $filesystem->dumpFile($config->getOutputDir().'/build_errors.txt', $errorLog); } 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 = new HtmlForPdfGenerator($projectNode, $config); $htmlForPdfGenerator->generateHtmlForPdf(); } elseif ($config->generateJsonFiles()) { - $metas = $buildResult->getMetadata(); - $jsonGenerator = new JsonGenerator($metas, $config); - $buildResult->setJsonResults($jsonGenerator->generateJson($builder->getIndexName())); + $jsonGenerator = new JsonGenerator($projectNode, $config); + $buildResult->setJsonResults($jsonGenerator->generateJson()); } return $buildResult; diff --git a/src/DocsKernel.php b/src/DocsKernel.php index 8ffbedd7..8c9b1f52 100644 --- a/src/DocsKernel.php +++ b/src/DocsKernel.php @@ -1,70 +1,7 @@ - * 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()) - ); - } - } -} +// This file has been replaced by GuidesContainerFactory.php as part of the +// migration from doctrine/rst-parser to phpdocumentor/guides. +// It is kept empty to avoid autoload errors during the transition. diff --git a/src/Generator/HtmlForPdfGenerator.php b/src/Generator/HtmlForPdfGenerator.php index 64495131..c86952f7 100644 --- a/src/Generator/HtmlForPdfGenerator.php +++ b/src/Generator/HtmlForPdfGenerator.php @@ -11,8 +11,8 @@ namespace SymfonyDocsBuilder\Generator; -use Doctrine\RST\Meta\MetaEntry; -use Doctrine\RST\Meta\Metas; +use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\ProjectNode; use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; @@ -21,13 +21,13 @@ class HtmlForPdfGenerator { - private $metas; + private $projectNode; private $buildConfig; - public function __construct(Metas $metas, BuildConfig $buildConfig) + public function __construct(ProjectNode $projectNode, BuildConfig $buildConfig) { - $this->metas = $metas; + $this->projectNode = $projectNode; $this->buildConfig = $buildConfig; } @@ -51,8 +51,8 @@ public function generateHtmlForPdf() // extracting all files from index's TOC, in the right order $parserFilename = $this->getParserFilename($indexFile, $this->buildConfig->getOutputDir()); - $meta = $this->getMetaEntry($parserFilename); - $files = current($meta->getTocs()); + $meta = $this->getDocumentEntry($parserFilename); + $files = array_map(fn(DocumentEntryNode $child) => $child->getFile(), $meta->getChildren()); array_unshift($files, sprintf('%s/index', $this->buildConfig->getSubdirectoryToBuild())); // building one big html file with all contents @@ -60,7 +60,7 @@ public function generateHtmlForPdf() $htmlDir = $this->buildConfig->getOutputDir(); $relativeImagesPath = str_repeat('../', substr_count($this->buildConfig->getSubdirectoryToBuild(), '/')); foreach ($files as $file) { - $meta = $this->getMetaEntry($file); + $meta = $this->getDocumentEntry($file); $filename = sprintf('%s/%s.html', $htmlDir, $file); if (!$fs->exists($filename)) { @@ -160,15 +160,15 @@ static function ($matches): string { return $content; } - private function getMetaEntry(string $parserFilename): MetaEntry + private function getDocumentEntry(string $parserFilename): DocumentEntryNode { - $metaEntry = $this->metas->get($parserFilename); + $documentEntry = $this->projectNode->findDocumentEntry($parserFilename); - if (null === $metaEntry) { - throw new \LogicException(sprintf('Could not find MetaEntry for file "%s"', $parserFilename)); + if (null === $documentEntry) { + throw new \LogicException(sprintf('Could not find DocumentEntryNode for file "%s"', $parserFilename)); } - return $metaEntry; + return $documentEntry; } private function getParserFilename(string $filePath, string $inputDir): string diff --git a/src/Generator/JsonGenerator.php b/src/Generator/JsonGenerator.php index 51de0dee..916038da 100644 --- a/src/Generator/JsonGenerator.php +++ b/src/Generator/JsonGenerator.php @@ -11,9 +11,8 @@ namespace SymfonyDocsBuilder\Generator; -use Doctrine\RST\Environment; -use Doctrine\RST\Meta\MetaEntry; -use Doctrine\RST\Meta\Metas; +use phpDocumentor\Guides\Nodes\DocumentTree\DocumentEntryNode; +use phpDocumentor\Guides\Nodes\ProjectNode; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Style\SymfonyStyle; @@ -25,16 +24,16 @@ class JsonGenerator { - private $metas; + private $projectNode; private $buildConfig; /** @var SymfonyStyle|null */ private $output; - public function __construct(Metas $metas, BuildConfig $buildConfig) + public function __construct(ProjectNode $projectNode, BuildConfig $buildConfig) { - $this->metas = $metas; + $this->projectNode = $projectNode; $this->buildConfig = $buildConfig; } @@ -49,7 +48,7 @@ public function generateJson(string $masterDocument = 'index'): array $fs = new Filesystem(); $progressBar = new ProgressBar($this->output ?: new NullOutput()); - $progressBar->setMaxSteps(\count($this->metas->getAll())); + $progressBar->setMaxSteps(\count($this->projectNode->getAllDocumentEntries())); $walkedFiles = []; $tocTreeHierarchy = $this->walkTocTreeAndReturnHierarchy( @@ -62,18 +61,18 @@ public function generateJson(string $masterDocument = 'index'): array $flattenedTocTree = $this->flattenTocTree($tocTreeHierarchy); $fJsonFiles = []; - foreach ($this->metas->getAll() as $filename => $metaEntry) { + foreach ($this->projectNode->getAllDocumentEntries() as $filename => $documentEntry) { $parserFilename = $filename; $jsonFilename = $this->buildConfig->getOutputDir().'/'.$filename.'.fjson'; $crawler = new Crawler(file_get_contents($this->buildConfig->getOutputDir().'/'.$filename.'.html')); // happens when some doc is a partial included in other doc an it doesn't have any titles - $toc = $this->generateToc($metaEntry, $crawler); + $toc = $this->generateToc($documentEntry, $crawler); $next = $this->determineNext($parserFilename, $flattenedTocTree, $masterDocument); $prev = $this->determinePrev($parserFilename, $flattenedTocTree); $data = [ - 'title' => $metaEntry->getTitle(), + 'title' => $documentEntry->getTitle()->toString(), 'parents' => $this->determineParents($parserFilename, $tocTreeHierarchy) ?: [], 'current_page_name' => $parserFilename, 'toc' => $toc, @@ -102,18 +101,20 @@ public function setOutput(SymfonyStyle $output) $this->output = $output; } - private function generateToc(MetaEntry $metaEntry, Crawler $crawler): array + private function generateToc(DocumentEntryNode $documentEntry, Crawler $crawler): array { $flatTocTree = []; foreach ($crawler->filter('h2, h3') as $heading) { - $headerId = $heading->getAttribute('id') ?? Environment::slugify($heading->textContent); + $headerId = $heading->getAttribute('id') ?? trim(preg_replace('/[^a-z0-9]+/', '-', strtolower($heading->textContent)), '-'); + + $url = $documentEntry->getFile() . '.html'; // this tocTree stores items sequentially (h2, h2, h3, h3, h2, h3, etc.) $flatTocTree[] = [ 'level' => 'h2' === $heading->tagName ? 1 : 2, - 'url' => sprintf('%s#%s', $metaEntry->getUrl(), $headerId), - 'page' => u($metaEntry->getUrl())->beforeLast('.html')->toString(), + 'url' => sprintf('%s#%s', $url, $headerId), + 'page' => u($url)->beforeLast('.html')->toString(), 'fragment' => $headerId, 'title' => $heading->textContent, 'children' => [], @@ -179,13 +180,13 @@ private function determinePrev(string $parserFilename, array $flattenedTocTree): return $this->makeRelativeLink($parserFilename, $previousFileName); } - private function getMetaEntry(string $parserFilename, bool $throwOnMissing = false): ?MetaEntry + private function getDocumentEntry(string $parserFilename, bool $throwOnMissing = false): ?DocumentEntryNode { - $metaEntry = $this->metas->get($parserFilename); + $documentEntry = $this->projectNode->findDocumentEntry($parserFilename); // this is possible if there are invalid references - if (null === $metaEntry) { - $message = sprintf('Could not find MetaEntry for file "%s"', $parserFilename); + if (null === $documentEntry) { + $message = sprintf('Could not find DocumentEntryNode for file "%s"', $parserFilename); if ($throwOnMissing) { throw new \Exception($message); @@ -196,7 +197,7 @@ private function getMetaEntry(string $parserFilename, bool $throwOnMissing = fal } } - return $metaEntry; + return $documentEntry; } /** @@ -227,11 +228,13 @@ private function walkTocTreeAndReturnHierarchy(string $filename, array &$walkedF $hierarchy = []; // happens in edge-cases such as empty or not found documents - if (null === $meta = $this->getMetaEntry($filename)) { + if (null === $documentEntry = $this->getDocumentEntry($filename)) { return $hierarchy; } - foreach ($meta->getTocs() as $toc) { + $tocs = [array_map(fn(DocumentEntryNode $child) => $child->getFile(), $documentEntry->getChildren())]; + + foreach ($tocs as $toc) { foreach ($toc as $tocFilename) { // only walk a file one time, the first time you see it if (in_array($tocFilename, $walkedFiles, true)) { @@ -291,13 +294,13 @@ private function determineParents(string $parserFilename, array $tocTreeHierarch private function makeRelativeLink(string $currentFilename, string $filename): array { // happens in edge-cases such as empty or not found documents - if (null === $meta = $this->getMetaEntry($filename)) { + if (null === $entry = $this->getDocumentEntry($filename)) { return ['title' => '', 'link' => '']; } return [ - 'title' => $meta->getTitle(), - 'link' => str_repeat('../', substr_count($currentFilename, '/')).$meta->getUrl(), + 'title' => $entry->getTitle()->toString(), + 'link' => str_repeat('../', substr_count($currentFilename, '/')) . $entry->getFile() . '.html', ]; } } diff --git a/src/GuidesContainerFactory.php b/src/GuidesContainerFactory.php new file mode 100644 index 00000000..5cd23ef1 --- /dev/null +++ b/src/GuidesContainerFactory.php @@ -0,0 +1,252 @@ +registerExtension(new GuidesExtension()); + $container->registerExtension(new ReStructuredTextExtension()); + + // Configure guides extension + $container->loadFromExtension('guides', [ + 'base_template_paths' => [__DIR__.'/Templates/default/html'], + 'output_format' => ['html'], + ]); + + // Load RST extension (triggers its service definitions) + $container->loadFromExtension('re_structured_text', []); + + // Register PSR-14 event dispatcher (public so we can add listeners after compilation) + $container->register(EventDispatcherInterface::class, EventDispatcher::class)->setPublic(true); + + // Register logger + $container->register(LoggerInterface::class, NullLogger::class); + + // Register BuildConfig as a synthetic service (set after compile) + $container->register(BuildConfig::class, BuildConfig::class)->setSynthetic(true); + + // Register custom directives + self::registerDirectives($container); + + // Register custom text roles (references) + self::registerTextRoles($container, $buildConfig); + + // Register custom node renderers + self::registerNodeRenderers($container); + + // Register custom node transformers + self::registerNodeTransformers($container, $buildConfig, $urlChecker); + + // Add compiler pass to remove optional services and make key services public + $container->addCompilerPass(new class implements CompilerPassInterface { + public function process(ContainerBuilder $container): void + { + // Remove definitions for classes from optional packages that aren't installed + foreach ($container->getDefinitions() as $id => $definition) { + $class = $definition->getClass() ?? $id; + if (str_contains($class, '\\') && !class_exists($class) && !interface_exists($class)) { + $container->removeDefinition($id); + } + } + + // Make CommandBus public so it can be fetched from the container + if ($container->hasDefinition(CommandBus::class)) { + $container->getDefinition(CommandBus::class)->setPublic(true); + } + } + }, PassConfig::TYPE_BEFORE_OPTIMIZATION, 1000); + + // Compile the container + $container->compile(); + + // Set synthetic services after compilation + $container->set(BuildConfig::class, $buildConfig); + if (null !== $urlChecker) { + $container->set(UrlChecker::class, $urlChecker); + } + + // Register event listeners on the compiled dispatcher + self::registerEventListeners($container, $buildConfig, $io); + + return $container; + } + + private static function registerDirectives(ContainerBuilder $container): void + { + // Directives that extend SubDirective/AbstractAdmonitionDirective/AbstractVersionChangeDirective + // need the $startingRule argument bound to DirectiveContentRule + $directivesNeedingRule = [ + SymfonyDirectives\AdmonitionDirective::class, + SymfonyDirectives\AttentionDirective::class, + SymfonyDirectives\BestPracticeDirective::class, + SymfonyDirectives\CautionDirective::class, + SymfonyDirectives\ConfigurationBlockDirective::class, + SymfonyDirectives\DangerDirective::class, + SymfonyDirectives\DeprecatedDirective::class, + SymfonyDirectives\ErrorDirective::class, + SymfonyDirectives\FigureDirective::class, + SymfonyDirectives\GlossaryDirective::class, + SymfonyDirectives\HintDirective::class, + SymfonyDirectives\ImportantDirective::class, + SymfonyDirectives\IndexDirective::class, + SymfonyDirectives\NoteDirective::class, + SymfonyDirectives\RoleDirective::class, + SymfonyDirectives\RstClassDirective::class, + SymfonyDirectives\ScreencastDirective::class, + SymfonyDirectives\SeeAlsoDirective::class, + SymfonyDirectives\SidebarDirective::class, + SymfonyDirectives\TabDirective::class, + SymfonyDirectives\TabsDirective::class, + SymfonyDirectives\TipDirective::class, + SymfonyDirectives\TopicDirective::class, + SymfonyDirectives\VersionAddedDirective::class, + SymfonyDirectives\WarningDirective::class, + ]; + + foreach ($directivesNeedingRule as $directiveClass) { + $container->register($directiveClass) + ->setAutowired(true) + ->setAutoconfigured(true) + ->setArgument('$startingRule', new Reference(DirectiveContentRule::class)) + ->addTag('phpdoc.guides.directive'); + } + + // Directives that extend BaseDirective (no $startingRule needed) + $simpleDirectives = [ + SymfonyDirectives\CodeBlockDirective::class, + ]; + + foreach ($simpleDirectives as $directiveClass) { + $container->register($directiveClass) + ->setAutowired(true) + ->setAutoconfigured(true) + ->addTag('phpdoc.guides.directive'); + } + } + + private static function registerTextRoles(ContainerBuilder $container, BuildConfig $buildConfig): void + { + $symfonyRepoUrl = $buildConfig->getSymfonyRepositoryUrl(); + $phpDocUrl = $buildConfig->getPhpDocUrl(); + + $textRoles = [ + SymfonyReferences\ClassReference::class => ['$symfonyRepositoryUrl' => $symfonyRepoUrl], + SymfonyReferences\MethodReference::class => ['$symfonyRepositoryUrl' => $symfonyRepoUrl], + SymfonyReferences\NamespaceReference::class => ['$symfonyRepositoryUrl' => $symfonyRepoUrl], + SymfonyReferences\PhpFunctionReference::class => ['$phpDocUrl' => $phpDocUrl], + SymfonyReferences\PhpMethodReference::class => ['$phpDocUrl' => $phpDocUrl], + SymfonyReferences\PhpClassReference::class => ['$phpDocUrl' => $phpDocUrl], + SymfonyReferences\TermReference::class => [], + SymfonyReferences\LeaderReference::class => [], + SymfonyReferences\MergerReference::class => [], + SymfonyReferences\DeciderReference::class => [], + ]; + + foreach ($textRoles as $class => $args) { + $def = $container->register($class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->addTag('phpdoc.guides.parser.rst.text_role'); + + foreach ($args as $name => $value) { + $def->setArgument($name, $value); + } + } + } + + private static function registerNodeRenderers(ContainerBuilder $container): void + { + $container->register(CodeNodeRenderer::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->addTag('phpdoc.guides.noderenderer.html'); + + $container->register(TitleNodeRenderer::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->addTag('phpdoc.guides.noderenderer.html'); + + $container->register(TabsNodeRenderer::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->addTag('phpdoc.guides.noderenderer.html'); + } + + private static function registerNodeTransformers(ContainerBuilder $container, BuildConfig $buildConfig, ?UrlChecker $urlChecker = null): void + { + $container->register(CopyImagesTransformer::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->setArgument('$buildConfig', new Reference(BuildConfig::class)) + ->addTag('phpdoc.guides.compiler.nodeTransformers'); + + $urlTransformerDef = $container->register(UrlNodeTransformer::class) + ->setAutowired(true) + ->setAutoconfigured(true) + ->setArgument('$symfonyVersion', $buildConfig->getSymfonyVersion()) + ->addTag('phpdoc.guides.compiler.nodeTransformers'); + + if (null !== $urlChecker) { + $container->register(UrlChecker::class, UrlChecker::class)->setSynthetic(true); + $urlTransformerDef->setArgument('$urlChecker', new Reference(UrlChecker::class)); + } + } + + private static function registerEventListeners(ContainerBuilder $container, BuildConfig $buildConfig, ?SymfonyStyle $io = null): void + { + /** @var EventDispatcher $dispatcher */ + $dispatcher = $container->get(EventDispatcherInterface::class); + + $dispatcher->addListener(PreParseDocument::class, new AdmonitionListener()); + $dispatcher->addListener(PreParseDocument::class, new DuplicatedHeaderIdListener()); + + if (!$buildConfig->getSubdirectoryToBuild()) { + $dispatcher->addListener(PostRenderProcess::class, new AssetsCopyListener($buildConfig->getOutputDir())); + } + + if (null !== $io) { + $progressListener = new BuildProgressListener($io); + $dispatcher->addListener(PostCollectFilesForParsingEvent::class, [$progressListener, 'onFilesCollected']); + $dispatcher->addListener(PostParseDocument::class, [$progressListener, 'onPostParseDocument']); + $dispatcher->addListener(PreRenderProcess::class, [$progressListener, 'onPreRender']); + } + } +} diff --git a/src/KernelFactory.php b/src/KernelFactory.php index 2feb16a3..8c9b1f52 100644 --- a/src/KernelFactory.php +++ b/src/KernelFactory.php @@ -1,117 +1,7 @@ - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace SymfonyDocsBuilder; -use Doctrine\RST\Configuration as RSTParserConfiguration; -use Doctrine\RST\HTML\Directives\ClassDirective; -use Doctrine\RST\Kernel; -use SymfonyDocsBuilder\CI\UrlChecker; -use SymfonyDocsBuilder\Directive as SymfonyDirectives; -use SymfonyDocsBuilder\Reference as SymfonyReferences; -use SymfonyDocsBuilder\Twig\AssetsExtension; -use SymfonyDocsBuilder\Twig\TocExtension; -use function Symfony\Component\String\u; - -final class KernelFactory -{ - public static function createKernel(BuildConfig $buildConfig, ?UrlChecker $urlChecker = null): Kernel - { - $configuration = new RSTParserConfiguration(); - // needed to avoid outputting parser errors on the console output or the webpage contents - $configuration->silentOnError(true); - $configuration->setCustomTemplateDirs([__DIR__.'/Templates']); - $configuration->setTheme($buildConfig->getTheme()); - $configuration->setCacheDir(sprintf('%s/var/cache', $buildConfig->getCacheDir())); - $configuration->abortOnError(false); - - if (!$buildConfig->isBuildCacheEnabled()) { - $configuration->setUseCachedMetas(false); - } - - $configuration->addFormat( - new SymfonyHTMLFormat( - $configuration->getTemplateRenderer(), - $configuration->getFormat(), - $urlChecker, - $buildConfig->getSymfonyVersion() - ) - ); - - if ($parseSubPath = $buildConfig->getSubdirectoryToBuild()) { - $configuration->setBaseUrl($buildConfig->getSymfonyDocUrl()); - $configuration->setBaseUrlEnabledCallable( - static function (string $path) use ($parseSubPath): bool { - return u($path)->containsAny($parseSubPath); - } - ); - } - - $twig = $configuration->getTemplateEngine(); - $twig->addExtension(new AssetsExtension()); - $twig->addExtension(new TocExtension()); - - return new DocsKernel( - $buildConfig, - $configuration, - self::getDirectives(), - self::getReferences($buildConfig) - ); - } - - private static function getDirectives(): array - { - return [ - new SymfonyDirectives\AdmonitionDirective(), - new SymfonyDirectives\AttentionDirective(), - new SymfonyDirectives\CautionDirective(), - new SymfonyDirectives\CodeBlockDirective(), - new SymfonyDirectives\ConfigurationBlockDirective(), - new SymfonyDirectives\DangerDirective(), - new SymfonyDirectives\DeprecatedDirective(), - new SymfonyDirectives\ErrorDirective(), - new SymfonyDirectives\FigureDirective(), - new SymfonyDirectives\HintDirective(), - new SymfonyDirectives\ImportantDirective(), - new SymfonyDirectives\IndexDirective(), - new SymfonyDirectives\RoleDirective(), - new SymfonyDirectives\NoteDirective(), - new SymfonyDirectives\RstClassDirective(new ClassDirective()), - new SymfonyDirectives\ScreencastDirective(), - new SymfonyDirectives\SeeAlsoDirective(), - new SymfonyDirectives\SidebarDirective(), - new SymfonyDirectives\TabDirective(), - new SymfonyDirectives\TabsDirective(), - new SymfonyDirectives\TipDirective(), - new SymfonyDirectives\TopicDirective(), - new SymfonyDirectives\WarningDirective(), - new SymfonyDirectives\VersionAddedDirective(), - new SymfonyDirectives\BestPracticeDirective(), - new SymfonyDirectives\GlossaryDirective(), - ]; - } - - private static function getReferences(BuildConfig $buildConfig): array - { - return [ - new SymfonyReferences\ClassReference($buildConfig->getSymfonyRepositoryUrl()), - new SymfonyReferences\MethodReference($buildConfig->getSymfonyRepositoryUrl()), - new SymfonyReferences\NamespaceReference($buildConfig->getSymfonyRepositoryUrl()), - new SymfonyReferences\PhpFunctionReference($buildConfig->getPhpDocUrl()), - new SymfonyReferences\PhpMethodReference($buildConfig->getPhpDocUrl()), - new SymfonyReferences\PhpClassReference($buildConfig->getPhpDocUrl()), - new SymfonyReferences\TermReference(), - new SymfonyReferences\LeaderReference(), - new SymfonyReferences\MergerReference(), - new SymfonyReferences\DeciderReference(), - ]; - } -} +// This file has been replaced by GuidesContainerFactory.php as part of the +// migration from doctrine/rst-parser to phpdocumentor/guides. +// It is kept empty to avoid autoload errors during the transition. diff --git a/src/Listener/AdmonitionListener.php b/src/Listener/AdmonitionListener.php index fbfbb33d..ab823678 100644 --- a/src/Listener/AdmonitionListener.php +++ b/src/Listener/AdmonitionListener.php @@ -11,11 +11,11 @@ namespace SymfonyDocsBuilder\Listener; -use Doctrine\RST\Event\PreParseDocumentEvent; +use phpDocumentor\Guides\Event\PreParseDocument; final class AdmonitionListener { - public function preParseDocument(PreParseDocumentEvent $event) + public function __invoke(PreParseDocument $event): void { // TODO: remove this temporary fix when Symfony Docs are updated to use the new '.. screencast::' directive $event->setContents(str_replace('.. admonition:: Screencast', '.. screencast::', $event->getContents())); diff --git a/src/Listener/AssetsCopyListener.php b/src/Listener/AssetsCopyListener.php index 36a4ec8e..c24122ca 100644 --- a/src/Listener/AssetsCopyListener.php +++ b/src/Listener/AssetsCopyListener.php @@ -11,19 +11,16 @@ namespace SymfonyDocsBuilder\Listener; +use phpDocumentor\Guides\Event\PostRenderProcess; use Symfony\Component\Filesystem\Filesystem; final class AssetsCopyListener { - /** @var string */ - private $targetDir; - - public function __construct(string $targetDir) + public function __construct(private readonly string $targetDir) { - $this->targetDir = $targetDir; } - public function postBuildRender() + public function __invoke(PostRenderProcess $event): void { $fs = new Filesystem(); $fs->mirror( diff --git a/src/Listener/BuildProgressListener.php b/src/Listener/BuildProgressListener.php index 11d1db64..28a273f8 100644 --- a/src/Listener/BuildProgressListener.php +++ b/src/Listener/BuildProgressListener.php @@ -11,75 +11,43 @@ namespace SymfonyDocsBuilder\Listener; -use Doctrine\Common\EventManager; -use Doctrine\RST\Event\PostParseDocumentEvent; -use Doctrine\RST\Event\PreBuildParseEvent; -use Doctrine\RST\Event\PreBuildRenderEvent; +use phpDocumentor\Guides\Event\PostCollectFilesForParsingEvent; +use phpDocumentor\Guides\Event\PostParseDocument; +use phpDocumentor\Guides\Event\PreRenderProcess; use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Style\SymfonyStyle; class BuildProgressListener { - private $io; - private $progressBar; - private $parsedFiles = []; + private ProgressBar $progressBar; + private array $parsedFiles = []; - public function __construct(SymfonyStyle $io) + public function __construct(private readonly SymfonyStyle $io) { - $this->io = $io; $this->progressBar = new ProgressBar($io); } - public function attachListeners(EventManager $eventManager) + public function onFilesCollected(PostCollectFilesForParsingEvent $event): void { - // sets up the "parsing" progress bar - $eventManager->addEventListener( - [PreBuildParseEvent::PRE_BUILD_PARSE], - $this - ); - - // advances "parsing" progress bar - $eventManager->addEventListener( - [PostParseDocumentEvent::POST_PARSE_DOCUMENT], - $this - ); - - // tries to handle progress bar for "rendering" - $eventManager->addEventListener( - [PreBuildRenderEvent::PRE_BUILD_RENDER], - $this - ); - } - - /** - * Called very early: used to initialize the "parsing" progress bar. - * - * @param PreBuildParseEvent $event - */ - public function preBuildParse(PreBuildParseEvent $event) - { - $parseQueue = $event->getParseQueue(); - $parseCount = \count($parseQueue->getAllFilesThatRequireParsing()); - $this->io->note(sprintf('Start parsing %d out-of-date rst files', $parseCount)); - $this->progressBar->setMaxSteps($parseCount); + $fileCount = count($event->getFiles()); + $this->io->note(sprintf('Start parsing %d rst files', $fileCount)); + $this->progressBar->setMaxSteps($fileCount); } - public function postParseDocument(PostParseDocumentEvent $postParseDocumentEvent): void + public function onPostParseDocument(PostParseDocument $event): void { - $file = $postParseDocumentEvent->getDocumentNode()->getEnvironment()->getCurrentFileName(); + $file = $event->getFileName(); if (!\in_array($file, $this->parsedFiles, true)) { $this->parsedFiles[] = $file; $this->progressBar->advance(); } } - public function preBuildRender() + public function onPreRender(PreRenderProcess $event): void { - // finishes the "parse" progress bar $this->progressBar->finish(); $this->io->newLine(2); $this->io->note('Rendering the HTML files...'); - // TODO: create a proper progress bar for rendering } } diff --git a/src/Listener/CopyImagesListener.php b/src/Listener/CopyImagesListener.php deleted file mode 100644 index 2e164872..00000000 --- a/src/Listener/CopyImagesListener.php +++ /dev/null @@ -1,63 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder\Listener; - -use Doctrine\RST\ErrorManager; -use Doctrine\RST\Event\PreNodeRenderEvent; -use Doctrine\RST\Nodes\ImageNode; -use Symfony\Component\Filesystem\Filesystem; -use SymfonyDocsBuilder\BuildConfig; - -class CopyImagesListener -{ - private $buildConfig; - private $errorManager; - - public function __construct(BuildConfig $buildConfig, ErrorManager $errorManager) - { - $this->buildConfig = $buildConfig; - $this->errorManager = $errorManager; - } - - public function preNodeRender(PreNodeRenderEvent $event) - { - $node = $event->getNode(); - if (!$node instanceof ImageNode) { - return; - } - - $sourceImage = $node->getEnvironment()->absoluteRelativePath($node->getUrl()); - - if (!file_exists($sourceImage)) { - $this->errorManager->error(sprintf( - 'Missing image file "%s" in "%s"', - $node->getUrl(), - $node->getEnvironment()->getCurrentFileName() - )); - - return; - } - - $fileInfo = new \SplFileInfo($sourceImage); - $fs = new Filesystem(); - - $newAbsoluteFilePath = $this->buildConfig->getImagesDir().'/'.$fileInfo->getFilename(); - $fs->copy($sourceImage, $newAbsoluteFilePath, true); - - if ('' === $this->buildConfig->getImagesPublicPrefix()) { - $newUrlPath = $node->getEnvironment()->relativeUrl('_images/'.$fileInfo->getFilename()); - } else { - $newUrlPath = $this->buildConfig->getImagesPublicPrefix().'/'.$fileInfo->getFilename(); - } - $node->setValue($newUrlPath); - } -} diff --git a/src/Listener/DuplicatedHeaderIdListener.php b/src/Listener/DuplicatedHeaderIdListener.php index b0d139bf..fb3f3583 100644 --- a/src/Listener/DuplicatedHeaderIdListener.php +++ b/src/Listener/DuplicatedHeaderIdListener.php @@ -11,12 +11,12 @@ namespace SymfonyDocsBuilder\Listener; -use Doctrine\RST\Event\PreParseDocumentEvent; +use phpDocumentor\Guides\Event\PreParseDocument; use SymfonyDocsBuilder\Renderers\TitleNodeRenderer; final class DuplicatedHeaderIdListener { - public function preParseDocument(PreParseDocumentEvent $event): void + public function __invoke(PreParseDocument $event): void { // needed because we only need to handle duplicated headers within // the same file, not across all the files being generated diff --git a/src/Node/TabNode.php b/src/Node/TabNode.php index 620cc750..d246d2c0 100644 --- a/src/Node/TabNode.php +++ b/src/Node/TabNode.php @@ -2,31 +2,23 @@ namespace SymfonyDocsBuilder\Node; -use Doctrine\RST\Nodes\Node; +use phpDocumentor\Guides\Nodes\CompoundNode; +use phpDocumentor\Guides\Nodes\Node; /** * Wraps nodes + options in a TabDirective. + * + * @extends CompoundNode */ -class TabNode extends Node +class TabNode extends CompoundNode { - /** - * @var Node[] - */ - private array $nodes; - private string $tabName; - public function __construct(array $nodes, string $tabName) + public function __construct(array $children, string $tabName) { - $this->nodes = $nodes; $this->tabName = $tabName; - parent::__construct(); - } - - public function getNodes(): array - { - return $this->nodes; + parent::__construct($children); } public function getTabName(): string diff --git a/src/Node/TabsNode.php b/src/Node/TabsNode.php new file mode 100644 index 00000000..0c515e16 --- /dev/null +++ b/src/Node/TabsNode.php @@ -0,0 +1,33 @@ + + */ +class TabsNode extends CompoundNode +{ + /** @param TabNode[] $tabs */ + public function __construct( + private readonly string $title, + array $tabs, + ) { + parent::__construct($tabs); + } + + public function getTitle(): string + { + return $this->title; + } + + /** @return TabNode[] */ + public function getTabs(): array + { + return $this->value; + } +} diff --git a/src/Reference/ClassReference.php b/src/Reference/ClassReference.php index 517e7f6e..1c6a8f30 100644 --- a/src/Reference/ClassReference.php +++ b/src/Reference/ClassReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class ClassReference extends Reference +class ClassReference implements TextRole { - private $symfonyRepositoryUrl; - - public function __construct(string $symfonyRepositoryUrl) + public function __construct(private readonly string $symfonyRepositoryUrl) { - $this->symfonyRepositoryUrl = $symfonyRepositoryUrl; } public function getName(): string @@ -28,9 +26,18 @@ public function getName(): string return 'class'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - $className = u($data)->replace('\\\\', '\\'); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + $className = u($content)->replace('\\\\', '\\'); /** * Symfony AI classes require some special handling because of its monorepo structure. Example: @@ -63,14 +70,6 @@ public function resolve(Environment $environment, string $data): ResolvedReferen $url = sprintf('%s/%s.php', $this->symfonyRepositoryUrl, $className->replace('\\', '/')); } - return new ResolvedReference( - $environment->getCurrentFileName(), - $className->afterLast('\\'), - $url, - [], - [ - 'title' => $className, - ] - ); + return new HyperLinkNode($className->afterLast('\\')->toString(), $url); } } diff --git a/src/Reference/DeciderReference.php b/src/Reference/DeciderReference.php index cf3da331..69f4ea35 100644 --- a/src/Reference/DeciderReference.php +++ b/src/Reference/DeciderReference.php @@ -9,26 +9,32 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; /** * @deprecated */ -class DeciderReference extends Reference +class DeciderReference implements TextRole { public function getName(): string { return 'decider'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - return new ResolvedReference( - $environment->getCurrentFileName(), - $data, - '#' - ); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + return new PlainTextInlineNode($content); } } diff --git a/src/Reference/LeaderReference.php b/src/Reference/LeaderReference.php index 3feb8052..856e6ed5 100644 --- a/src/Reference/LeaderReference.php +++ b/src/Reference/LeaderReference.php @@ -9,26 +9,32 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; /** * @deprecated */ -class LeaderReference extends Reference +class LeaderReference implements TextRole { public function getName(): string { return 'leader'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - return new ResolvedReference( - $environment->getCurrentFileName(), - $data, - '#' - ); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + return new PlainTextInlineNode($content); } } diff --git a/src/Reference/MergerReference.php b/src/Reference/MergerReference.php index 571ec7b8..9c5005d7 100644 --- a/src/Reference/MergerReference.php +++ b/src/Reference/MergerReference.php @@ -9,26 +9,32 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; /** * @deprecated */ -class MergerReference extends Reference +class MergerReference implements TextRole { public function getName(): string { return 'merger'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - return new ResolvedReference( - $environment->getCurrentFileName(), - $data, - '#' - ); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + return new PlainTextInlineNode($content); } } diff --git a/src/Reference/MethodReference.php b/src/Reference/MethodReference.php index b068f334..a46dfbdb 100644 --- a/src/Reference/MethodReference.php +++ b/src/Reference/MethodReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class MethodReference extends Reference +class MethodReference implements TextRole { - private $symfonyRepositoryUrl; - - public function __construct(string $symfonyRepositoryUrl) + public function __construct(private readonly string $symfonyRepositoryUrl) { - $this->symfonyRepositoryUrl = $symfonyRepositoryUrl; } public function getName(): string @@ -28,25 +26,30 @@ public function getName(): string return 'method'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - $data = u($data); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + $data = u($content); if (!$data->containsAny('::')) { - throw new \RuntimeException(sprintf('Malformed method reference "%s" in file "%s"', $data, $environment->getCurrentFileName())); + throw new \RuntimeException(sprintf('Malformed method reference "%s"', $data)); } [$className, $methodName] = $data->split('::', 2); $className = $className->replace('\\\\', '\\'); $scrollTextFragment = sprintf('#:~:text=%s', rawurlencode('function '.$methodName)); - return new ResolvedReference( - $environment->getCurrentFileName(), + + return new HyperLinkNode( $methodName.'()', sprintf('%s/%s.php%s', $this->symfonyRepositoryUrl, $className->replace('\\', '/'), $scrollTextFragment), - [], - [ - 'title' => sprintf('%s::%s()', $className, $methodName), - ] ); } } diff --git a/src/Reference/NamespaceReference.php b/src/Reference/NamespaceReference.php index 96eaff97..e819bbb9 100644 --- a/src/Reference/NamespaceReference.php +++ b/src/Reference/NamespaceReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class NamespaceReference extends Reference +class NamespaceReference implements TextRole { - private $symfonyRepositoryUrl; - - public function __construct(string $symfonyRepositoryUrl) + public function __construct(private readonly string $symfonyRepositoryUrl) { - $this->symfonyRepositoryUrl = $symfonyRepositoryUrl; } public function getName(): string @@ -28,18 +26,22 @@ public function getName(): string return 'namespace'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - $className = u($data)->replace('\\\\', '\\'); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + $className = u($content)->replace('\\\\', '\\'); - return new ResolvedReference( - $environment->getCurrentFileName(), - $className->afterLast('\\'), + return new HyperLinkNode( + $className->afterLast('\\')->toString(), sprintf('%s/%s', $this->symfonyRepositoryUrl, $className->replace('\\', '/')), - [], - [ - 'title' => $className, - ] ); } } diff --git a/src/Reference/PhpClassReference.php b/src/Reference/PhpClassReference.php index fbaabe44..f19c654a 100644 --- a/src/Reference/PhpClassReference.php +++ b/src/Reference/PhpClassReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class PhpClassReference extends Reference +class PhpClassReference implements TextRole { - private $phpDocUrl; - - public function __construct(string $phpDocUrl) + public function __construct(private readonly string $phpDocUrl) { - $this->phpDocUrl = $phpDocUrl; } public function getName(): string @@ -28,18 +26,22 @@ public function getName(): string return 'phpclass'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - $className = u($data)->replace('\\\\', '\\'); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + $className = u($content)->replace('\\\\', '\\'); - return new ResolvedReference( - $environment->getCurrentFileName(), - $className->afterLast('\\'), + return new HyperLinkNode( + $className->afterLast('\\')->toString(), sprintf('%s/class.%s.php', $this->phpDocUrl, $className->replace('\\', '-')->lower()), - [], - [ - 'title' => $className, - ] ); } } diff --git a/src/Reference/PhpFunctionReference.php b/src/Reference/PhpFunctionReference.php index ee36d0a2..49e57279 100644 --- a/src/Reference/PhpFunctionReference.php +++ b/src/Reference/PhpFunctionReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class PhpFunctionReference extends Reference +class PhpFunctionReference implements TextRole { - private $phpDocUrl; - - public function __construct(string $phpDocUrl) + public function __construct(private readonly string $phpDocUrl) { - $this->phpDocUrl = $phpDocUrl; } public function getName(): string @@ -28,16 +26,20 @@ public function getName(): string return 'phpfunction'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - return new ResolvedReference( - $environment->getCurrentFileName(), - $data, - sprintf('%s/function.%s.php', $this->phpDocUrl, u($data)->replace('_', '-')->lower()), - [], - [ - 'title' => $data, - ] + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + return new HyperLinkNode( + $content, + sprintf('%s/function.%s.php', $this->phpDocUrl, u($content)->replace('_', '-')->lower()), ); } } diff --git a/src/Reference/PhpMethodReference.php b/src/Reference/PhpMethodReference.php index 9a85a918..640db303 100644 --- a/src/Reference/PhpMethodReference.php +++ b/src/Reference/PhpMethodReference.php @@ -9,18 +9,16 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\HyperLinkNode; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; use function Symfony\Component\String\u; -class PhpMethodReference extends Reference +class PhpMethodReference implements TextRole { - private $phpDocUrl; - - public function __construct(string $phpDocUrl) + public function __construct(private readonly string $phpDocUrl) { - $this->phpDocUrl = $phpDocUrl; } public function getName(): string @@ -28,24 +26,28 @@ public function getName(): string return 'phpmethod'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - $data = u($data); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + $data = u($content); if (!$data->containsAny('::')) { - throw new \RuntimeException(sprintf('Malformed method reference "%s" in file "%s"', $data, $environment->getCurrentFileName())); + throw new \RuntimeException(sprintf('Malformed method reference "%s"', $data)); } [$className, $methodName] = $data->split('::', 2); $className = $className->replace('\\\\', '\\'); - return new ResolvedReference( - $environment->getCurrentFileName(), + return new HyperLinkNode( $methodName.'()', sprintf('%s/%s.%s.php', $this->phpDocUrl, $className->replace('\\', '-')->lower(), $methodName->lower()), - [], - [ - 'title' => sprintf('%s::%s()', $className, $methodName), - ] ); } } diff --git a/src/Reference/TermReference.php b/src/Reference/TermReference.php index 18ee953f..508d7841 100644 --- a/src/Reference/TermReference.php +++ b/src/Reference/TermReference.php @@ -9,26 +9,32 @@ namespace SymfonyDocsBuilder\Reference; -use Doctrine\RST\Environment; -use Doctrine\RST\References\Reference; -use Doctrine\RST\References\ResolvedReference; +use phpDocumentor\Guides\Nodes\Inline\InlineNodeInterface; +use phpDocumentor\Guides\Nodes\Inline\PlainTextInlineNode; +use phpDocumentor\Guides\RestructuredText\Parser\DocumentParserContext; +use phpDocumentor\Guides\RestructuredText\TextRoles\TextRole; /** * @deprecated */ -class TermReference extends Reference +class TermReference implements TextRole { public function getName(): string { return 'term'; } - public function resolve(Environment $environment, string $data): ResolvedReference + public function getAliases(): array { - return new ResolvedReference( - $environment->getCurrentFileName(), - $data, - '#' - ); + return []; + } + + public function processNode( + DocumentParserContext $documentParserContext, + string $role, + string $content, + string $rawContent, + ): InlineNodeInterface { + return new PlainTextInlineNode($content); } } diff --git a/src/Renderers/CodeNodeRenderer.php b/src/Renderers/CodeNodeRenderer.php index d5e771ee..77619472 100644 --- a/src/Renderers/CodeNodeRenderer.php +++ b/src/Renderers/CodeNodeRenderer.php @@ -11,14 +11,20 @@ namespace SymfonyDocsBuilder\Renderers; -use Doctrine\RST\Nodes\CodeNode; -use Doctrine\RST\Renderers\NodeRenderer; -use Doctrine\RST\Templates\TemplateRenderer; +use phpDocumentor\Guides\NodeRenderers\NodeRenderer; +use phpDocumentor\Guides\Nodes\CodeNode; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\RenderContext; +use phpDocumentor\Guides\TemplateRenderer; use Highlight\Highlighter; +use function assert; +use function is_a; + +/** @implements NodeRenderer */ class CodeNodeRenderer implements NodeRenderer { - private static $isHighlighterConfigured = false; + private static bool $isHighlighterConfigured = false; private const LANGUAGES_MAPPING = [ 'caddy' => 'plaintext', @@ -39,26 +45,22 @@ class CodeNodeRenderer implements NodeRenderer 'vcl' => 'c', ]; - /** @var CodeNode */ - private $codeNode; - - /** @var TemplateRenderer */ - private $templateRenderer; + public function __construct(private readonly TemplateRenderer $templateRenderer) + { + } - public function __construct(CodeNode $codeNode, TemplateRenderer $templateRenderer) + public function supports(string $nodeFqcn): bool { - $this->codeNode = $codeNode; - $this->templateRenderer = $templateRenderer; + return $nodeFqcn === CodeNode::class || is_a($nodeFqcn, CodeNode::class, true); } - public function render(): string + public function render(Node $node, RenderContext $renderContext): string { - $code = trim($this->codeNode->getValue()); - if ($this->codeNode->isRaw()) { - return $code; - } + assert($node instanceof CodeNode); + + $code = trim($node->getValue()); - $language = $this->codeNode->getLanguage() ?? 'php'; + $language = $node->getLanguage() ?: 'php'; $languageMapping = self::LANGUAGES_MAPPING[$language] ?? $language; $languages = array_unique([$language, $languageMapping]); @@ -82,15 +84,16 @@ public function render(): string // 'caption' is used by code blocks to define the path of the file they belong to // 'patch_file' is a special value used by "diff patches", which don't correspond to any file - $codeCaption = $this->codeNode->getOptions()['caption'] ?? null; + $codeCaption = $node->getCaption()?->toString() ?? $node->getOption('caption', null); if ('patch_file' === $codeCaption) { $codeCaption = null; } - return $this->templateRenderer->render( + return $this->templateRenderer->renderTemplate( + $renderContext, 'code.html.twig', [ - 'custom_css_classes' => $this->codeNode->getClassesString(), + 'custom_css_classes' => $node->getClassesString(), 'languages' => $languages, 'line_numbers' => $lineNumbers, 'code' => $highlightedCode, @@ -116,7 +119,7 @@ public static function isLanguageSupported(string $lang): bool return \in_array($lang, $supportedLanguages, true); } - private function configureHighlighter() + private function configureHighlighter(): void { if (false === self::$isHighlighterConfigured) { Highlighter::registerLanguage('php', __DIR__.'/../Templates/highlight.php/php.json', true); diff --git a/src/Renderers/SpanNodeRenderer.php b/src/Renderers/SpanNodeRenderer.php index e1e77755..d58a2b38 100644 --- a/src/Renderers/SpanNodeRenderer.php +++ b/src/Renderers/SpanNodeRenderer.php @@ -1,169 +1,7 @@ - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace SymfonyDocsBuilder\Renderers; -use Doctrine\RST\Environment; -use Doctrine\RST\HTML\Renderers\SpanNodeRenderer as BaseSpanNodeRenderer; -use Doctrine\RST\Nodes\SpanNode; -use Doctrine\RST\References\ResolvedReference; -use Doctrine\RST\Renderers\SpanNodeRenderer as AbstractSpanNodeRenderer; -use SymfonyDocsBuilder\CI\UrlChecker; -use function Symfony\Component\String\u; - -class SpanNodeRenderer extends AbstractSpanNodeRenderer -{ - /** @var BaseSpanNodeRenderer */ - private $decoratedSpanNodeRenderer; - /** @var UrlChecker|null */ - private $urlChecker; - private $symfonyVersion; - - public function __construct( - Environment $environment, - SpanNode $span, - BaseSpanNodeRenderer $decoratedSpanNodeRenderer, - ?UrlChecker $urlChecker = null, - ?string $symfonyVersion = null - ) - { - parent::__construct($environment, $span); - - $this->decoratedSpanNodeRenderer = $decoratedSpanNodeRenderer; - $this->urlChecker = $urlChecker; - $this->symfonyVersion = $symfonyVersion; - } - - public function render(): string - { - // Work around "~" being parsed as non-breaking space by rst-parser, - // while this is not part of the specification. - $spanValue = $this->span->getValue(); - - if (str_contains($spanValue, '__TILDE__')) { - throw new \Exception('Cannot render content containing the text "__TILDE__" as it is used as a special placeholder in the build.'); - } - - $spanValue = str_replace('~', '__TILDE__', $spanValue); - $this->span->setValue($spanValue); - - $rendered = parent::render(); - - return str_replace('__TILDE__', '~', $rendered); - } - - /** @inheritDoc */ - public function link(?string $url, string $title, array $attributes = []): string - { - $url = (string) $url; - - if ( - $this->urlChecker && - $this->isExternalUrl($url) && - !u($url)->startsWith(['http://localhost', 'http://192.168']) - ) { - $this->urlChecker->checkUrl($url); - } - - if (!$this->isSafeUrl($url)) { - $attributes = $this->addAttributesForUnsafeUrl($attributes); - } - - if (null !== $this->symfonyVersion) { - $url = u($url)->replace('{version}', $this->symfonyVersion)->toString(); - } - - return $this->decoratedSpanNodeRenderer->link($url, $title, $attributes); - } - - public function reference(ResolvedReference $reference, array $value): string - { - if (!$this->isSafeUrl($reference->getUrl())) { - $reference = new ResolvedReference( - $reference->getFile(), - $reference->getTitle(), - $reference->getUrl(), - $reference->getTitles(), - $this->addAttributesForUnsafeUrl($reference->getAttributes()) - ); - } - - return $this->decoratedSpanNodeRenderer->reference($reference, $value); - } - - public function literal(string $text): 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($text, '\\') >= 4) { - // breaking before the backslask is what Firefox browser does - $text = str_replace('\\', '\\', $text); - } - - return $this->decoratedSpanNodeRenderer->literal($text); - } - - public function emphasis(string $text): string - { - return $this->decoratedSpanNodeRenderer->emphasis($text); - } - - public function strongEmphasis(string $text): string - { - return $this->decoratedSpanNodeRenderer->strongEmphasis($text); - } - - public function nbsp(): string - { - return $this->decoratedSpanNodeRenderer->nbsp(); - } - - public function br(): string - { - return $this->decoratedSpanNodeRenderer->br(); - } - - public function escape(string $span): string - { - return $this->decoratedSpanNodeRenderer->escape($span); - } - - private function isExternalUrl($url): bool - { - return u($url)->containsAny('://'); - } - - /* - * 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) - $isSymfonyUrl = preg_match('{^http(s)?://(.*\.)?symfony.(com|wip)}', $url); - $isRelativeUrl = !str_starts_with($url, 'http://') && !str_starts_with($url, 'https://'); - - return $isSymfonyUrl || $isRelativeUrl; - } - - private function addAttributesForUnsafeUrl(array $attributes): array - { - return array_merge( - $attributes, - ['rel' => 'external noopener noreferrer', 'target' => '_blank'] - ); - } -} +// This file has been replaced by UrlNodeTransformer as part of the migration +// from doctrine/rst-parser to phpdocumentor/guides. +// It is kept empty to avoid autoload errors during the transition. diff --git a/src/Renderers/TabsNodeRenderer.php b/src/Renderers/TabsNodeRenderer.php new file mode 100644 index 00000000..83246618 --- /dev/null +++ b/src/Renderers/TabsNodeRenderer.php @@ -0,0 +1,73 @@ + */ +class TabsNodeRenderer implements NodeRenderer, NodeRendererFactoryAware +{ + private ?NodeRendererFactory $nodeRendererFactory = null; + + public function __construct(private readonly TemplateRenderer $templateRenderer) + { + } + + public function setNodeRendererFactory(NodeRendererFactory $nodeRendererFactory): void + { + $this->nodeRendererFactory = $nodeRendererFactory; + } + + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === TabsNode::class || is_a($nodeFqcn, TabsNode::class, true); + } + + public function render(Node $node, RenderContext $renderContext): string + { + assert($node instanceof TabsNode); + assert($this->nodeRendererFactory !== null); + + $slugger = new AsciiSlugger(); + $tabs = []; + foreach ($node->getTabs() as $tabNode) { + assert($tabNode instanceof TabNode); + + $tabSlug = $slugger->slug($tabNode->getTabName())->lower()->toString(); + + $renderedContent = ''; + foreach ($tabNode->getChildren() as $child) { + $renderedContent .= $this->nodeRendererFactory->get($child)->render($child, $renderContext); + } + + $tabs[] = [ + 'label' => $tabNode->getTabName(), + 'slug' => $tabSlug, + 'hash' => hash('xxh128', $tabSlug.$renderedContent), + 'content' => $renderedContent, + ]; + } + + return $this->templateRenderer->renderTemplate( + $renderContext, + 'directives/tabs.html.twig', + [ + 'title' => $node->getTitle(), + 'tabs' => $tabs, + ] + ); + } +} diff --git a/src/Renderers/TitleNodeRenderer.php b/src/Renderers/TitleNodeRenderer.php index 10042c5d..e1b0e437 100644 --- a/src/Renderers/TitleNodeRenderer.php +++ b/src/Renderers/TitleNodeRenderer.php @@ -11,25 +11,23 @@ namespace SymfonyDocsBuilder\Renderers; -use Doctrine\RST\Environment; -use Doctrine\RST\Nodes\TitleNode; -use Doctrine\RST\Renderers\NodeRenderer; -use Doctrine\RST\Templates\TemplateRenderer; +use phpDocumentor\Guides\NodeRenderers\NodeRenderer; +use phpDocumentor\Guides\Nodes\Node; +use phpDocumentor\Guides\Nodes\TitleNode; +use phpDocumentor\Guides\RenderContext; +use phpDocumentor\Guides\TemplateRenderer; +use function assert; +use function is_a; + +/** @implements NodeRenderer */ class TitleNodeRenderer implements NodeRenderer { - /** @var TitleNode */ - private $titleNode; - - /** @var TemplateRenderer */ - private $templateRenderer; - - private static $idUsagesCountByFilename = []; + /** @var array> */ + private static array $idUsagesCountByFilename = []; - public function __construct(TitleNode $titleNode, TemplateRenderer $templateRenderer) + public function __construct(private readonly TemplateRenderer $templateRenderer) { - $this->titleNode = $titleNode; - $this->templateRenderer = $templateRenderer; } public static function resetHeaderIdCache(): void @@ -37,24 +35,47 @@ public static function resetHeaderIdCache(): void self::$idUsagesCountByFilename = []; } - public function render(): string + public function supports(string $nodeFqcn): bool + { + return $nodeFqcn === TitleNode::class || is_a($nodeFqcn, TitleNode::class, true); + } + + public function render(Node $node, RenderContext $renderContext): string { - $filename = $this->titleNode->getEnvironment()->getCurrentFileName(); - $id = $this->titleNode->getId(); + assert($node instanceof TitleNode); + + $filename = $renderContext->getCurrentFileName(); + $id = $node->getId(); $idUsagesCount = self::$idUsagesCountByFilename[$filename][$id] ?? 0; if (0 === $idUsagesCount) { - $computedId = $this->titleNode->getId(); + $computedId = $id; } else { - $computedId = Environment::slugify($this->titleNode->getValue()->getText().'-'.$idUsagesCount); + $computedId = self::slugify($node->toString().'-'.$idUsagesCount); } self::$idUsagesCountByFilename[$filename][$id] = $idUsagesCount + 1; - return $this->templateRenderer->render('header-title.html.twig', [ - 'titleNode' => $this->titleNode, - 'id' => $computedId, - ]); + return $this->templateRenderer->renderTemplate( + $renderContext, + 'header-title.html.twig', + [ + 'titleNode' => $node, + 'id' => $computedId, + ] + ); + } + + /** + * Simple slugification for header IDs, replacing the old Environment::slugify(). + */ + private static function slugify(string $text): string + { + $text = strtolower($text); + $text = preg_replace('/[^a-z0-9]+/', '-', $text); + $text = trim($text, '-'); + + return $text; } } diff --git a/src/SymfonyHTMLFormat.php b/src/SymfonyHTMLFormat.php index 16ea957d..57317f31 100644 --- a/src/SymfonyHTMLFormat.php +++ b/src/SymfonyHTMLFormat.php @@ -1,89 +1,8 @@ - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - namespace SymfonyDocsBuilder; -use Doctrine\RST\Formats\Format; -use Doctrine\RST\Nodes\CodeNode; -use Doctrine\RST\Nodes\SpanNode; -use Doctrine\RST\Nodes\TitleNode; -use Doctrine\RST\Renderers\CallableNodeRendererFactory; -use Doctrine\RST\Renderers\NodeRendererFactory; -use Doctrine\RST\Templates\TemplateRenderer; -use SymfonyDocsBuilder\CI\UrlChecker; -use Doctrine\RST\HTML\Renderers\SpanNodeRenderer as BaseSpanNodeRenderer; - -final class SymfonyHTMLFormat implements Format -{ - protected $templateRenderer; - private $htmlFormat; - /** @var UrlChecker|null */ - private $urlChecker; - private $symfonyVersion; - - public function __construct(TemplateRenderer $templateRenderer, Format $HTMLFormat, ?UrlChecker $urlChecker = null, ?string $symfonyVersion = null) - { - $this->templateRenderer = $templateRenderer; - $this->htmlFormat = $HTMLFormat; - $this->urlChecker = $urlChecker; - $this->symfonyVersion = $symfonyVersion; - } - - public function getFileExtension(): string - { - return Format::HTML; - } - - public function getDirectives(): array - { - return $this->htmlFormat->getDirectives(); - } - - /** - * @return NodeRendererFactory[] - */ - public function getNodeRendererFactories(): array - { - $nodeRendererFactories = $this->htmlFormat->getNodeRendererFactories(); - - $nodeRendererFactories[CodeNode::class] = new CallableNodeRendererFactory( - function (CodeNode $node) { - return new Renderers\CodeNodeRenderer( - $node, - $this->templateRenderer - ); - } - ); - - $nodeRendererFactories[SpanNode::class] = new CallableNodeRendererFactory( - function (SpanNode $node) { - return new Renderers\SpanNodeRenderer( - $node->getEnvironment(), - $node, - new BaseSpanNodeRenderer($node->getEnvironment(), $node, $this->templateRenderer), - $this->urlChecker, - $this->symfonyVersion - ); - } - ); - - $nodeRendererFactories[TitleNode::class] = new CallableNodeRendererFactory( - function (TitleNode $node) { - return new Renderers\TitleNodeRenderer( - $node, - $this->templateRenderer - ); - } - ); - - return $nodeRendererFactories; - } -} +// This file has been replaced by custom NodeRenderers registered in +// GuidesContainerFactory.php as part of the migration from +// doctrine/rst-parser to phpdocumentor/guides. +// It is kept empty to avoid autoload errors during the transition. diff --git a/src/Templates/default/html/directives/tabs.html.twig b/src/Templates/default/html/directives/tabs.html.twig new file mode 100644 index 00000000..4c7991f1 --- /dev/null +++ b/src/Templates/default/html/directives/tabs.html.twig @@ -0,0 +1,17 @@ +
+
+ {% for tab in tabs %} + + {% endfor %} +
+ + {% for tab in tabs %} +
+ {{ tab.content|raw }} +
+ {% endfor %} +
diff --git a/src/Templates/default/html/header-title.html.twig b/src/Templates/default/html/header-title.html.twig index 0ffbc460..6b980100 100644 --- a/src/Templates/default/html/header-title.html.twig +++ b/src/Templates/default/html/header-title.html.twig @@ -1 +1 @@ -{{ titleNode.value.render()|raw }} +{{ renderNode(titleNode.value) }} diff --git a/src/Templates/default/html/structure/layout.html.twig b/src/Templates/default/html/structure/layout.html.twig new file mode 100644 index 00000000..99539d59 --- /dev/null +++ b/src/Templates/default/html/structure/layout.html.twig @@ -0,0 +1,14 @@ + + + + {{ 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/Templates/rtd/html/header-title.html.twig b/src/Templates/rtd/html/header-title.html.twig index fe2b810e..c5e74af4 100644 --- a/src/Templates/rtd/html/header-title.html.twig +++ b/src/Templates/rtd/html/header-title.html.twig @@ -1,4 +1,4 @@ - {{ titleNode.value.render()|raw }} + {{ renderNode(titleNode.value) }} diff --git a/tests/AbstractIntegrationTest.php b/tests/AbstractIntegrationTest.php index 0c7819ec..e7f81137 100644 --- a/tests/AbstractIntegrationTest.php +++ b/tests/AbstractIntegrationTest.php @@ -2,7 +2,6 @@ namespace SymfonyDocsBuilder\Tests; -use Doctrine\RST\Builder; use PHPUnit\Framework\TestCase; use Symfony\Component\Filesystem\Filesystem; use SymfonyDocsBuilder\BuildConfig; diff --git a/tests/IntegrationTest.php b/tests/IntegrationTest.php index 07f37338..e14cc83a 100644 --- a/tests/IntegrationTest.php +++ b/tests/IntegrationTest.php @@ -9,15 +9,12 @@ 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\Filesystem\Filesystem; use Symfony\Component\Finder\Finder; +use SymfonyDocsBuilder\BuildConfig; use SymfonyDocsBuilder\DocBuilder; -use SymfonyDocsBuilder\KernelFactory; -use SymfonyDocsBuilder\Renderers\TitleNodeRenderer; class IntegrationTest extends AbstractIntegrationTest { @@ -89,23 +86,32 @@ public function integrationProvider() */ 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); + $rstContent = file_get_contents($sourceFile); + + $filesystem = new Filesystem(); + $tmpDir = sys_get_temp_dir().'/doc_builder_block_test_'.random_int(1, 100000000); + $filesystem->mkdir($tmpDir); + $filesystem->dumpFile($tmpDir.'/index.rst', $rstContent); + + $buildConfig = (new BuildConfig()) + ->setSymfonyVersion('4.0') + ->setContentIsString() + ->setContentDir($tmpDir) + ->setOutputDir($tmpDir.'/output') + ->disableBuildCache() + ->disableJsonFileGeneration() + ; + + $builder = new DocBuilder(); + $buildResult = $builder->build($buildConfig); + $actualHtml = $buildResult->getStringResult(); - $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(); @@ -116,8 +122,10 @@ public function testParseUnitBlock(string $blockName) $this->assertSame( $this->normalize($indenter->indent($expected)), - $this->normalize($indenter->indent(trim($actualCrawler->filter('body')->html()))) + $this->normalize($indenter->indent(trim($actualHtml ?? ''))) ); + + $filesystem->remove($tmpDir); } public function parserUnitBlockProvider() @@ -337,26 +345,38 @@ public function testParseString() RST; $htmlString = << -

Lorem ipsum dolor sit amet

-

Consectetur adipisicing elit, sed do eiusmod +

+

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
  • +
  • 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 + + +

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()); + $this->assertSame( + $this->normalize($htmlString), + $this->normalize((new DocBuilder())->buildString($rstString)->getStringResult()) + ); } private function normalize(string $str): string diff --git a/tests/JsonIntegrationTest.php b/tests/JsonIntegrationTest.php index b9ab0924..11009f0d 100644 --- a/tests/JsonIntegrationTest.php +++ b/tests/JsonIntegrationTest.php @@ -10,7 +10,6 @@ namespace SymfonyDocsBuilder\Tests; use SymfonyDocsBuilder\DocBuilder; -use SymfonyDocsBuilder\Renderers\TitleNodeRenderer; class JsonIntegrationTest extends AbstractIntegrationTest { @@ -39,8 +38,8 @@ public function getJsonTests() 'parents' => [], 'prev' => null, 'next' => [ - 'title' => 'Dashboards', - 'link' => 'dashboards.html', + 'title' => 'Design', + 'link' => 'design.html', ], 'title' => 'JSON Generation Test', ] @@ -49,15 +48,20 @@ public function getJsonTests() yield 'dashboards' => [ 'file' => 'dashboards', 'data' => [ - 'parents' => [], - 'prev' => [ - 'title' => 'JSON Generation Test', - 'link' => 'index.html', + 'parents' => [ + [ + 'title' => 'Design', + 'link' => 'design.html', + ], ], - 'next' => [ + 'prev' => [ 'title' => 'Design', 'link' => 'design.html', ], + 'next' => [ + 'title' => 'CRUD', + 'link' => 'crud.html', + ], 'title' => 'Dashboards', ] ]; @@ -67,12 +71,12 @@ public function getJsonTests() 'data' => [ 'parents' => [], 'prev' => [ - 'title' => 'Dashboards', - 'link' => 'dashboards.html', + 'title' => 'JSON Generation Test', + 'link' => 'index.html', ], 'next' => [ - 'title' => 'CRUD', - 'link' => 'crud.html', + 'title' => 'Dashboards', + 'link' => 'dashboards.html', ], 'title' => 'Design', 'toc_options' => [ @@ -137,8 +141,8 @@ public function getJsonTests() ], ], 'prev' => [ - 'title' => 'Design', - 'link' => 'design.html', + 'title' => 'Dashboards', + 'link' => 'dashboards.html', ], 'next' => [ 'title' => 'Design Sub-Page', diff --git a/tests/fixtures/expected/blocks/code-blocks/bash.html b/tests/fixtures/expected/blocks/code-blocks/bash.html index 499d0016..a68a8937 100644 --- a/tests/fixtures/expected/blocks/code-blocks/bash.html +++ b/tests/fixtures/expected/blocks/code-blocks/bash.html @@ -1,6 +1,6 @@
-
+
1
git clone git@github.com:symfony/symfony.git
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/code-caption.html b/tests/fixtures/expected/blocks/code-blocks/code-caption.html index a4eaf9d7..4fbd190c 100644 --- a/tests/fixtures/expected/blocks/code-blocks/code-caption.html +++ b/tests/fixtures/expected/blocks/code-blocks/code-caption.html @@ -1,44 +1,24 @@
-
config/routes.php
-
-
1
-
-       
-           
-               $
-               foo
-           =
-           'bar';
-   
-
+
config/routes.php
+
+
1
+
$foo = 'bar';
+
-
-
config/routes.php
-
-
1
-
-       
-           
-               $
-               foo
-           =
-           'bar';
-   
-
+
config/routes.php
+
+
1
+
$foo = 'bar';
+
-
-
-
1
+        
+
1
 2
 3
-
-       
-           --- a/src/Controller/DefaultController.php
-           +++ b/src/Controller/DefaultController.php
-           @@ -2,7 +2,9 @@
-       
-   
-
-
+
--- a/src/Controller/DefaultController.php
++++ b/src/Controller/DefaultController.php
+@@ -2,7 +2,9 @@
+
+ \ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/diff.html b/tests/fixtures/expected/blocks/code-blocks/diff.html index 0a020cf4..917e37ab 100644 --- a/tests/fixtures/expected/blocks/code-blocks/diff.html +++ b/tests/fixtures/expected/blocks/code-blocks/diff.html @@ -1,39 +1,30 @@
-
+
1
 2
 3
 4
 5
-
-            
-                + Added line
-                - Removed line
-                Normal line
+        
+ Added line
 - Removed line
-                + Added line
-            
-        
+ Normal line +- Removed line ++ Added line
-
-
+
1
 2
 3
 4
 5
 6
-
-            
-                Normal line
+        
Normal line
 + Added line
-                - Removed line
-                Normal line
 - Removed line
-                + Added line
-            
-        
+ Normal line +- Removed line ++ Added line
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/html-php.html b/tests/fixtures/expected/blocks/code-blocks/html-php.html index d77a92ef..ac081d99 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html-php.html +++ b/tests/fixtures/expected/blocks/code-blocks/html-php.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -23,6 +23,6 @@
     <body>
         <?php echo 'body'; ?>
     </body>
-</html>
-
+</html>
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/html-twig.html b/tests/fixtures/expected/blocks/code-blocks/html-twig.html index dbe31018..23eae5e2 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html-twig.html +++ b/tests/fixtures/expected/blocks/code-blocks/html-twig.html @@ -1,9 +1,8 @@
-
+
1
 2
{# some code #}
-<!-- some code -->
-
+<!-- some code -->
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/html.html b/tests/fixtures/expected/blocks/code-blocks/html.html index f35e7e3e..c80f8382 100644 --- a/tests/fixtures/expected/blocks/code-blocks/html.html +++ b/tests/fixtures/expected/blocks/code-blocks/html.html @@ -1,6 +1,6 @@
-
+
1
-
<!-- some code -->
-
+
<!-- some code -->
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/ini.html b/tests/fixtures/expected/blocks/code-blocks/ini.html index 01600a98..27910031 100644 --- a/tests/fixtures/expected/blocks/code-blocks/ini.html +++ b/tests/fixtures/expected/blocks/code-blocks/ini.html @@ -1,6 +1,6 @@
-
+
1
fetch = +refs/notes/*:refs/notes/*
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/php-annotations.html b/tests/fixtures/expected/blocks/code-blocks/php-annotations.html index d43ecf08..c6cba6c3 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-annotations.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-annotations.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -29,4 +29,4 @@
     protected $bankAccountNumber;
 }
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html index 1d6bce0b..9aacfe69 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-attributes.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-attributes.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -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)
-                {
-                }
+use Symfony\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 function someAction(#[CurrentUser, AttributeName('value')] User $user)
+    {
+    }
 }
-
+
\ No newline at end of file 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..ce57d80c 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html +++ b/tests/fixtures/expected/blocks/code-blocks/php-nested-comments.html @@ -1,6 +1,7 @@

You can do that by adding a "stamp" to your message:

+
-
+
1
 2
 3
@@ -11,62 +12,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),
+    ]);
+}
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/php.html b/tests/fixtures/expected/blocks/code-blocks/php.html index 163de59a..f27e1b17 100644 --- a/tests/fixtures/expected/blocks/code-blocks/php.html +++ b/tests/fixtures/expected/blocks/code-blocks/php.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -12,7 +12,7 @@
         
// config/routes.php
 namespace Symfony\Component\Routing\Loader\Configurator;
 
-    use App\Controller\CompanyController;
+use App\Controller\CompanyController;
 
 return static function (RoutingConfigurator $routes): void {
     $routes->add('about_us', ['nl' => '/over-ons', 'en' => '/about-us'])
@@ -21,7 +21,7 @@
     
-
+
1
 2
 3
@@ -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,13 +53,11 @@
             self::Right => $translator->trans('text_align.right.label', locale: $locale),
         };
     }
-}
-            
-        
+}
-
+
1
 2
 3
@@ -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,21 +89,18 @@
         $payload->getUserId(),
         $this->loadUser(...)
     );
-}
-        
+}
-
+
1
 2
 3
 4
-

-public function __construct(
+        
public function __construct(
     private string $username
-) {
-}
-        
+)
{ +}
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/terminal.html b/tests/fixtures/expected/blocks/code-blocks/terminal.html index 4567fdb7..73cc9be3 100644 --- a/tests/fixtures/expected/blocks/code-blocks/terminal.html +++ b/tests/fixtures/expected/blocks/code-blocks/terminal.html @@ -1,48 +1,30 @@
-
+
1
-
git clone git@github.com:symfony/symfony.git
+
git clone git@github.com:symfony/symfony.git
-
-
1
+        
+
1
 2
-
-           
-               $ cowsay
-               'eat more chicken'
-               $ cowsay
-               'mmmm'
-           
-       
-
+
$ cowsay 'eat more chicken'
+$ cowsay 'mmmm'
+
-
-
-
1
+        
+
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
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/text.html b/tests/fixtures/expected/blocks/code-blocks/text.html index fe77dea1..baaa6244 100644 --- a/tests/fixtures/expected/blocks/code-blocks/text.html +++ b/tests/fixtures/expected/blocks/code-blocks/text.html @@ -1,6 +1,6 @@
-
+
1
some text with special chars < > " & and some text with special chars already escaped < > " &
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/twig.html b/tests/fixtures/expected/blocks/code-blocks/twig.html index 83c841cf..4f6e7ccf 100644 --- a/tests/fixtures/expected/blocks/code-blocks/twig.html +++ b/tests/fixtures/expected/blocks/code-blocks/twig.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -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
+}}
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/xml.html b/tests/fixtures/expected/blocks/code-blocks/xml.html index 47722577..2b336d9c 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 -->
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/code-blocks/yaml.html b/tests/fixtures/expected/blocks/code-blocks/yaml.html index 2de3b7b2..85ad96ee 100644 --- a/tests/fixtures/expected/blocks/code-blocks/yaml.html +++ b/tests/fixtures/expected/blocks/code-blocks/yaml.html @@ -1,5 +1,5 @@
-
+
1
 2
 3
@@ -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
-
+
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/directives/admonition.html b/tests/fixtures/expected/blocks/directives/admonition.html index f8f22804..621ec216 100644 --- a/tests/fixtures/expected/blocks/directives/admonition.html +++ b/tests/fixtures/expected/blocks/directives/admonition.html @@ -1,5 +1,5 @@ -
-

- Some Admonition -

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

-
+
+

Some Admonition

+

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

+ +
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/directives/best-practice.html b/tests/fixtures/expected/blocks/directives/best-practice.html index a23604d2..2c35c758 100644 --- a/tests/fixtures/expected/blocks/directives/best-practice.html +++ b/tests/fixtures/expected/blocks/directives/best-practice.html @@ -1,5 +1,5 @@ -
-

- Best Practice -

Use the bcrypt encoder for hashing your users' passwords.

-
+
+ +

Use the bcrypt encoder for hashing your users' passwords.

+ +
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/directives/caution.html b/tests/fixtures/expected/blocks/directives/caution.html index 717c09f2..64cee031 100644 --- a/tests/fixtures/expected/blocks/directives/caution.html +++ b/tests/fixtures/expected/blocks/directives/caution.html @@ -1,6 +1,5 @@ -
-

- - Caution -

Using too many sidebars or caution directives can be distracting!

-
+
+ +

Using too many sidebars or caution directives can be distracting!

+ +
\ No newline at end of file diff --git a/tests/fixtures/expected/blocks/directives/class.html b/tests/fixtures/expected/blocks/directives/class.html index 22c16352..ab8483aa 100644 --- a/tests/fixtures/expected/blocks/directives/class.html +++ b/tests/fixtures/expected/blocks/directives/class.html @@ -1,6 +1,8 @@ -
  • list-item-1
  • -
  • list-item-2
  • -
  • list-item-3
  • +
      +
    • list-item-1
    • +
    • list-item-2
    • +
    • list-item-3
    -

    some text

    + +

    some text

    \ No newline at end of file diff --git a/tests/fixtures/expected/blocks/directives/configuration-block.html b/tests/fixtures/expected/blocks/directives/configuration-block.html index 4b940ce5..11a52632 100644 --- a/tests/fixtures/expected/blocks/directives/configuration-block.html +++ b/tests/fixtures/expected/blocks/directives/configuration-block.html @@ -1,24 +1,27 @@
    - - -
    - -
    -
    -
    -
    1
    -
    # app/config/services.yml
    + +
    -
    -
    -