wiki-grav/plugins/page-toc/classes/TocGenerator.php
2022-06-06 18:16:01 +02:00

180 lines
5.2 KiB
PHP

<?php
/**
* PageTOC
*
* This plugin allows creation of Table of Contents + Link Anchors
*
* Based on the original version https://github.com/caseyamcl/toc
* by Casey McLaughlin <caseyamcl@gmail.com>
*
* 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 <caseyamcl@gmail.com>
*/
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 <li> 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 <li> items
*/
public function getOrderedHtmlMenu(
string $markup,
int $topLevel = 1,
int $depth = 6,
RendererInterface $renderer = null
): string {
return $this->getHtmlMenu($markup, $topLevel, $depth, $renderer, true);
}
}