1<?php 2 3/* 4 * This file is part of the league/commonmark package. 5 * 6 * (c) Colin O'Dell <colinodell@gmail.com> 7 * (c) Rezo Zero / Ambroise Maupate 8 * 9 * For the full copyright and license information, please view the LICENSE 10 * file that was distributed with this source code. 11 */ 12 13declare(strict_types=1); 14 15namespace League\CommonMark\Extension\Footnote\Event; 16 17use League\CommonMark\Event\DocumentParsedEvent; 18use League\CommonMark\Extension\Footnote\Node\Footnote; 19use League\CommonMark\Extension\Footnote\Node\FootnoteBackref; 20use League\CommonMark\Extension\Footnote\Node\FootnoteContainer; 21use League\CommonMark\Node\Block\Document; 22use League\CommonMark\Node\NodeIterator; 23use League\CommonMark\Reference\Reference; 24use League\Config\ConfigurationAwareInterface; 25use League\Config\ConfigurationInterface; 26 27final class GatherFootnotesListener implements ConfigurationAwareInterface 28{ 29 private ConfigurationInterface $config; 30 31 public function onDocumentParsed(DocumentParsedEvent $event): void 32 { 33 $document = $event->getDocument(); 34 $footnotes = []; 35 36 foreach ($document->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { 37 if (! $node instanceof Footnote) { 38 continue; 39 } 40 41 // Look for existing reference with footnote label 42 $ref = $document->getReferenceMap()->get($node->getReference()->getLabel()); 43 if ($ref !== null) { 44 // Use numeric title to get footnotes order 45 $footnotes[(int) $ref->getTitle()] = $node; 46 } else { 47 // Footnote call is missing, append footnote at the end 48 $footnotes[\PHP_INT_MAX] = $node; 49 } 50 51 $key = '#' . $this->config->get('footnote/footnote_id_prefix') . $node->getReference()->getDestination(); 52 if ($document->data->has($key)) { 53 $this->createBackrefs($node, $document->data->get($key)); 54 } 55 } 56 57 // Only add a footnote container if there are any 58 if (\count($footnotes) === 0) { 59 return; 60 } 61 62 $container = $this->getFootnotesContainer($document); 63 64 \ksort($footnotes); 65 foreach ($footnotes as $footnote) { 66 $container->appendChild($footnote); 67 } 68 } 69 70 private function getFootnotesContainer(Document $document): FootnoteContainer 71 { 72 $footnoteContainer = new FootnoteContainer(); 73 $document->appendChild($footnoteContainer); 74 75 return $footnoteContainer; 76 } 77 78 /** 79 * Look for all footnote refs pointing to this footnote and create each footnote backrefs. 80 * 81 * @param Footnote $node The target footnote 82 * @param Reference[] $backrefs References to create backrefs for 83 */ 84 private function createBackrefs(Footnote $node, array $backrefs): void 85 { 86 // Backrefs should be added to the child paragraph 87 $target = $node->lastChild(); 88 if ($target === null) { 89 // This should never happen, but you never know 90 $target = $node; 91 } 92 93 foreach ($backrefs as $backref) { 94 $target->appendChild(new FootnoteBackref(new Reference( 95 $backref->getLabel(), 96 '#' . $this->config->get('footnote/ref_id_prefix') . $backref->getLabel(), 97 $backref->getTitle() 98 ))); 99 } 100 } 101 102 public function setConfiguration(ConfigurationInterface $configuration): void 103 { 104 $this->config = $configuration; 105 } 106} 107