* * Licensed under MIT, see LICENSE. */ declare(strict_types=1); namespace Grav\Plugin\PageToc; use Knp\Menu\ItemInterface; use Knp\Menu\Matcher\Matcher; use Knp\Menu\MenuFactory; use Knp\Menu\MenuItem; use Knp\Menu\Renderer\ListRenderer; use Knp\Menu\Renderer\RendererInterface; /** * Table Of Contents Generator generates TOCs from HTML Markup * * @author Casey McLaughlin */ class TocGenerator { use HtmlHelper; private const DEFAULT_NAME = 'TOC'; /** * @var MenuFactory */ private $menuFactory; /** * Constructor */ public function __construct() { $this->menuFactory = new MenuFactory(); } /** * Get Menu * * Returns a KNP Menu object, which can be traversed or rendered * * @param string $markup Content to get items fro $this->getItems($markup, $topLevel, $depth)m * @param int $topLevel Top Header (1 through 6) * @param int $depth Depth (1 through 6) * @return ItemInterface KNP Menu */ public function getMenu(string $markup, int $topLevel = 1, int $depth = 6): ItemInterface { $menu = $this->menuFactory->createItem(static::DEFAULT_NAME); if (trim($markup) == '') { return $menu; } $tagsToMatch = $this->determineHeaderTags($topLevel, $depth); $lastElem = $menu; $domDocument = $this->getHTMLParser($markup); foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $i => $node) { if (! $node->hasAttribute('id')) { continue; } $tagName = $node->tagName; $level = array_search(strtolower($tagName), $tagsToMatch) + 1; /** @var MenuItem $parent */ if ($level == 1) { $parent = $menu; } elseif ($level == $lastElem->getLevel()) { $parent = $lastElem->getParent(); } elseif ($level > $lastElem->getLevel()) { $parent = $lastElem; for ($i = $lastElem->getLevel(); $i < ($level - 1); $i++) { $parent = $parent->addChild(''); } } else { //if ($level < $lastElem->getLevel()) $parent = $lastElem->getParent(); while ($parent->getLevel() > $level - 1) { $parent = $parent->getParent(); } } $lastElem = $parent->addChild( $node->getAttribute('id'), [ 'label' => $node->getAttribute('title') ?: $node->textContent, 'uri' => '#' . $node->getAttribute('id') ] ); } return $this->trimMenu($menu); } /** * Trim empty items from the menu * * @param ItemInterface $menuItem * @return ItemInterface */ protected function trimMenu(ItemInterface $menuItem): ItemInterface { // if any of these circumstances are true, then just bail and return the menu item if ( count($menuItem->getChildren()) === 0 or count($menuItem->getChildren()) > 1 or ! empty($menuItem->getFirstChild()->getLabel()) ) { return $menuItem; } // otherwise, find the first level where there is actual content and use that. while (count($menuItem->getChildren()) == 1 && empty($menuItem->getFirstChild()->getLabel())) { $menuItem = $menuItem->getFirstChild(); } return $menuItem; } /** * Get HTML menu in unordered list form * * @param string $markup Content to get items from * @param int $topLevel Top Header (1 through 6) * @param int $depth Depth (1 through 6) * @param RendererInterface|null $renderer * @param bool $ordered * @return string HTML
  • items */ public function getHtmlMenu( string $markup, int $topLevel = 1, int $depth = 6, ?RendererInterface $renderer = null, bool $ordered = false ): string { if (! $renderer) { $options = ['currentClass' => 'active', 'ancestorClass' => 'active_ancestor']; $renderer = $ordered ? new OrderedListRenderer(new Matcher(), $options) : new ListRenderer(new Matcher(), $options); } $menu = $this->getMenu($markup, $topLevel, $depth); return $renderer->render($menu); } /** * Get HTML menu in ordered list form * * @param string $markup Content to get items from * @param int $topLevel Top Header (1 through 6) * @param int $depth Depth (1 through 6) * @param RendererInterface|null $renderer * @return string HTML
  • items */ public function getOrderedHtmlMenu( string $markup, int $topLevel = 1, int $depth = 6, RendererInterface $renderer = null ): string { return $this->getHtmlMenu($markup, $topLevel, $depth, $renderer, true); } }