From ac4184a50f2d0ed6ff2ff8d2a6d536be6007a39a Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Thu, 1 Apr 2021 12:54:59 +0200 Subject: [PATCH 1/5] Change TOC generation to make it more flexible --- src/DocsKernel.php | 7 ++ src/Generator/JsonGenerator.php | 27 +------ src/Generator/TocGenerator.php | 95 ++++++++++++++++++++++++ src/Listener/TocCustomizerListener.php | 55 ++++++++++++++ src/Templates/default/html/toc.html.twig | 7 ++ 5 files changed, 167 insertions(+), 24 deletions(-) create mode 100644 src/Generator/TocGenerator.php create mode 100644 src/Listener/TocCustomizerListener.php diff --git a/src/DocsKernel.php b/src/DocsKernel.php index f98b032..8761092 100644 --- a/src/DocsKernel.php +++ b/src/DocsKernel.php @@ -14,10 +14,12 @@ use Doctrine\RST\Configuration; use Doctrine\RST\ErrorManager; use Doctrine\RST\Event\PostBuildRenderEvent; +use Doctrine\RST\Event\PostNodeCreateEvent; use Doctrine\RST\Event\PreNodeRenderEvent; use Doctrine\RST\Kernel; use SymfonyDocsBuilder\Listener\AssetsCopyListener; use SymfonyDocsBuilder\Listener\CopyImagesListener; +use SymfonyDocsBuilder\Listener\TocCustomizerListener; class DocsKernel extends Kernel { @@ -47,6 +49,11 @@ private function initializeListeners(EventManager $eventManager, ErrorManager $e new CopyImagesListener($this->buildConfig, $errorManager) ); + $eventManager->addEventListener( + PreNodeRenderEvent::PRE_NODE_RENDER, + new TocCustomizerListener($this->buildConfig, $errorManager) + ); + if (!$this->buildConfig->getSubdirectoryToBuild()) { $eventManager->addEventListener( [PostBuildRenderEvent::POST_BUILD_RENDER], diff --git a/src/Generator/JsonGenerator.php b/src/Generator/JsonGenerator.php index 988528a..50c913e 100644 --- a/src/Generator/JsonGenerator.php +++ b/src/Generator/JsonGenerator.php @@ -11,7 +11,6 @@ namespace SymfonyDocsBuilder\Generator; -use Doctrine\RST\Environment; use Doctrine\RST\Meta\MetaEntry; use Doctrine\RST\Meta\Metas; use Symfony\Component\Console\Helper\ProgressBar; @@ -20,7 +19,6 @@ use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; use SymfonyDocsBuilder\BuildConfig; -use function Symfony\Component\String\u; class JsonGenerator { @@ -67,13 +65,15 @@ public function generateJson(string $masterDocument = 'index'): array $crawler = new Crawler(file_get_contents($this->buildConfig->getOutputDir().'/'.$filename.'.html')); + $tocGenerator = new TocGenerator($metaEntry); $next = $this->determineNext($parserFilename, $flattenedTocTree, $masterDocument); $prev = $this->determinePrev($parserFilename, $flattenedTocTree); $data = [ 'title' => $metaEntry->getTitle(), 'parents' => $this->determineParents($parserFilename, $tocTreeHierarchy) ?: [], 'current_page_name' => $parserFilename, - 'toc' => $this->generateToc($metaEntry, current($metaEntry->getTitles())[1]), + 'toc' => $tocGenerator->getToc(), + 'toc_num_items' => $tocGenerator->getNumItemsPerLevel(), 'next' => $next, 'prev' => $prev, 'body' => $crawler->filter('body')->html(), @@ -98,27 +98,6 @@ public function setOutput(SymfonyStyle $output) $this->output = $output; } - private function generateToc(MetaEntry $metaEntry, ?array $titles): array - { - if (null === $titles) { - return []; - } - - $tocTree = []; - - foreach ($titles as $title) { - $tocTree[] = [ - 'url' => sprintf('%s#%s', $metaEntry->getUrl(), Environment::slugify($title[0])), - 'page' => u($metaEntry->getUrl())->beforeLast('.html'), - 'fragment' => Environment::slugify($title[0]), - 'title' => $title[0], - 'children' => $this->generateToc($metaEntry, $title[1]), - ]; - } - - return $tocTree; - } - private function determineNext(string $parserFilename, array $flattenedTocTree): ?array { $foundCurrentFile = false; diff --git a/src/Generator/TocGenerator.php b/src/Generator/TocGenerator.php new file mode 100644 index 0000000..f01ced1 --- /dev/null +++ b/src/Generator/TocGenerator.php @@ -0,0 +1,95 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyDocsBuilder\Generator; + +use Doctrine\RST\Environment; +use Doctrine\RST\Meta\MetaEntry; +use function Symfony\Component\String\u; + +/** + * It encapsulates all the logic needed to generate the full TOC items + * from the given doc meta entries and provides some TOC utilities. + */ +class TocGenerator +{ + private $metaEntry; + private $cachedToc; + + public function __construct(MetaEntry $metaEntry) + { + $this->metaEntry = $metaEntry; + } + + public function getToc(): array + { + if (null !== $this->cachedToc) { + return $this->cachedToc; + } + + return $this->cachedToc = $this->doGenerateToc($this->metaEntry, current($this->metaEntry->getTitles())[1]); + } + + public function getFlatenedToc(): array + { + return $this->doFlattenToc($this->getToc()); + } + + /** + * Returns the number of TOC items per indentation level + * (which correspond to

,

,

, etc. elements) + */ + public function getNumItemsPerLevel(): array + { + $numItems = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0]; + foreach ($this->getFlatenedToc() as $tocItem) { + ++$numItems[$tocItem['level']]; + } + + $numItems['total'] = array_sum($numItems); + + return $numItems; + } + + private function doGenerateToc(MetaEntry $metaEntry, ?array $titles, $level = 1): array + { + if (null === $titles) { + return []; + } + + $toc = []; + foreach ($titles as $title) { + $toc[] = [ + 'url' => sprintf('%s#%s', $metaEntry->getUrl(), Environment::slugify($title[0])), + 'page' => u($metaEntry->getUrl())->beforeLast('.html')->toString(), + 'fragment' => Environment::slugify($title[0]), + 'title' => $title[0], + 'level' => $level, + 'children' => $this->doGenerateToc($metaEntry, $title[1], $level + 1), + ]; + } + + return $toc; + } + + private function doFlattenToc(array $toc, array &$flattenedToc = []): array + { + foreach ($toc as $item) { + $flattenedToc[] = $item; + + if ([] !== $item['children']) { + $this->doFlattenToc($item['children'], $flattenedToc); + } + } + + return $flattenedToc; + } +} diff --git a/src/Listener/TocCustomizerListener.php b/src/Listener/TocCustomizerListener.php new file mode 100644 index 0000000..3d82266 --- /dev/null +++ b/src/Listener/TocCustomizerListener.php @@ -0,0 +1,55 @@ + + * 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\TocNode; +use SymfonyDocsBuilder\BuildConfig; +use SymfonyDocsBuilder\Generator\TocGenerator; +use SymfonyDocsBuilder\Toc\TocHandler; + +class TocCustomizerListener +{ + 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 TocNode) { + return; + } + + $tocMaxDepth = $node->getDepth(); + $metaEntry = '???'; // <--- How can I get this? + $tocNumItems = (new TocGenerator($metaEntry))->getNumItemsPerLevel(); + + $numVisibleTocItems = 0; + foreach ($tocNumItems as $level => $numItemsInThisLevel) { + if ($level > $tocMaxDepth) { + break; + } + + $numVisibleTocItems += $numItemsInThisLevel; + } + + $tocSize = $numVisibleTocItems < 10 ? 'md' : ($numVisibleTocItems < 20 ? 'lg' : 'xl'); + $node->setClasses(array_merge($node->getClasses(), ['toc-size-'.$tocSize])); + } +} diff --git a/src/Templates/default/html/toc.html.twig b/src/Templates/default/html/toc.html.twig index cad7ccd..2b1f099 100644 --- a/src/Templates/default/html/toc.html.twig +++ b/src/Templates/default/html/toc.html.twig @@ -1,4 +1,11 @@ {% apply spaceless %} + {# this defines the size of the TOC (which depends on how many visible items that TOC has) + which is used to display TOCs differently (e.g. show large TOCs in 2 columns, etc.) #} + {% set is_first_deep_level = 1 == (toc_deep_level ?? 1) %} + {% if is_first_deep_level %} + {% set toc_size = numOfVisibleTocItems < 10 ? 'md' : numOfVisibleTocItems < 20 ? 'lg' : 'xl' %} + {% endif %} +
{% include "toc-level.html.twig" %}
From 014cc7db76d7a44ac037f194762435d3e85b398f Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 2 Apr 2021 13:32:19 +0200 Subject: [PATCH 2/5] - --- src/DocsKernel.php | 5 - src/Generator/JsonGenerator.php | 73 +++++++- src/Generator/TocGenerator.php | 95 ----------- src/Listener/TocCustomizerListener.php | 55 ------ src/Renderers/TocNodeRenderer.php | 179 ++++++++++++++++++++ src/SymfonyHTMLFormat.php | 11 ++ src/Templates/default/html/toc.html.twig | 9 +- tests/JsonIntegrationTest.php | 7 +- tests/fixtures/expected/build-pdf/book.html | 2 +- tests/fixtures/expected/main/index.html | 2 +- tests/fixtures/expected/toctree/index.html | 10 +- tests/fixtures/source/json/design.rst | 13 +- 12 files changed, 286 insertions(+), 175 deletions(-) delete mode 100644 src/Generator/TocGenerator.php delete mode 100644 src/Listener/TocCustomizerListener.php create mode 100644 src/Renderers/TocNodeRenderer.php diff --git a/src/DocsKernel.php b/src/DocsKernel.php index 8761092..315ec22 100644 --- a/src/DocsKernel.php +++ b/src/DocsKernel.php @@ -49,11 +49,6 @@ private function initializeListeners(EventManager $eventManager, ErrorManager $e new CopyImagesListener($this->buildConfig, $errorManager) ); - $eventManager->addEventListener( - PreNodeRenderEvent::PRE_NODE_RENDER, - new TocCustomizerListener($this->buildConfig, $errorManager) - ); - if (!$this->buildConfig->getSubdirectoryToBuild()) { $eventManager->addEventListener( [PostBuildRenderEvent::POST_BUILD_RENDER], diff --git a/src/Generator/JsonGenerator.php b/src/Generator/JsonGenerator.php index 50c913e..988c2a9 100644 --- a/src/Generator/JsonGenerator.php +++ b/src/Generator/JsonGenerator.php @@ -11,6 +11,7 @@ namespace SymfonyDocsBuilder\Generator; +use Doctrine\RST\Environment; use Doctrine\RST\Meta\MetaEntry; use Doctrine\RST\Meta\Metas; use Symfony\Component\Console\Helper\ProgressBar; @@ -19,6 +20,7 @@ use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; use SymfonyDocsBuilder\BuildConfig; +use function Symfony\Component\String\u; class JsonGenerator { @@ -65,15 +67,14 @@ public function generateJson(string $masterDocument = 'index'): array $crawler = new Crawler(file_get_contents($this->buildConfig->getOutputDir().'/'.$filename.'.html')); - $tocGenerator = new TocGenerator($metaEntry); $next = $this->determineNext($parserFilename, $flattenedTocTree, $masterDocument); $prev = $this->determinePrev($parserFilename, $flattenedTocTree); $data = [ 'title' => $metaEntry->getTitle(), 'parents' => $this->determineParents($parserFilename, $tocTreeHierarchy) ?: [], 'current_page_name' => $parserFilename, - 'toc' => $tocGenerator->getToc(), - 'toc_num_items' => $tocGenerator->getNumItemsPerLevel(), + 'toc' => $toc = $this->generateToc($metaEntry, current($metaEntry->getTitles())[1]), + 'toc_options' => $this->getTocOptions($toc), 'next' => $next, 'prev' => $prev, 'body' => $crawler->filter('body')->html(), @@ -98,6 +99,72 @@ public function setOutput(SymfonyStyle $output) $this->output = $output; } + private function generateToc(MetaEntry $metaEntry, ?array $titles, int $level = 1): array + { + if (null === $titles) { + return []; + } + + $tocTree = []; + + foreach ($titles as $title) { + $tocTree[] = [ + 'level' => $level, + 'url' => sprintf('%s#%s', $metaEntry->getUrl(), Environment::slugify($title[0])), + 'page' => u($metaEntry->getUrl())->beforeLast('.html'), + 'fragment' => Environment::slugify($title[0]), + 'title' => $title[0], + 'children' => $this->generateToc($metaEntry, $title[1], $level + 1), + ]; + } + + return $tocTree; + } + + private function getTocOptions(array $toc): array + { + $flattendToc = $this->flattenToc($toc); + $maxDepth = 0; + $numVisibleItems = 0; + foreach ($flattendToc as $tocItem) { + $maxDepth = max($maxDepth, $tocItem['level']); + $numVisibleItems++; + } + + return [ + 'maxDepth' => $maxDepth, + 'numVisibleItems' => $numVisibleItems, + 'size' => $this->getTocSize($numVisibleItems), + ]; + } + + // If you change this method, make the same change in TocNodeRenderer too + private function getTocSize(int $numVisibleItems): string + { + if ($numVisibleItems < 10) { + return 'md'; + } + + if ($numVisibleItems < 20) { + return 'lg'; + } + + return 'xl'; + } + + private function flattenToc(array $toc, array &$flattenedToc = []): array + { + foreach ($toc as $item) { + $flattenedToc[] = $item; + + if ([] !== $item['children']) { + $this->flattenToc($item['children'], $flattenedToc); + } + } + + return $flattenedToc; + } + private function determineNext(string $parserFilename, array $flattenedTocTree): ?array { $foundCurrentFile = false; diff --git a/src/Generator/TocGenerator.php b/src/Generator/TocGenerator.php deleted file mode 100644 index f01ced1..0000000 --- a/src/Generator/TocGenerator.php +++ /dev/null @@ -1,95 +0,0 @@ - - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace SymfonyDocsBuilder\Generator; - -use Doctrine\RST\Environment; -use Doctrine\RST\Meta\MetaEntry; -use function Symfony\Component\String\u; - -/** - * It encapsulates all the logic needed to generate the full TOC items - * from the given doc meta entries and provides some TOC utilities. - */ -class TocGenerator -{ - private $metaEntry; - private $cachedToc; - - public function __construct(MetaEntry $metaEntry) - { - $this->metaEntry = $metaEntry; - } - - public function getToc(): array - { - if (null !== $this->cachedToc) { - return $this->cachedToc; - } - - return $this->cachedToc = $this->doGenerateToc($this->metaEntry, current($this->metaEntry->getTitles())[1]); - } - - public function getFlatenedToc(): array - { - return $this->doFlattenToc($this->getToc()); - } - - /** - * Returns the number of TOC items per indentation level - * (which correspond to

,

,

, etc. elements) - */ - public function getNumItemsPerLevel(): array - { - $numItems = [1 => 0, 2 => 0, 3 => 0, 4 => 0, 5 => 0, 6 => 0]; - foreach ($this->getFlatenedToc() as $tocItem) { - ++$numItems[$tocItem['level']]; - } - - $numItems['total'] = array_sum($numItems); - - return $numItems; - } - - private function doGenerateToc(MetaEntry $metaEntry, ?array $titles, $level = 1): array - { - if (null === $titles) { - return []; - } - - $toc = []; - foreach ($titles as $title) { - $toc[] = [ - 'url' => sprintf('%s#%s', $metaEntry->getUrl(), Environment::slugify($title[0])), - 'page' => u($metaEntry->getUrl())->beforeLast('.html')->toString(), - 'fragment' => Environment::slugify($title[0]), - 'title' => $title[0], - 'level' => $level, - 'children' => $this->doGenerateToc($metaEntry, $title[1], $level + 1), - ]; - } - - return $toc; - } - - private function doFlattenToc(array $toc, array &$flattenedToc = []): array - { - foreach ($toc as $item) { - $flattenedToc[] = $item; - - if ([] !== $item['children']) { - $this->doFlattenToc($item['children'], $flattenedToc); - } - } - - return $flattenedToc; - } -} diff --git a/src/Listener/TocCustomizerListener.php b/src/Listener/TocCustomizerListener.php deleted file mode 100644 index 3d82266..0000000 --- a/src/Listener/TocCustomizerListener.php +++ /dev/null @@ -1,55 +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\TocNode; -use SymfonyDocsBuilder\BuildConfig; -use SymfonyDocsBuilder\Generator\TocGenerator; -use SymfonyDocsBuilder\Toc\TocHandler; - -class TocCustomizerListener -{ - 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 TocNode) { - return; - } - - $tocMaxDepth = $node->getDepth(); - $metaEntry = '???'; // <--- How can I get this? - $tocNumItems = (new TocGenerator($metaEntry))->getNumItemsPerLevel(); - - $numVisibleTocItems = 0; - foreach ($tocNumItems as $level => $numItemsInThisLevel) { - if ($level > $tocMaxDepth) { - break; - } - - $numVisibleTocItems += $numItemsInThisLevel; - } - - $tocSize = $numVisibleTocItems < 10 ? 'md' : ($numVisibleTocItems < 20 ? 'lg' : 'xl'); - $node->setClasses(array_merge($node->getClasses(), ['toc-size-'.$tocSize])); - } -} diff --git a/src/Renderers/TocNodeRenderer.php b/src/Renderers/TocNodeRenderer.php new file mode 100644 index 0000000..35da8aa --- /dev/null +++ b/src/Renderers/TocNodeRenderer.php @@ -0,0 +1,179 @@ + + * 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\Nodes\TocNode; +use Doctrine\RST\Renderers\NodeRenderer; +use Doctrine\RST\Templates\TemplateRenderer; + +class TocNodeRenderer implements NodeRenderer +{ + /** @var Environment */ + private $environment; + + /** @var TocNode */ + private $tocNode; + + /** @var TemplateRenderer */ + private $templateRenderer; + + public function __construct(Environment $environment, TocNode $tocNode, TemplateRenderer $templateRenderer) + { + $this->environment = $environment; + $this->tocNode = $tocNode; + $this->templateRenderer = $templateRenderer; + } + + public function render(): string + { + $options = $this->tocNode->getOptions(); + + if (isset($options['hidden'])) { + return ''; + } + + $tocItems = []; + + foreach ($this->tocNode->getFiles() as $file) { + $reference = $this->environment->resolve('doc', $file); + + if ($reference === null) { + continue; + } + + $url = $this->environment->relativeUrl($reference->getUrl()); + + $this->buildLevel($url, $reference->getTitles(), 1, $tocItems, $file); + } + + return $this->templateRenderer->render('toc.html.twig', [ + 'tocNode' => $this->tocNode, + 'tocItems' => $tocItems, + 'tocOptions' => $this->buildTocOptions($this->tocNode, $tocItems), + ]); + } + + private function buildTocOptions(TocNode $tocNode, array $tocItems): array + { + $maxDepth = $tocNode->getDepth(); + $numVisibleItems = 0; + foreach ($tocItems as $tocItem) { + if ($tocItem['level'] <= $maxDepth) { + $numVisibleItems++; + } + } + + return [ + 'maxDepth' => $maxDepth, + 'numVisibleItems' => $numVisibleItems, + 'size' => $this->getTocSize($numVisibleItems), + ]; + } + + // If you change this method, make the same change in JsonGenerator too + private function getTocSize(int $numVisibleItems): string + { + if ($numVisibleItems < 10) { + return 'md'; + } + + if ($numVisibleItems < 20) { + return 'lg'; + } + + return 'xl'; + } + + /** + * @param mixed[]|array $titles + * @param mixed[] $tocItems + */ + private function buildLevel( + ?string $url, + array $titles, + int $level, + array &$tocItems, + string $file + ): void { + foreach ($titles as $k => $entry) { + [$title, $children] = $entry; + + [$title, $target] = $this->generateTarget( + $url, + $title, + // don't add anchor for first h1 in a different file (link directly to the file) + ! ($level === 1 && $k === 0 && $file !== '/' . $this->environment->getCurrentFileName()) + ); + + $tocItem = [ + 'targetId' => $this->generateTargetId($target), + 'targetUrl' => $this->environment->generateUrl($target), + 'title' => $title, + 'level' => $level, + 'children' => [], + ]; + + // render children until we hit the configured maxdepth + if (count($children) > 0 && $level < $this->tocNode->getDepth()) { + $this->buildLevel($url, $children, $level + 1, $tocItem['children'], $file); + } + + $tocItems[] = $tocItem; + } + } + + private function generateTargetId(string $target): string + { + return Environment::slugify($target); + } + + /** + * @param string[]|string $title + * + * @return mixed[] + */ + private function generateTarget(?string $url, $title, bool $withAnchor): array + { + $target = $url; + if ($withAnchor) { + $anchor = $this->generateAnchorFromTitle($title); + $target .= '#' . $anchor; + } + + if (is_array($title)) { + [$title, $target] = $title; + + $reference = $this->environment->resolve('doc', $target); + + if ($reference === null) { + return [$title, $target]; + } + + $target = $this->environment->relativeUrl($reference->getUrl()); + } + + return [$title, $target]; + } + + /** + * @param string[]|string $title + */ + private function generateAnchorFromTitle($title): string + { + $slug = is_array($title) + ? $title[1] + : $title; + + return Environment::slugify($slug); + } +} diff --git a/src/SymfonyHTMLFormat.php b/src/SymfonyHTMLFormat.php index 9aafa57..a8d4bb6 100644 --- a/src/SymfonyHTMLFormat.php +++ b/src/SymfonyHTMLFormat.php @@ -14,6 +14,7 @@ use Doctrine\RST\Formats\Format; use Doctrine\RST\Nodes\CodeNode; use Doctrine\RST\Nodes\SpanNode; +use Doctrine\RST\Nodes\TocNode; use Doctrine\RST\Renderers\CallableNodeRendererFactory; use Doctrine\RST\Renderers\NodeRendererFactory; use Doctrine\RST\Templates\TemplateRenderer; @@ -53,6 +54,16 @@ public function getNodeRendererFactories(): array { $nodeRendererFactories = $this->htmlFormat->getNodeRendererFactories(); + $nodeRendererFactories[TocNode::class] = new CallableNodeRendererFactory( + function (TocNode $node) { + return new Renderers\TocNodeRenderer( + $node->getEnvironment(), + $node, + $this->templateRenderer + ); + } + ); + $nodeRendererFactories[CodeNode::class] = new CallableNodeRendererFactory( function (CodeNode $node) { return new Renderers\CodeNodeRenderer( diff --git a/src/Templates/default/html/toc.html.twig b/src/Templates/default/html/toc.html.twig index 2b1f099..d655feb 100644 --- a/src/Templates/default/html/toc.html.twig +++ b/src/Templates/default/html/toc.html.twig @@ -1,12 +1,5 @@ {% apply spaceless %} - {# this defines the size of the TOC (which depends on how many visible items that TOC has) - which is used to display TOCs differently (e.g. show large TOCs in 2 columns, etc.) #} - {% set is_first_deep_level = 1 == (toc_deep_level ?? 1) %} - {% if is_first_deep_level %} - {% set toc_size = numOfVisibleTocItems < 10 ? 'md' : numOfVisibleTocItems < 20 ? 'lg' : 'xl' %} - {% endif %} - -
+
{% include "toc-level.html.twig" %}
{% endapply %} diff --git a/tests/JsonIntegrationTest.php b/tests/JsonIntegrationTest.php index 0ef094d..7d3d07b 100644 --- a/tests/JsonIntegrationTest.php +++ b/tests/JsonIntegrationTest.php @@ -74,7 +74,12 @@ public function getJsonTests() 'link' => 'crud.html', ], 'title' => 'Design', - ] + 'toc_options' => [ + 'maxDepth' => 2, + 'numVisibleItems' => 3, + 'size' => 'md' + ], + ], ]; yield 'crud' => [ diff --git a/tests/fixtures/expected/build-pdf/book.html b/tests/fixtures/expected/build-pdf/book.html index deb53f8..a214463 100644 --- a/tests/fixtures/expected/build-pdf/book.html +++ b/tests/fixtures/expected/build-pdf/book.html @@ -6,7 +6,7 @@

Book

Here is a link to the main index

-
+
  • First page
  • Second page
  • diff --git a/tests/fixtures/expected/main/index.html b/tests/fixtures/expected/main/index.html index 57fc68e..1346373 100644 --- a/tests/fixtures/expected/main/index.html +++ b/tests/fixtures/expected/main/index.html @@ -10,7 +10,7 @@

    Some Test Docs!

    - +

    A header

    diff --git a/tests/fixtures/expected/toctree/index.html b/tests/fixtures/expected/toctree/index.html index 68b4a2f..e07a28e 100644 --- a/tests/fixtures/expected/toctree/index.html +++ b/tests/fixtures/expected/toctree/index.html @@ -9,11 +9,11 @@ diff --git a/tests/fixtures/source/json/design.rst b/tests/fixtures/source/json/design.rst index fd8feb9..b9a4537 100644 --- a/tests/fixtures/source/json/design.rst +++ b/tests/fixtures/source/json/design.rst @@ -4,10 +4,21 @@ Design Something that should not be left to most programmers to try to do. +Section 1 +--------- + The toctree below should affects the next/prev. The -first entry is effectively ignored, as it wasa already +first entry is effectively ignored, as it was already included by the toctree in index.rst (which is parsed first). +Subsection 1 +~~~~~~~~~~~~ + +This is a subsection of the first section. That's all. + +Section 2 +--------- + However, crud (which is ALSO included in the toctree in index.rst), WILL be read here, as the "crud" in index.rst has not been read yet (design comes first). Also, design/sub-page WILL be considered. From 7b5b9dd3a4e42cd39f78eac85d8d2de9ceb02655 Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 2 Apr 2021 13:33:31 +0200 Subject: [PATCH 3/5] - --- src/DocsKernel.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DocsKernel.php b/src/DocsKernel.php index 315ec22..f98b032 100644 --- a/src/DocsKernel.php +++ b/src/DocsKernel.php @@ -14,12 +14,10 @@ use Doctrine\RST\Configuration; use Doctrine\RST\ErrorManager; use Doctrine\RST\Event\PostBuildRenderEvent; -use Doctrine\RST\Event\PostNodeCreateEvent; use Doctrine\RST\Event\PreNodeRenderEvent; use Doctrine\RST\Kernel; use SymfonyDocsBuilder\Listener\AssetsCopyListener; use SymfonyDocsBuilder\Listener\CopyImagesListener; -use SymfonyDocsBuilder\Listener\TocCustomizerListener; class DocsKernel extends Kernel { From f0825ac5a8ade10173e3eab8e40cfe12eb3ef1df Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 2 Apr 2021 16:09:20 +0200 Subject: [PATCH 4/5] - --- src/Generator/JsonGenerator.php | 47 +----- src/KernelFactory.php | 2 + src/Renderers/TocNodeRenderer.php | 179 ----------------------- src/SymfonyHTMLFormat.php | 10 -- src/Templates/default/html/toc.html.twig | 3 +- src/Twig/TocExtension.php | 66 +++++++++ 6 files changed, 72 insertions(+), 235 deletions(-) delete mode 100644 src/Renderers/TocNodeRenderer.php create mode 100644 src/Twig/TocExtension.php diff --git a/src/Generator/JsonGenerator.php b/src/Generator/JsonGenerator.php index 988c2a9..6a087b6 100644 --- a/src/Generator/JsonGenerator.php +++ b/src/Generator/JsonGenerator.php @@ -20,6 +20,7 @@ use Symfony\Component\DomCrawler\Crawler; use Symfony\Component\Filesystem\Filesystem; use SymfonyDocsBuilder\BuildConfig; +use SymfonyDocsBuilder\Twig\TocExtension; use function Symfony\Component\String\u; class JsonGenerator @@ -74,7 +75,7 @@ public function generateJson(string $masterDocument = 'index'): array 'parents' => $this->determineParents($parserFilename, $tocTreeHierarchy) ?: [], 'current_page_name' => $parserFilename, 'toc' => $toc = $this->generateToc($metaEntry, current($metaEntry->getTitles())[1]), - 'toc_options' => $this->getTocOptions($toc), + 'toc_options' => TocExtension::getOptions($toc), 'next' => $next, 'prev' => $prev, 'body' => $crawler->filter('body')->html(), @@ -121,50 +122,6 @@ private function generateToc(MetaEntry $metaEntry, ?array $titles, int $level = return $tocTree; } - private function getTocOptions(array $toc): array - { - $flattendToc = $this->flattenToc($toc); - $maxDepth = 0; - $numVisibleItems = 0; - foreach ($flattendToc as $tocItem) { - $maxDepth = max($maxDepth, $tocItem['level']); - $numVisibleItems++; - } - - return [ - 'maxDepth' => $maxDepth, - 'numVisibleItems' => $numVisibleItems, - 'size' => $this->getTocSize($numVisibleItems), - ]; - } - - // If you change this method, make the same change in TocNodeRenderer too - private function getTocSize(int $numVisibleItems): string - { - if ($numVisibleItems < 10) { - return 'md'; - } - - if ($numVisibleItems < 20) { - return 'lg'; - } - - return 'xl'; - } - - private function flattenToc(array $toc, array &$flattenedToc = []): array - { - foreach ($toc as $item) { - $flattenedToc[] = $item; - - if ([] !== $item['children']) { - $this->flattenToc($item['children'], $flattenedToc); - } - } - - return $flattenedToc; - } - private function determineNext(string $parserFilename, array $flattenedTocTree): ?array { $foundCurrentFile = false; diff --git a/src/KernelFactory.php b/src/KernelFactory.php index c878b6c..80230c0 100644 --- a/src/KernelFactory.php +++ b/src/KernelFactory.php @@ -17,6 +17,7 @@ use SymfonyDocsBuilder\Directive as SymfonyDirectives; use SymfonyDocsBuilder\Reference as SymfonyReferences; use SymfonyDocsBuilder\Twig\AssetsExtension; +use SymfonyDocsBuilder\Twig\TocExtension; use function Symfony\Component\String\u; /** @@ -55,6 +56,7 @@ static function (string $path) use ($parseSubPath): bool { $twig = $configuration->getTemplateEngine(); $twig->addExtension(new AssetsExtension()); + $twig->addExtension(new TocExtension()); return new DocsKernel( $buildConfig, diff --git a/src/Renderers/TocNodeRenderer.php b/src/Renderers/TocNodeRenderer.php deleted file mode 100644 index 35da8aa..0000000 --- a/src/Renderers/TocNodeRenderer.php +++ /dev/null @@ -1,179 +0,0 @@ - - * 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\Nodes\TocNode; -use Doctrine\RST\Renderers\NodeRenderer; -use Doctrine\RST\Templates\TemplateRenderer; - -class TocNodeRenderer implements NodeRenderer -{ - /** @var Environment */ - private $environment; - - /** @var TocNode */ - private $tocNode; - - /** @var TemplateRenderer */ - private $templateRenderer; - - public function __construct(Environment $environment, TocNode $tocNode, TemplateRenderer $templateRenderer) - { - $this->environment = $environment; - $this->tocNode = $tocNode; - $this->templateRenderer = $templateRenderer; - } - - public function render(): string - { - $options = $this->tocNode->getOptions(); - - if (isset($options['hidden'])) { - return ''; - } - - $tocItems = []; - - foreach ($this->tocNode->getFiles() as $file) { - $reference = $this->environment->resolve('doc', $file); - - if ($reference === null) { - continue; - } - - $url = $this->environment->relativeUrl($reference->getUrl()); - - $this->buildLevel($url, $reference->getTitles(), 1, $tocItems, $file); - } - - return $this->templateRenderer->render('toc.html.twig', [ - 'tocNode' => $this->tocNode, - 'tocItems' => $tocItems, - 'tocOptions' => $this->buildTocOptions($this->tocNode, $tocItems), - ]); - } - - private function buildTocOptions(TocNode $tocNode, array $tocItems): array - { - $maxDepth = $tocNode->getDepth(); - $numVisibleItems = 0; - foreach ($tocItems as $tocItem) { - if ($tocItem['level'] <= $maxDepth) { - $numVisibleItems++; - } - } - - return [ - 'maxDepth' => $maxDepth, - 'numVisibleItems' => $numVisibleItems, - 'size' => $this->getTocSize($numVisibleItems), - ]; - } - - // If you change this method, make the same change in JsonGenerator too - private function getTocSize(int $numVisibleItems): string - { - if ($numVisibleItems < 10) { - return 'md'; - } - - if ($numVisibleItems < 20) { - return 'lg'; - } - - return 'xl'; - } - - /** - * @param mixed[]|array $titles - * @param mixed[] $tocItems - */ - private function buildLevel( - ?string $url, - array $titles, - int $level, - array &$tocItems, - string $file - ): void { - foreach ($titles as $k => $entry) { - [$title, $children] = $entry; - - [$title, $target] = $this->generateTarget( - $url, - $title, - // don't add anchor for first h1 in a different file (link directly to the file) - ! ($level === 1 && $k === 0 && $file !== '/' . $this->environment->getCurrentFileName()) - ); - - $tocItem = [ - 'targetId' => $this->generateTargetId($target), - 'targetUrl' => $this->environment->generateUrl($target), - 'title' => $title, - 'level' => $level, - 'children' => [], - ]; - - // render children until we hit the configured maxdepth - if (count($children) > 0 && $level < $this->tocNode->getDepth()) { - $this->buildLevel($url, $children, $level + 1, $tocItem['children'], $file); - } - - $tocItems[] = $tocItem; - } - } - - private function generateTargetId(string $target): string - { - return Environment::slugify($target); - } - - /** - * @param string[]|string $title - * - * @return mixed[] - */ - private function generateTarget(?string $url, $title, bool $withAnchor): array - { - $target = $url; - if ($withAnchor) { - $anchor = $this->generateAnchorFromTitle($title); - $target .= '#' . $anchor; - } - - if (is_array($title)) { - [$title, $target] = $title; - - $reference = $this->environment->resolve('doc', $target); - - if ($reference === null) { - return [$title, $target]; - } - - $target = $this->environment->relativeUrl($reference->getUrl()); - } - - return [$title, $target]; - } - - /** - * @param string[]|string $title - */ - private function generateAnchorFromTitle($title): string - { - $slug = is_array($title) - ? $title[1] - : $title; - - return Environment::slugify($slug); - } -} diff --git a/src/SymfonyHTMLFormat.php b/src/SymfonyHTMLFormat.php index a8d4bb6..b3991ca 100644 --- a/src/SymfonyHTMLFormat.php +++ b/src/SymfonyHTMLFormat.php @@ -54,16 +54,6 @@ public function getNodeRendererFactories(): array { $nodeRendererFactories = $this->htmlFormat->getNodeRendererFactories(); - $nodeRendererFactories[TocNode::class] = new CallableNodeRendererFactory( - function (TocNode $node) { - return new Renderers\TocNodeRenderer( - $node->getEnvironment(), - $node, - $this->templateRenderer - ); - } - ); - $nodeRendererFactories[CodeNode::class] = new CallableNodeRendererFactory( function (CodeNode $node) { return new Renderers\CodeNodeRenderer( diff --git a/src/Templates/default/html/toc.html.twig b/src/Templates/default/html/toc.html.twig index d655feb..55b8737 100644 --- a/src/Templates/default/html/toc.html.twig +++ b/src/Templates/default/html/toc.html.twig @@ -1,5 +1,6 @@ {% apply spaceless %} -
    + {% set toc_options = toc_options(tocItems) %} +
    {% include "toc-level.html.twig" %}
    {% endapply %} diff --git a/src/Twig/TocExtension.php b/src/Twig/TocExtension.php new file mode 100644 index 0000000..ebea8c8 --- /dev/null +++ b/src/Twig/TocExtension.php @@ -0,0 +1,66 @@ + + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SymfonyDocsBuilder\Twig; + +use Twig\Extension\AbstractExtension; +use Twig\TwigFunction; + +class TocExtension extends AbstractExtension +{ + public function getFunctions(): array + { + return [ + new TwigFunction('toc_options', [$this, 'getOptions']), + ]; + } + + public static function getOptions(array $toc): array + { + $flattendToc = self::flattenToc($toc); + $maxDepth = 0; + $numVisibleItems = 0; + foreach ($flattendToc as $tocItem) { + $maxDepth = max($maxDepth, $tocItem['level']); + $numVisibleItems++; + } + + return [ + 'maxDepth' => $maxDepth, + 'numVisibleItems' => $numVisibleItems, + 'size' => self::getTocSize($numVisibleItems), + ]; + } + + private static function flattenToc(array $toc, array &$flattenedToc = []): array + { + foreach ($toc as $item) { + $flattenedToc[] = $item; + + if ([] !== $item['children']) { + self::flattenToc($item['children'], $flattenedToc); + } + } + + return $flattenedToc; + } + + private static function getTocSize(int $numVisibleItems): string + { + if ($numVisibleItems < 10) { + return 'md'; + } + + if ($numVisibleItems < 20) { + return 'lg'; + } + + return 'xl'; + } +} From f3891bdf98d8d60997f701e708a0c88f6266ff6f Mon Sep 17 00:00:00 2001 From: Javier Eguiluz Date: Fri, 2 Apr 2021 16:11:23 +0200 Subject: [PATCH 5/5] - --- src/SymfonyHTMLFormat.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/SymfonyHTMLFormat.php b/src/SymfonyHTMLFormat.php index b3991ca..9aafa57 100644 --- a/src/SymfonyHTMLFormat.php +++ b/src/SymfonyHTMLFormat.php @@ -14,7 +14,6 @@ use Doctrine\RST\Formats\Format; use Doctrine\RST\Nodes\CodeNode; use Doctrine\RST\Nodes\SpanNode; -use Doctrine\RST\Nodes\TocNode; use Doctrine\RST\Renderers\CallableNodeRendererFactory; use Doctrine\RST\Renderers\NodeRendererFactory; use Doctrine\RST\Templates\TemplateRenderer;