1<?php 2 3declare(strict_types=1); 4 5/* 6 * This file is part of the league/commonmark package. 7 * 8 * (c) Colin O'Dell <colinodell@gmail.com> 9 * 10 * For the full copyright and license information, please view the LICENSE 11 * file that was distributed with this source code. 12 */ 13 14namespace League\CommonMark\Extension\HeadingPermalink; 15 16use League\CommonMark\Environment\EnvironmentAwareInterface; 17use League\CommonMark\Environment\EnvironmentInterface; 18use League\CommonMark\Event\DocumentParsedEvent; 19use League\CommonMark\Extension\CommonMark\Node\Block\Heading; 20use League\CommonMark\Node\NodeIterator; 21use League\CommonMark\Node\RawMarkupContainerInterface; 22use League\CommonMark\Node\StringContainerHelper; 23use League\CommonMark\Normalizer\TextNormalizerInterface; 24use League\Config\ConfigurationInterface; 25use League\Config\Exception\InvalidConfigurationException; 26 27/** 28 * Searches the Document for Heading elements and adds HeadingPermalinks to each one 29 */ 30final class HeadingPermalinkProcessor implements EnvironmentAwareInterface 31{ 32 public const INSERT_BEFORE = 'before'; 33 public const INSERT_AFTER = 'after'; 34 public const INSERT_NONE = 'none'; 35 36 /** @psalm-readonly-allow-private-mutation */ 37 private TextNormalizerInterface $slugNormalizer; 38 39 /** @psalm-readonly-allow-private-mutation */ 40 private ConfigurationInterface $config; 41 42 public function setEnvironment(EnvironmentInterface $environment): void 43 { 44 $this->config = $environment->getConfiguration(); 45 $this->slugNormalizer = $environment->getSlugNormalizer(); 46 } 47 48 public function __invoke(DocumentParsedEvent $e): void 49 { 50 $min = (int) $this->config->get('heading_permalink/min_heading_level'); 51 $max = (int) $this->config->get('heading_permalink/max_heading_level'); 52 $applyToHeading = (bool) $this->config->get('heading_permalink/apply_id_to_heading'); 53 $idPrefix = (string) $this->config->get('heading_permalink/id_prefix'); 54 $slugLength = (int) $this->config->get('slug_normalizer/max_length'); 55 $headingClass = (string) $this->config->get('heading_permalink/heading_class'); 56 57 if ($idPrefix !== '') { 58 $idPrefix .= '-'; 59 } 60 61 foreach ($e->getDocument()->iterator(NodeIterator::FLAG_BLOCKS_ONLY) as $node) { 62 if ($node instanceof Heading && $node->getLevel() >= $min && $node->getLevel() <= $max) { 63 $this->addHeadingLink($node, $slugLength, $idPrefix, $applyToHeading, $headingClass); 64 } 65 } 66 } 67 68 private function addHeadingLink(Heading $heading, int $slugLength, string $idPrefix, bool $applyToHeading, string $headingClass): void 69 { 70 $text = StringContainerHelper::getChildText($heading, [RawMarkupContainerInterface::class]); 71 $slug = $this->slugNormalizer->normalize($text, [ 72 'node' => $heading, 73 'length' => $slugLength, 74 ]); 75 76 if ($applyToHeading) { 77 $heading->data->set('attributes/id', $idPrefix . $slug); 78 } 79 80 if ($headingClass !== '') { 81 $heading->data->append('attributes/class', $headingClass); 82 } 83 84 $headingLinkAnchor = new HeadingPermalink($slug); 85 86 switch ($this->config->get('heading_permalink/insert')) { 87 case self::INSERT_BEFORE: 88 $heading->prependChild($headingLinkAnchor); 89 90 return; 91 case self::INSERT_AFTER: 92 $heading->appendChild($headingLinkAnchor); 93 94 return; 95 case self::INSERT_NONE: 96 return; 97 default: 98 throw new InvalidConfigurationException("Invalid configuration value for heading_permalink/insert; expected 'before', 'after', or 'none'"); 99 } 100 } 101} 102