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