SchulIT /
icc
| 1 | <?php |
||
| 2 | |||
| 3 | namespace App\Markdown; |
||
| 4 | |||
| 5 | use EasySlugger\SluggerInterface; |
||
| 6 | use League\CommonMark\Block\Element\Document; |
||
| 7 | use League\CommonMark\Block\Element\Heading; |
||
| 8 | use League\CommonMark\DocParser; |
||
| 9 | use League\CommonMark\EnvironmentInterface; |
||
| 10 | use Psr\Cache\CacheItemPoolInterface; |
||
| 11 | |||
| 12 | class TableOfContentsHelper { |
||
| 13 | public function __construct(private SluggerInterface $slugger, private CacheItemPoolInterface $cache, private EnvironmentInterface $environment) |
||
| 14 | { |
||
| 15 | } |
||
| 16 | |||
| 17 | 14 | public function getTableOfContents(string $markdown) { |
|
| 18 | 14 | $hash = hash('sha512', $markdown); |
|
| 19 | 14 | $key = sprintf('markdown.toc.%s', $hash); |
|
| 20 | 14 | ||
| 21 | 14 | $item = $this->cache->getItem($key); |
|
| 22 | |||
| 23 | if(!$item->isHit()) { |
||
| 24 | $toc = $this->computeToc($markdown); |
||
| 25 | |||
| 26 | $item->set(serialize($toc)); |
||
| 27 | } |
||
| 28 | |||
| 29 | return unserialize($item->get()); |
||
| 30 | } |
||
| 31 | |||
| 32 | private function computeToc(string $markdown): array { |
||
| 33 | $parser = new DocParser($this->environment); |
||
| 34 | $document = $parser->parse($markdown); |
||
| 35 | $toc = $this->processDocument($document); |
||
| 36 | |||
| 37 | $counter = [ |
||
| 38 | 2 => 0, |
||
| 39 | 3 => 0, |
||
| 40 | 4 => 0, |
||
| 41 | 5 => 0, |
||
| 42 | 6 => 0 |
||
| 43 | ]; |
||
| 44 | |||
| 45 | $items = [ ]; |
||
| 46 | $lastLevel = 2; |
||
| 47 | |||
| 48 | foreach($toc as $value) { |
||
| 49 | $level = $value['level']; |
||
| 50 | |||
| 51 | if($level < $lastLevel) { |
||
| 52 | // reset counters |
||
| 53 | for($i = $lastLevel; $i < count($counter); $i++) { |
||
|
0 ignored issues
–
show
|
|||
| 54 | $counter[$i] = 0; |
||
| 55 | } |
||
| 56 | } |
||
| 57 | |||
| 58 | $counter[$level]++; |
||
| 59 | |||
| 60 | $currentLevel = ''; |
||
| 61 | for($i = 2; $i <= $level; $i++) { |
||
| 62 | $currentLevel .= $counter[$i] . '.'; |
||
| 63 | } |
||
| 64 | $currentLevel = substr($currentLevel, 0, -1); |
||
| 65 | |||
| 66 | $items[] = [ |
||
| 67 | 'level' => $currentLevel, |
||
| 68 | 'id' => $value['id'], |
||
| 69 | 'text' => $value['text'] |
||
| 70 | ]; |
||
| 71 | |||
| 72 | $lastLevel = $level; |
||
| 73 | } |
||
| 74 | |||
| 75 | return $items; |
||
| 76 | } |
||
| 77 | |||
| 78 | private function processDocument(Document $document): array { |
||
| 79 | $toc = [ ]; |
||
| 80 | $walker = $document->walker(); |
||
| 81 | |||
| 82 | while($event = $walker->next()) { |
||
| 83 | $node = $event->getNode(); |
||
| 84 | |||
| 85 | if(!($node instanceof Heading) || !$event->isEntering()) { |
||
| 86 | continue; |
||
| 87 | } |
||
| 88 | |||
| 89 | $heading = $node->getStringContent(); |
||
| 90 | $slug = $this->slugger->slugify($heading); |
||
| 91 | |||
| 92 | $level = min($node->getLevel() + 1, 6); |
||
| 93 | |||
| 94 | $toc[] = [ |
||
| 95 | 'id' => $slug, |
||
| 96 | 'level' => $level, |
||
| 97 | 'text' => $heading |
||
| 98 | ]; |
||
| 99 | } |
||
| 100 | |||
| 101 | return $toc; |
||
| 102 | } |
||
| 103 | } |
If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration: