From df0cab306ce8fb2a1b870046441ded6ff1ed95f8 Mon Sep 17 00:00:00 2001 From: Casey McLaughlin Date: Tue, 1 Oct 2019 15:54:44 -0400 Subject: [PATCH] Trim empty levels from top of menu tree (fixes #1) --- CHANGELOG.md | 1 + src/TocGenerator.php | 33 +++++++++++++++++++++++--- tests/TocGeneratorTest.php | 47 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 76 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1ae0135..4ec04a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ All notable changes to this project are documented in this file. ### Fixed - Several issues in the README (typos, etc) - Version number in COPYRIGHT notice +- Empty levels are now automatically trimmed from the generated output (fixes #1) ### Changed - Updated PHP requirements to modern versions (7.1+) diff --git a/src/TocGenerator.php b/src/TocGenerator.php index bbab80d..c3707f7 100644 --- a/src/TocGenerator.php +++ b/src/TocGenerator.php @@ -36,6 +36,8 @@ class TocGenerator { use HtmlHelper; + private const DEFAULT_NAME = 'TOC'; + /** * @var HTML5 */ @@ -71,7 +73,7 @@ public function __construct(MenuFactory $menuFactory = null, HTML5 $htmlParser = public function getMenu(string $markup, int $topLevel = 1, int $depth = 6): ItemInterface { // Setup an empty menu object - $menu = $this->menuFactory->createItem('TOC'); + $menu = $this->menuFactory->createItem(static::DEFAULT_NAME); // Empty? Return empty menu item if (trim($markup) == '') { @@ -86,7 +88,7 @@ public function getMenu(string $markup, int $topLevel = 1, int $depth = 6): Item // Do it... $domDocument = $this->domParser->loadHTML($markup); - foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $node) { + foreach ($this->traverseHeaderTags($domDocument, $topLevel, $depth) as $i => $node) { // Skip items without IDs if (! $node->hasAttribute('id')) { continue; @@ -123,7 +125,32 @@ public function getMenu(string $markup, int $topLevel = 1, int $depth = 6): Item ); } - return $menu; + 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; } /** diff --git a/tests/TocGeneratorTest.php b/tests/TocGeneratorTest.php index cacb4f9..4a17b81 100644 --- a/tests/TocGeneratorTest.php +++ b/tests/TocGeneratorTest.php @@ -19,6 +19,7 @@ namespace TOC; +use Knp\Menu\ItemInterface; use PHPUnit\Framework\TestCase; use TOC\Util\TOCTestUtils; @@ -64,7 +65,7 @@ public function testGetMenuTraversesLevelsCorrectly(): void $this->assertEquals($fixture, $actual); } - public function testGetMenuGeneratesIdsForElementsWithoutIDs(): void + public function testGetMenuDoesNotGenerateIDsForElementsWithoutIDs(): void { $html = "

A-Header

Foobar

@@ -121,11 +122,53 @@ public function testGetMenuReturnsEmptyMenuItemWhenNoContentOrMatches(): void $this->assertEquals(0, count($obj->getMenu(""))); } - public function testGetMenuRespectsOlOption(): void + public function testGetOrderedMenu(): void { $obj = new TocGenerator(); $html = "

A-Header

A-Header

"; $menuHtml = $obj->getOrderedHtmlMenu($html, 1, 6, null); $this->assertStringStartsWith('
    ', $menuHtml); } + + /** + * @dataProvider unusedHeadingLevelsAreTrimmedDataProvider + * @param ItemInterface $menuItem + * @param int $expectedTopLevelItems + * @param int $expectedSubItems + */ + public function testUnusedHeadingLevelsAreTrimmedFromGeneratedMenu( + ItemInterface $menuItem, + int $expectedTopLevelItems, + int $expectedSubItems = 0 + ): void { + $this->assertEquals($expectedTopLevelItems, count($menuItem->getChildren())); + + if ($expectedSubItems > 0) { + $this->assertEquals($expectedSubItems, count($menuItem->getFirstChild()->getChildren())); + } + } + + /** + * @return iterable|ItemInterface[] + */ + public function unusedHeadingLevelsAreTrimmedDataProvider(): iterable + { + $obj = new TocGenerator(); + + yield [ + $obj->getMenu("

    X-Header

    Y-Header

    Z-Header

    ", 1, 6), + 1, + 2 + ]; + + yield [$obj->getMenu("

    X-Header

    ", 1, 6), 1]; + + yield [ + $obj->getMenu('
    X-Header
    Y-Header
    ', 1, 6), 2 + ]; + + yield [ + $obj->getMenu('
    Y-Header
    ', 1, 5), 0 + ]; + } }