src/View/Helper/Navigation/AbstractMenu.php
<?php
/**
* This file is part of the mimmi20/mezzio-navigation-laminasviewrenderer package.
*
* Copyright (c) 2020-2024, Thomas Mueller <mimmi20@live.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
declare(strict_types = 1);
namespace Mimmi20\Mezzio\Navigation\LaminasView\View\Helper\Navigation;
use Laminas\I18n\Exception\RuntimeException;
use Laminas\Stdlib\Exception\InvalidArgumentException;
use Laminas\View\Exception;
use Laminas\View\Helper\EscapeHtmlAttr;
use Laminas\View\Model\ModelInterface;
use Mimmi20\LaminasView\Helper\PartialRenderer\Helper\PartialRendererInterface;
use Mimmi20\Mezzio\Navigation\ContainerInterface;
use Mimmi20\Mezzio\Navigation\Page\PageInterface;
use Mimmi20\NavigationHelper\ContainerParser\ContainerParserInterface;
use Mimmi20\NavigationHelper\Htmlify\HtmlifyInterface;
use Override;
use RecursiveIteratorIterator;
use function array_key_exists;
use function array_merge;
use function assert;
use function count;
use function get_debug_type;
use function implode;
use function is_array;
use function is_bool;
use function is_int;
use function is_string;
use function rtrim;
use function sprintf;
use function str_repeat;
use const PHP_EOL;
/**
* Helper for rendering menus from navigation containers.
*
* phpcs:disable SlevomatCodingStandard.Classes.TraitUseDeclaration.MultipleTraitsPerDeclaration
*/
abstract class AbstractMenu extends AbstractHelper implements MenuInterface
{
/**
* Whether page class should be applied to <li> element.
*/
protected bool $addClassToListItem = false;
/**
* Whether labels should be escaped.
*/
protected bool $escapeLabels = true;
/**
* Whether only active branch should be rendered.
*/
protected bool $onlyActiveBranch = false;
/**
* Partial view script to use for rendering menu.
*
* @var array<int, string>|ModelInterface|string|null
*/
protected array | ModelInterface | string | null $partial = null;
/**
* Whether parents should be rendered when only rendering active branch.
*/
protected bool $renderParents = true;
/**
* CSS class to use for the ul element.
*/
protected string $ulClass = 'navigation';
/**
* CSS class to use for the li elements.
*/
protected string $liClass = '';
/**
* CSS class to use for the active li element.
*/
protected string $liActiveClass = 'active';
/** @throws void */
public function __construct(
HtmlifyInterface $htmlify,
ContainerParserInterface $containerParser,
protected EscapeHtmlAttr $escaper,
private readonly PartialRendererInterface $renderer,
) {
parent::__construct($htmlify, $containerParser);
}
/**
* Renders menu.
*
* Implements {@link ViewHelperInterface::render()}.
*
* If a partial view is registered in the helper, the menu will be rendered
* using the given partial script. If no partial is registered, the menu
* will be rendered as an 'ul' element by the helper's internal method.
*
* @see renderPartial()
* @see renderMenu()
*
* @param ContainerInterface<PageInterface>|string|null $container [optional] container to render. Default is null, which indicates that the helper should render the container returned by {@link getContainer()}.
*
* @throws Exception\InvalidArgumentException
* @throws Exception\RuntimeException
*/
#[Override]
public function render(ContainerInterface | string | null $container = null): string
{
$partial = $this->getPartial();
if ($partial) {
return $this->renderPartial($container, $partial);
}
return $this->renderMenu($container);
}
/**
* Renders the given $container by invoking the partial view helper.
*
* The container will simply be passed on as a model to the view script
* as-is, and will be available in the partial script as 'container', e.g.
* <code>echo 'Number of pages: ', count($this->container);</code>.
*
* @param ContainerInterface<PageInterface>|string|null $container [optional] container to pass to view script. Default is to use the container registered in the helper.
* @param array<int, string>|ModelInterface|string|null $partial [optional] partial view script to use. Default is to use the partial registered in the helper. If an array is given, the first value is used for the partial view script.
*
* @throws Exception\RuntimeException if no partial provided
* @throws Exception\InvalidArgumentException if partial is invalid array
*/
#[Override]
public function renderPartial(
ContainerInterface | string | null $container = null,
array | ModelInterface | string | null $partial = null,
): string {
return $this->renderPartialModel([], $container, $partial);
}
/**
* Renders the given $container by invoking the partial view helper with the given parameters as the model.
*
* The container will simply be passed on as a model to the view script
* as-is, and will be available in the partial script as 'container', e.g.
* <code>echo 'Number of pages: ', count($this->container);</code>.
*
* Any parameters provided will be passed to the partial via the view model.
*
* @param array<int|string, mixed> $params
* @param ContainerInterface<PageInterface>|string|null $container [optional] container to pass to view script. Default is to use the container registered in the helper.
* @param array<int, string>|string|null $partial [optional] partial view script to use. Default is to use the partial registered in the helper. If an array is given, the first value is used for the partial view script.
*
* @throws Exception\RuntimeException if no partial provided
* @throws Exception\InvalidArgumentException if partial is invalid array
*/
#[Override]
public function renderPartialWithParams(
array $params = [],
ContainerInterface | string | null $container = null,
array | string | null $partial = null,
): string {
return $this->renderPartialModel($params, $container, $partial);
}
/**
* Returns an HTML string containing an 'a' element for the given page if
* the page's href is not empty, and a 'span' element if it is empty.
*
* Overrides {@link AbstractHelper::htmlify()}.
*
* @param PageInterface $page page to generate HTML for
* @param bool $escapeLabel Whether to escape the label
* @param bool $addClassToListItem Whether to add the page class to the list item
*
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
#[Override]
public function htmlify(PageInterface $page, bool $escapeLabel = true, bool $addClassToListItem = false): string
{
try {
return $this->htmlify->toHtml(static::class, $page, $escapeLabel, $addClassToListItem);
} catch (RuntimeException $e) {
throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
}
}
/**
* Sets a flag indicating whether labels should be escaped.
*
* @param bool $flag [optional] escape labels
*
* @throws void
*/
#[Override]
public function escapeLabels(bool $flag = true): static
{
$this->escapeLabels = $flag;
return $this;
}
/** @throws void */
public function getEscapeLabels(): bool
{
return $this->escapeLabels;
}
/**
* Enables/disables page class applied to <li> element.
*
* @param bool $flag [optional] page class applied to <li> element Default
* is true
*
* @return static fluent interface
*
* @throws void
*/
#[Override]
public function setAddClassToListItem(bool $flag = true): static
{
$this->addClassToListItem = $flag;
return $this;
}
/**
* Returns flag indicating whether page class should be applied to <li> element.
*
* By default, this value is false.
*
* @return bool whether parents should be rendered
*
* @throws void
*/
#[Override]
public function getAddClassToListItem(): bool
{
return $this->addClassToListItem;
}
/**
* Sets a flag indicating whether only active branch should be rendered.
*
* @param bool $flag [optional] render only active branch
*
* @throws void
*/
#[Override]
public function setOnlyActiveBranch(bool $flag = true): static
{
$this->onlyActiveBranch = $flag;
return $this;
}
/**
* Returns a flag indicating whether only active branch should be rendered.
*
* By default, this value is false, meaning the entire menu will be rendered.
*
* @throws void
*/
#[Override]
public function getOnlyActiveBranch(): bool
{
return $this->onlyActiveBranch;
}
/**
* Sets which partial view script to use for rendering menu.
*
* @param array<int, string>|ModelInterface|string|null $partial partial view script or null. If an array is given, the first value is used for the partial view script.
*
* @throws void
*
* @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
*/
#[Override]
public function setPartial($partial): static
{
if (
$partial === null
|| is_string($partial)
|| is_array($partial)
|| $partial instanceof ModelInterface
) {
$this->partial = $partial;
}
return $this;
}
/**
* Returns partial view script to use for rendering menu.
*
* @return array<int, string>|ModelInterface|string|null
*
* @throws void
*/
#[Override]
public function getPartial(): array | ModelInterface | string | null
{
return $this->partial;
}
/**
* Enables/disables rendering of parents when only rendering active branch.
*
* See {@link setOnlyActiveBranch()} for more information.
*
* @param bool $flag [optional] render parents when rendering active branch
*
* @throws void
*/
#[Override]
public function setRenderParents(bool $flag = true): static
{
$this->renderParents = $flag;
return $this;
}
/**
* Returns flag indicating whether parents should be rendered when rendering only the active branch.
*
* By default, this value is true.
*
* @throws void
*/
#[Override]
public function getRenderParents(): bool
{
return $this->renderParents;
}
/**
* Sets CSS class to use for the first 'ul' element when rendering.
*
* @param string $ulClass CSS class to set
*
* @throws void
*/
#[Override]
public function setUlClass(string $ulClass): static
{
$this->ulClass = $ulClass;
return $this;
}
/**
* Returns CSS class to use for the first 'ul' element when rendering.
*
* @throws void
*/
#[Override]
public function getUlClass(): string
{
return $this->ulClass;
}
/**
* Sets CSS class to use for the 'li' elements when rendering.
*
* @param string $liClass CSS class to set
*
* @throws void
*/
#[Override]
public function setLiClass(string $liClass): static
{
$this->liClass = $liClass;
return $this;
}
/**
* Returns CSS class to use for the 'li' elements when rendering.
*
* @throws void
*/
#[Override]
public function getLiClass(): string
{
return $this->liClass;
}
/**
* Sets CSS class to use for the active 'li' element when rendering.
*
* @param string $liActiveClass CSS class to set
*
* @throws void
*/
#[Override]
public function setLiActiveClass(string $liActiveClass): static
{
$this->liActiveClass = $liActiveClass;
return $this;
}
/**
* Returns CSS class to use for the active 'li' element when rendering.
*
* @throws void
*/
#[Override]
public function getLiActiveClass(): string
{
return $this->liActiveClass;
}
/**
* Renders helper.
*
* Renders a HTML 'ul' for the given $container. If $container is not given,
* the container registered in the helper will be used.
*
* Available $options:
*
* @param ContainerInterface<PageInterface>|string|null $container [optional] container to create menu from. Default is to use the container retrieved from {@link getContainer()}.
* @param array<string, bool|int|string|null> $options [optional] options for controlling rendering
* @phpstan-param array{indent?: int|string|null, ulClass?: string|null, liClass?: string|null, minDepth?: int|null, maxDepth?: int|null, onlyActiveBranch?: bool, renderParents?: bool, escapeLabels?: bool, addClassToListItem?: bool, liActiveClass?: string|null} $options
*
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
#[Override]
public function renderMenu(ContainerInterface | string | null $container = null, array $options = []): string
{
try {
$container = $this->containerParser->parseContainer($container);
} catch (InvalidArgumentException $e) {
throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if ($container === null) {
$container = $this->getContainer();
}
$options = $this->normalizeOptions($options);
assert(is_string($options['ulClass']));
assert(is_string($options['liClass']));
assert(is_string($options['indent']));
assert(is_int($options['minDepth']));
assert(is_bool($options['onlyActiveBranch']));
assert(is_bool($options['renderParents']));
assert(is_bool($options['escapeLabels']));
assert(is_bool($options['addClassToListItem']));
assert(is_string($options['liActiveClass']));
assert($container instanceof ContainerInterface);
if ($options['onlyActiveBranch'] && !$options['renderParents']) {
return $this->renderDeepestMenu(
container: $container,
ulClass: $options['ulClass'],
liCssClass: $options['liClass'],
indent: $options['indent'],
minDepth: $options['minDepth'],
maxDepth: $options['maxDepth'],
escapeLabels: $options['escapeLabels'],
addClassToListItem: $options['addClassToListItem'],
liActiveClass: $options['liActiveClass'],
);
}
return $this->renderNormalMenu(
container: $container,
ulClass: $options['ulClass'],
liCssClass: $options['liClass'],
indent: $options['indent'],
minDepth: $options['minDepth'],
maxDepth: $options['maxDepth'],
onlyActive: $options['onlyActiveBranch'],
escapeLabels: $options['escapeLabels'],
addClassToListItem: $options['addClassToListItem'],
liActiveClass: $options['liActiveClass'],
);
}
/**
* Renders the inner-most sub menu for the active page in the $container.
*
* This is a convenience method which is equivalent to the following call:
* <code>
* renderMenu($container, array(
* 'indent' => $indent,
* 'ulClass' => $ulClass,
* 'liClass' => $liClass,
* 'minDepth' => null,
* 'maxDepth' => null,
* 'onlyActiveBranch' => true,
* 'renderParents' => false,
* 'liActiveClass' => $liActiveClass
* ));
* </code>
*
* @param ContainerInterface<PageInterface>|null $container [optional] container to render. Default is to render the container registered in the helper.
* @param string|null $ulClass [optional] CSS class to use for UL element. Default is to use the value from {@link getUlClass()}.
* @param string|null $liClass [optional] CSS class to use for LI elements. Default is to use the value from {@link getLiClass()}.
* @param int|string|null $indent [optional] indentation as a string or number of spaces. Default is to use the value retrieved from {@link getIndent()}.
* @param string|null $liActiveClass [optional] CSS class to use for UL
*
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
#[Override]
public function renderSubMenu(
ContainerInterface | null $container = null,
string | null $ulClass = null,
string | null $liClass = null,
int | string | null $indent = null,
string | null $liActiveClass = null,
): string {
$this->setMaxDepth(null);
$this->setMinDepth(null);
$this->setRenderParents(false);
$this->setAddClassToListItem(false);
return $this->renderMenu(
$container,
[
'indent' => $indent,
'ulClass' => $ulClass,
'liClass' => $liClass,
'onlyActiveBranch' => true,
'escapeLabels' => true,
'liActiveClass' => $liActiveClass,
],
);
}
/**
* Normalizes given render options.
*
* @param array<string, bool|int|string|null> $options [optional] options for controlling rendering
* @phpstan-param array{indent?: int|string|null, ulClass?: string|null, liClass?: string|null, minDepth?: int|null, maxDepth?: int|null, onlyActiveBranch?: bool, renderParents?: bool, escapeLabels?: bool, addClassToListItem?: bool, liActiveClass?: string|null} $options
*
* @return array<string, bool|int|string|null>
* @phpstan-return array{indent: string, ulClass: string, liClass: string, minDepth: int|null, maxDepth: int|null, onlyActiveBranch: bool, renderParents: bool, escapeLabels: bool, addClassToListItem: bool, liActiveClass: string}
*
* @throws void
*/
protected function normalizeOptions(array $options = []): array
{
if (isset($options['indent'])) {
assert(is_int($options['indent']) || is_string($options['indent']));
$options['indent'] = $this->getWhitespace($options['indent']);
} else {
$options['indent'] = $this->getIndent();
}
if (!array_key_exists('ulClass', $options) || $options['ulClass'] === null) {
$options['ulClass'] = $this->getUlClass();
}
if (!array_key_exists('liClass', $options) || $options['liClass'] === null) {
$options['liClass'] = $this->getLiClass();
}
if (!array_key_exists('minDepth', $options) || $options['minDepth'] === null) {
$options['minDepth'] = $this->getMinDepth();
}
if ($options['minDepth'] < 0 || $options['minDepth'] === null) {
$options['minDepth'] = 0;
}
if (!array_key_exists('maxDepth', $options) || $options['maxDepth'] === null) {
$options['maxDepth'] = $this->getMaxDepth();
}
if (!array_key_exists('onlyActiveBranch', $options)) {
$options['onlyActiveBranch'] = $this->getOnlyActiveBranch();
}
if (!array_key_exists('escapeLabels', $options)) {
$options['escapeLabels'] = $this->getEscapeLabels();
}
if (!array_key_exists('renderParents', $options)) {
$options['renderParents'] = $this->getRenderParents();
}
if (!array_key_exists('addClassToListItem', $options)) {
$options['addClassToListItem'] = $this->getAddClassToListItem();
}
if (!array_key_exists('liActiveClass', $options) || $options['liActiveClass'] === null) {
$options['liActiveClass'] = $this->getLiActiveClass();
}
return $options;
}
/**
* @param array<string, int|PageInterface|null> $found
* @phpstan-param array{page?: PageInterface|null, depth?: int|null} $found
*
* @throws void
*/
protected function isActiveBranch(array $found, PageInterface $page, int | null $maxDepth): bool
{
if (!array_key_exists('page', $found) || !($found['page'] instanceof PageInterface)) {
return false;
}
$foundPage = $found['page'];
$foundDepth = $found['depth'] ?? 0;
if ($foundPage->hasPage($page)) {
// accept if page is a direct child of the active page
return true;
}
if (
$foundPage->getParent() instanceof ContainerInterface
&& $foundPage->getParent()->hasPage($page)
) {
// page is a sibling of the active page...
if (
!$foundPage->hasPages(!$this->renderInvisible)
|| is_int($maxDepth) && $foundDepth + 1 > $maxDepth
) {
// accept if active page has no children, or the
// children are too deep to be rendered
return true;
}
}
return false;
}
/**
* Render a partial with the given "model".
*
* @param array<int|string, mixed> $params
* @param ContainerInterface<PageInterface>|string|null $container
* @param array<int, string>|ModelInterface|string|null $partial
*
* @throws Exception\RuntimeException if no partial provided
* @throws Exception\InvalidArgumentException if partial is invalid array
*/
private function renderPartialModel(
array $params,
ContainerInterface | string | null $container,
array | ModelInterface | string | null $partial,
): string {
if ($partial === null) {
$partial = $this->getPartial();
}
if ($partial === null || $partial === '' || $partial === []) {
throw new Exception\RuntimeException(
'Unable to render menu: No partial view script provided',
);
}
if (is_array($partial)) {
if (count($partial) !== 2) {
throw new Exception\InvalidArgumentException(
'Unable to render menu: A view partial supplied as '
. 'an array must contain one value: the partial view script',
);
}
$partial = $partial[0];
}
try {
$container = $this->containerParser->parseContainer($container);
} catch (InvalidArgumentException $e) {
throw new Exception\InvalidArgumentException($e->getMessage(), $e->getCode(), $e);
}
if ($container === null) {
$container = $this->getContainer();
}
return $this->renderer->render(
$partial,
array_merge($params, ['container' => $container]),
);
}
/**
* Renders the deepest active menu within [$minDepth, $maxDepth], (called from {@link renderMenu()}).
*
* @param ContainerInterface<PageInterface> $container container to render
* @param string $ulClass CSS class for first UL
* @param string $liCssClass CSS class for all LI
* @param string $indent initial indentation
* @param int $minDepth minimum depth
* @param int|null $maxDepth maximum depth
* @param bool $escapeLabels Whether or not to escape the labels
* @param bool $addClassToListItem Whether or not page class applied to <li> element
* @param string $liActiveClass CSS class for active LI
*
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
private function renderDeepestMenu(
ContainerInterface $container,
string $ulClass,
string $liCssClass,
string $indent,
int $minDepth,
int | null $maxDepth,
bool $escapeLabels,
bool $addClassToListItem,
string $liActiveClass,
): string {
$active = $this->findActive($container, $minDepth - 1, $maxDepth);
if (!array_key_exists('page', $active) || !($active['page'] instanceof PageInterface)) {
return '';
}
$activePage = $active['page'];
// special case if active page is one below minDepth
if (!array_key_exists('depth', $active) || $active['depth'] < $minDepth) {
if (!$activePage->hasPages(!$this->renderInvisible)) {
return '';
}
} elseif (!$active['page']->hasPages(!$this->renderInvisible)) {
// found pages has no children; render siblings
$activePage = $active['page']->getParent();
} elseif (is_int($maxDepth) && $active['depth'] + 1 > $maxDepth) {
// children are below max depth; render siblings
$activePage = $active['page']->getParent();
}
$ulClass = $ulClass ? ' class="' . ($this->escaper)($ulClass) . '"' : '';
$html = $indent . '<ul' . $ulClass . '>' . PHP_EOL;
assert(
$activePage instanceof ContainerInterface,
sprintf(
'$activePage should be an Instance of %s, but was %s',
ContainerInterface::class,
get_debug_type($activePage),
),
);
foreach ($activePage as $subPage) {
if (!$this->accept($subPage)) {
continue;
}
// render li tag and page
$liClasses = [];
// Is page active?
if ($subPage->isActive(true)) {
$liClasses[] = $liActiveClass;
}
if ($liCssClass) {
$liClasses[] = $liCssClass;
}
if ($subPage->getLiClass()) {
$liClasses[] = $subPage->getLiClass();
}
// Add CSS class from page to <li>
if ($addClassToListItem && $subPage->getClass()) {
$liClasses[] = $subPage->getClass();
}
$liClass = $liClasses === []
? ''
: ' class="' . ($this->escaper)(implode(' ', $liClasses)) . '"';
$html .= $indent . ' <li' . $liClass . '>' . PHP_EOL;
try {
$subPageHtml = $this->htmlify->toHtml(
static::class,
$subPage,
$escapeLabels,
$addClassToListItem,
);
} catch (RuntimeException $e) {
throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
}
$html .= $indent . ' ' . $subPageHtml . PHP_EOL;
$html .= $indent . ' </li>' . PHP_EOL;
}
return $html . $indent . '</ul>';
}
/**
* Renders a normal menu (called from {@link renderMenu()}).
*
* @param ContainerInterface<PageInterface> $container container to render
* @param string $ulClass CSS class for first UL
* @param string $liCssClass CSS class for all LI
* @param string $indent initial indentation
* @param int|null $minDepth minimum depth
* @param int|null $maxDepth maximum depth
* @param bool $onlyActive render only active branch?
* @param bool $escapeLabels Whether or not to escape the labels
* @param bool $addClassToListItem Whether or not page class applied to <li> element
* @param string $liActiveClass CSS class for active LI
*
* @throws Exception\RuntimeException
* @throws Exception\InvalidArgumentException
*/
private function renderNormalMenu(
ContainerInterface $container,
string $ulClass,
string $liCssClass,
string $indent,
int | null $minDepth,
int | null $maxDepth,
bool $onlyActive,
bool $escapeLabels,
bool $addClassToListItem,
string $liActiveClass,
): string {
$html = '';
// find deepest active
$found = $this->findActive($container, $minDepth, $maxDepth);
// create iterator
$iterator = new RecursiveIteratorIterator($container, RecursiveIteratorIterator::SELF_FIRST);
if (is_int($maxDepth)) {
$iterator->setMaxDepth($maxDepth);
}
// iterate container
$prevDepth = -1;
foreach ($iterator as $page) {
assert(
$page instanceof PageInterface,
sprintf(
'$page should be an Instance of %s, but was %s',
PageInterface::class,
get_debug_type($page),
),
);
$depth = $iterator->getDepth();
if ($depth < $minDepth || !$this->accept($page)) {
// page is below minDepth or not accepted by Authorization/Visibility
continue;
}
$isActive = $page->isActive(true);
if ($onlyActive && !$isActive) {
// page is not active itself, but might be in the active branch
$accept = $this->isActiveBranch($found, $page, $maxDepth);
if (!$accept) {
continue;
}
}
// make sure indentation is correct
$depth -= $minDepth;
$myIndent = $indent . str_repeat(' ', $depth);
if ($depth > $prevDepth) {
// start new ul tag
$ulClass = $ulClass && $depth === 0 ? ' class="' . ($this->escaper)($ulClass) . '"' : '';
$html .= $myIndent . '<ul' . $ulClass . '>' . PHP_EOL;
} elseif ($prevDepth > $depth) {
// close li/ul tags until we're at current depth
for ($i = $prevDepth; $i > $depth; --$i) {
$ind = $indent . str_repeat(' ', $i);
$html .= $ind . ' </li>' . PHP_EOL;
$html .= $ind . '</ul>' . PHP_EOL;
}
// close previous li tag
$html .= $myIndent . ' </li>' . PHP_EOL;
} else {
// close previous li tag
$html .= $myIndent . ' </li>' . PHP_EOL;
}
// render li tag and page
$liClasses = [];
// Is page active?
if ($isActive) {
$liClasses[] = $liActiveClass;
}
if ($liCssClass) {
$liClasses[] = $liCssClass;
}
if ($page->getLiClass()) {
$liClasses[] = $page->getLiClass();
}
// Add CSS class from page to <li>
if ($addClassToListItem && $page->getClass()) {
$liClasses[] = $page->getClass();
}
$liClass = $liClasses === []
? ''
: ' class="' . ($this->escaper)(implode(' ', $liClasses)) . '"';
try {
$pageHtml = $this->htmlify->toHtml(
static::class,
$page,
$escapeLabels,
$addClassToListItem,
);
} catch (RuntimeException $e) {
throw new Exception\RuntimeException($e->getMessage(), $e->getCode(), $e);
}
$html .= $myIndent . ' <li' . $liClass . '>' . PHP_EOL
. $myIndent . ' ' . $pageHtml . PHP_EOL;
// store as previous depth for next iteration
$prevDepth = $depth;
}
if ($html) {
// done iterating container; close open ul/li tags
for ($i = $prevDepth + 1; 0 < $i; --$i) {
$myIndent = $indent . str_repeat(' ', $i - 1);
$html .= $myIndent . ' </li>' . PHP_EOL
. $myIndent . '</ul>' . PHP_EOL;
}
$html = rtrim($html, PHP_EOL);
}
return $html;
}
}