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