Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.55% covered (success)
93.55%
29 / 31
80.00% covered (warning)
80.00%
8 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 1
ClassList
93.55% covered (success)
93.55%
29 / 31
80.00% covered (warning)
80.00%
8 / 10
20.11
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
 contains
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 add
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 remove
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
2
 toggle
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 values
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 count
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 tokens
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 write
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 assertValidToken
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\Html\Dom;
6
7/**
8 * Live token list backed by an Element's `class` attribute. Per WHATWG DOM
9 * ยง7.1, mutations write back through to the underlying `class` attribute.
10 *
11 * Order-preserving and deduplicating: the underlying class attribute is
12 * canonicalised to a single-space-separated, dedup'd list on every mutation.
13 */
14final class ClassList
15{
16    public function __construct(private Element $owner) {}
17
18    public function contains(string $token): bool
19    {
20        return in_array($token, $this->tokens(), true);
21    }
22
23    public function add(string ...$tokens): void
24    {
25        $current = $this->tokens();
26        foreach ($tokens as $t) {
27            self::assertValidToken($t);
28            if (!in_array($t, $current, true)) {
29                $current[] = $t;
30            }
31        }
32        $this->write($current);
33    }
34
35    public function remove(string ...$tokens): void
36    {
37        $current = $this->tokens();
38        foreach ($tokens as $t) {
39            self::assertValidToken($t);
40        }
41        $current = array_values(array_filter($current, fn(string $c): bool => !in_array($c, $tokens, true)));
42        $this->write($current);
43    }
44
45    public function toggle(string $token, ?bool $force = null): bool
46    {
47        self::assertValidToken($token);
48        $present = $this->contains($token);
49        $shouldBePresent = $force ?? !$present;
50        if ($shouldBePresent && !$present) {
51            $this->add($token);
52        } elseif (!$shouldBePresent && $present) {
53            $this->remove($token);
54        }
55        return $shouldBePresent;
56    }
57
58    /** @return list<string> */
59    public function values(): array
60    {
61        return $this->tokens();
62    }
63
64    public function count(): int
65    {
66        return count($this->tokens());
67    }
68
69    /** @return list<string> */
70    private function tokens(): array
71    {
72        $raw = $this->owner->getAttribute('class') ?? '';
73        $tokens = preg_split('/\s+/', trim($raw)) ?: [];
74        return array_values(array_unique(array_filter($tokens, static fn(string $t): bool => $t !== '')));
75    }
76
77    /** @param list<string> $tokens */
78    private function write(array $tokens): void
79    {
80        $this->owner->setAttribute('class', implode(' ', $tokens));
81    }
82
83    private static function assertValidToken(string $token): void
84    {
85        if ($token === '') {
86            throw new \InvalidArgumentException('Class token may not be empty');
87        }
88        if (preg_match('/\s/', $token)) {
89            throw new \InvalidArgumentException('Class token may not contain whitespace: ' . $token);
90        }
91    }
92}