Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.57% covered (success)
98.57%
69 / 70
94.74% covered (success)
94.74%
18 / 19
CRAP
0.00% covered (danger)
0.00%
0 / 1
MatchableSvgElement
98.57% covered (success)
98.57%
69 / 70
94.74% covered (success)
94.74%
18 / 19
44
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 localName
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 namespaceUri
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 elementId
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 classes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasAttribute
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAttributeValue
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 allAttributes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parentElement
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 previousElementSibling
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 nextElementSibling
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 elementChildren
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 indexAmongSiblings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 indexAmongSiblingsFromEnd
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 indexAmongTypeSiblings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 indexAmongTypeSiblingsFromEnd
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 parentElementChildren
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
4
 sameTypeSiblings
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 positionAmong
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Svg\Css;
6
7use Phpdftk\Css\Selector\MatchableElement;
8use Phpdftk\Svg\Element;
9
10/**
11 * Adapter that lets the `phpdftk/css` Selectors-4 matcher walk an
12 * `Phpdftk\Svg\Element` tree. Lives in its own namespace so the SVG parser
13 * stays usable without the CSS dependency — only callers that opt into the
14 * bridge layer load this class, and only then does PHP resolve the
15 * `MatchableElement` interface.
16 *
17 * The adapter is read-only: it never mutates the wrapped element. Sibling
18 * and child traversals filter out `Text` data nodes — only `Element`
19 * instances are visible to the matcher, matching the CSS Selectors model.
20 */
21final class MatchableSvgElement implements MatchableElement
22{
23    /** SVG namespace URI — every parsed SVG element lives in this namespace. */
24    public const string SVG_NS = 'http://www.w3.org/2000/svg';
25
26    public function __construct(public readonly Element $element) {}
27
28    public function localName(): string
29    {
30        return $this->element->localName;
31    }
32
33    public function namespaceUri(): string
34    {
35        return self::SVG_NS;
36    }
37
38    public function elementId(): ?string
39    {
40        $id = $this->element->getAttribute('id');
41        if ($id === null) {
42            return null;
43        }
44        $trimmed = trim($id);
45        return $trimmed === '' ? null : strtolower($trimmed);
46    }
47
48    /** @return list<string> */
49    public function classes(): array
50    {
51        return $this->element->classList();
52    }
53
54    public function hasAttribute(string $name): bool
55    {
56        return $this->element->hasAttribute($name);
57    }
58
59    public function getAttributeValue(string $name): ?string
60    {
61        return $this->element->getAttribute($name);
62    }
63
64    /** @return array<string, string> */
65    public function allAttributes(): array
66    {
67        return $this->element->attributes;
68    }
69
70    public function parentElement(): ?MatchableElement
71    {
72        $parent = $this->element->parent;
73        return $parent === null ? null : new self($parent);
74    }
75
76    public function previousElementSibling(): ?MatchableElement
77    {
78        $siblings = $this->parentElementChildren();
79        if ($siblings === []) {
80            return null;
81        }
82        $position = $this->positionAmong($siblings);
83        if ($position <= 0) {
84            return null;
85        }
86        return new self($siblings[$position - 1]);
87    }
88
89    public function nextElementSibling(): ?MatchableElement
90    {
91        $siblings = $this->parentElementChildren();
92        if ($siblings === []) {
93            return null;
94        }
95        $position = $this->positionAmong($siblings);
96        if ($position < 0 || $position + 1 >= count($siblings)) {
97            return null;
98        }
99        return new self($siblings[$position + 1]);
100    }
101
102    /** @return list<MatchableElement> */
103    public function elementChildren(): array
104    {
105        $out = [];
106        foreach ($this->element->children as $child) {
107            if ($child instanceof Element) {
108                $out[] = new self($child);
109            }
110        }
111        return $out;
112    }
113
114    public function indexAmongSiblings(): int
115    {
116        $siblings = $this->parentElementChildren();
117        if ($siblings === []) {
118            return 1;
119        }
120        $position = $this->positionAmong($siblings);
121        return $position < 0 ? 1 : $position + 1;
122    }
123
124    public function indexAmongSiblingsFromEnd(): int
125    {
126        $siblings = $this->parentElementChildren();
127        if ($siblings === []) {
128            return 1;
129        }
130        $position = $this->positionAmong($siblings);
131        return $position < 0 ? 1 : count($siblings) - $position;
132    }
133
134    public function indexAmongTypeSiblings(): int
135    {
136        $sameType = $this->sameTypeSiblings();
137        if ($sameType === []) {
138            return 1;
139        }
140        $position = $this->positionAmong($sameType);
141        return $position < 0 ? 1 : $position + 1;
142    }
143
144    public function indexAmongTypeSiblingsFromEnd(): int
145    {
146        $sameType = $this->sameTypeSiblings();
147        if ($sameType === []) {
148            return 1;
149        }
150        $position = $this->positionAmong($sameType);
151        return $position < 0 ? 1 : count($sameType) - $position;
152    }
153
154    /** @return list<Element> */
155    private function parentElementChildren(): array
156    {
157        $parent = $this->element->parent;
158        if ($parent === null) {
159            return [];
160        }
161        $out = [];
162        foreach ($parent->children as $child) {
163            if ($child instanceof Element) {
164                $out[] = $child;
165            }
166        }
167        return $out;
168    }
169
170    /** @return list<Element> */
171    private function sameTypeSiblings(): array
172    {
173        $out = [];
174        foreach ($this->parentElementChildren() as $child) {
175            if ($child->localName === $this->element->localName) {
176                $out[] = $child;
177            }
178        }
179        return $out;
180    }
181
182    /**
183     * @param list<Element> $elements
184     * @return int -1 when this element isn't in the list (shouldn't happen
185     *             for a well-formed tree, but defensive)
186     */
187    private function positionAmong(array $elements): int
188    {
189        foreach ($elements as $i => $candidate) {
190            if ($candidate === $this->element) {
191                return $i;
192            }
193        }
194        return -1;
195    }
196}