1<?php 2 3/* 4 * This file is part of the league/commonmark package. 5 * 6 * (c) Colin O'Dell <colinodell@gmail.com> 7 * 8 * For the full copyright and license information, please view the LICENSE 9 * file that was distributed with this source code. 10 */ 11 12namespace League\CommonMark\Extension\TableOfContents; 13 14use League\CommonMark\Block\Element\Document; 15use League\CommonMark\Block\Element\Heading; 16use League\CommonMark\Event\DocumentParsedEvent; 17use League\CommonMark\Exception\InvalidOptionException; 18use League\CommonMark\Extension\HeadingPermalink\HeadingPermalink; 19use League\CommonMark\Extension\TableOfContents\Node\TableOfContents; 20use League\CommonMark\Extension\TableOfContents\Node\TableOfContentsPlaceholder; 21use League\CommonMark\Util\ConfigurationAwareInterface; 22use League\CommonMark\Util\ConfigurationInterface; 23 24final class TableOfContentsBuilder implements ConfigurationAwareInterface 25{ 26 /** 27 * @deprecated Use TableOfContentsGenerator::STYLE_BULLET instead 28 */ 29 public const STYLE_BULLET = TableOfContentsGenerator::STYLE_BULLET; 30 31 /** 32 * @deprecated Use TableOfContentsGenerator::STYLE_ORDERED instead 33 */ 34 public const STYLE_ORDERED = TableOfContentsGenerator::STYLE_ORDERED; 35 36 /** 37 * @deprecated Use TableOfContentsGenerator::NORMALIZE_DISABLED instead 38 */ 39 public const NORMALIZE_DISABLED = TableOfContentsGenerator::NORMALIZE_DISABLED; 40 41 /** 42 * @deprecated Use TableOfContentsGenerator::NORMALIZE_RELATIVE instead 43 */ 44 public const NORMALIZE_RELATIVE = TableOfContentsGenerator::NORMALIZE_RELATIVE; 45 46 /** 47 * @deprecated Use TableOfContentsGenerator::NORMALIZE_FLAT instead 48 */ 49 public const NORMALIZE_FLAT = TableOfContentsGenerator::NORMALIZE_FLAT; 50 51 public const POSITION_TOP = 'top'; 52 public const POSITION_BEFORE_HEADINGS = 'before-headings'; 53 public const POSITION_PLACEHOLDER = 'placeholder'; 54 55 /** @var ConfigurationInterface */ 56 private $config; 57 58 public function onDocumentParsed(DocumentParsedEvent $event): void 59 { 60 $document = $event->getDocument(); 61 62 $generator = new TableOfContentsGenerator( 63 $this->config->get('table_of_contents/style', TableOfContentsGenerator::STYLE_BULLET), 64 $this->config->get('table_of_contents/normalize', TableOfContentsGenerator::NORMALIZE_RELATIVE), 65 (int) $this->config->get('table_of_contents/min_heading_level', 1), 66 (int) $this->config->get('table_of_contents/max_heading_level', 6) 67 ); 68 69 $toc = $generator->generate($document); 70 if ($toc === null) { 71 // No linkable headers exist, so no TOC could be generated 72 return; 73 } 74 75 // Add custom CSS class(es), if defined 76 $class = $this->config->get('table_of_contents/html_class', 'table-of-contents'); 77 if (!empty($class)) { 78 $toc->data['attributes']['class'] = $class; 79 } 80 81 // Add the TOC to the Document 82 $position = $this->config->get('table_of_contents/position', self::POSITION_TOP); 83 if ($position === self::POSITION_TOP) { 84 $document->prependChild($toc); 85 } elseif ($position === self::POSITION_BEFORE_HEADINGS) { 86 $this->insertBeforeFirstLinkedHeading($document, $toc); 87 } elseif ($position === self::POSITION_PLACEHOLDER) { 88 $this->replacePlaceholders($document, $toc); 89 } else { 90 throw new InvalidOptionException(\sprintf('Invalid config option "%s" for "table_of_contents/position"', $position)); 91 } 92 } 93 94 private function insertBeforeFirstLinkedHeading(Document $document, TableOfContents $toc): void 95 { 96 $walker = $document->walker(); 97 while ($event = $walker->next()) { 98 if ($event->isEntering() && ($node = $event->getNode()) instanceof HeadingPermalink && ($parent = $node->parent()) instanceof Heading) { 99 $parent->insertBefore($toc); 100 101 return; 102 } 103 } 104 } 105 106 private function replacePlaceholders(Document $document, TableOfContents $toc): void 107 { 108 $walker = $document->walker(); 109 while ($event = $walker->next()) { 110 // Add the block once we find a placeholder (and we're about to leave it) 111 if (!$event->getNode() instanceof TableOfContentsPlaceholder) { 112 continue; 113 } 114 115 if ($event->isEntering()) { 116 continue; 117 } 118 119 $event->getNode()->replaceWith(clone $toc); 120 } 121 } 122 123 public function setConfiguration(ConfigurationInterface $config) 124 { 125 $this->config = $config; 126 } 127} 128