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) 2015 Martin Hasoň <martin.hason@gmail.com> 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\Attributes\Event; 16 17use League\CommonMark\Event\DocumentParsedEvent; 18use League\CommonMark\Extension\Attributes\Node\Attributes; 19use League\CommonMark\Extension\Attributes\Node\AttributesInline; 20use League\CommonMark\Extension\Attributes\Util\AttributesHelper; 21use League\CommonMark\Extension\CommonMark\Node\Block\FencedCode; 22use League\CommonMark\Extension\CommonMark\Node\Block\ListBlock; 23use League\CommonMark\Extension\CommonMark\Node\Block\ListItem; 24use League\CommonMark\Node\Inline\AbstractInline; 25use League\CommonMark\Node\Node; 26 27final class AttributesListener 28{ 29 private const DIRECTION_PREFIX = 'prefix'; 30 private const DIRECTION_SUFFIX = 'suffix'; 31 32 public function processDocument(DocumentParsedEvent $event): void 33 { 34 foreach ($event->getDocument()->iterator() as $node) { 35 if (! ($node instanceof Attributes || $node instanceof AttributesInline)) { 36 continue; 37 } 38 39 [$target, $direction] = self::findTargetAndDirection($node); 40 41 if ($target instanceof Node) { 42 $parent = $target->parent(); 43 if ($parent instanceof ListItem && $parent->parent() instanceof ListBlock && $parent->parent()->isTight()) { 44 $target = $parent; 45 } 46 47 if ($direction === self::DIRECTION_SUFFIX) { 48 $attributes = AttributesHelper::mergeAttributes($target, $node->getAttributes()); 49 } else { 50 $attributes = AttributesHelper::mergeAttributes($node->getAttributes(), $target); 51 } 52 53 $target->data->set('attributes', $attributes); 54 } 55 56 $node->detach(); 57 } 58 } 59 60 /** 61 * @param Attributes|AttributesInline $node 62 * 63 * @return array<Node|string|null> 64 */ 65 private static function findTargetAndDirection($node): array 66 { 67 $target = null; 68 $direction = null; 69 $previous = $next = $node; 70 while (true) { 71 $previous = self::getPrevious($previous); 72 $next = self::getNext($next); 73 74 if ($previous === null && $next === null) { 75 if (! $node->parent() instanceof FencedCode) { 76 $target = $node->parent(); 77 $direction = self::DIRECTION_SUFFIX; 78 } 79 80 break; 81 } 82 83 if ($node instanceof AttributesInline && ($previous === null || ($previous instanceof AbstractInline && $node->isBlock()))) { 84 continue; 85 } 86 87 if ($previous !== null && ! self::isAttributesNode($previous)) { 88 $target = $previous; 89 $direction = self::DIRECTION_SUFFIX; 90 91 break; 92 } 93 94 if ($next !== null && ! self::isAttributesNode($next)) { 95 $target = $next; 96 $direction = self::DIRECTION_PREFIX; 97 98 break; 99 } 100 } 101 102 return [$target, $direction]; 103 } 104 105 /** 106 * Get any previous block (sibling or parent) this might apply to 107 */ 108 private static function getPrevious(?Node $node = null): ?Node 109 { 110 if ($node instanceof Attributes) { 111 if ($node->getTarget() === Attributes::TARGET_NEXT) { 112 return null; 113 } 114 115 if ($node->getTarget() === Attributes::TARGET_PARENT) { 116 return $node->parent(); 117 } 118 } 119 120 return $node instanceof Node ? $node->previous() : null; 121 } 122 123 /** 124 * Get any previous block (sibling or parent) this might apply to 125 */ 126 private static function getNext(?Node $node = null): ?Node 127 { 128 if ($node instanceof Attributes && $node->getTarget() !== Attributes::TARGET_NEXT) { 129 return null; 130 } 131 132 return $node instanceof Node ? $node->next() : null; 133 } 134 135 private static function isAttributesNode(Node $node): bool 136 { 137 return $node instanceof Attributes || $node instanceof AttributesInline; 138 } 139} 140