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