Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
84.62% covered (warning)
84.62%
22 / 26
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
PseudoClassSelector
84.62% covered (warning)
84.62%
22 / 26
25.00% covered (danger)
25.00%
1 / 4
14.71
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
 specificity
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 argumentMaxSpecificity
83.33% covered (warning)
83.33%
5 / 6
0.00% covered (danger)
0.00%
0 / 1
4.07
 toString
80.00% covered (warning)
80.00%
8 / 10
0.00% covered (danger)
0.00%
0 / 1
5.20
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Selector;
6
7/**
8 * Pseudo-class selector per Selectors 4 §3.5: `:hover`, `:first-child`,
9 * `:nth-child(2n+1)`, `:not(...)`, `:is(...)`, `:where(...)`, `:has(...)`,
10 * `:host`, `:host(...)`, `:host-context(...)`, `:lang(en)`, etc.
11 *
12 * Specificity per Selectors 4 §16:
13 *  - `:where()` always contributes (0,0,0).
14 *  - `:is()` / `:not()` / `:has()` contribute the max specificity of their
15 *    argument selector list.
16 *  - `:nth-child(... of S)` and `:nth-last-child(... of S)` similarly take
17 *    the max of S, then add (0, 1, 0) for the pseudo itself.
18 *  - All other pseudo-classes contribute (0, 1, 0).
19 *
20 * `arguments` carries the parsed inner SelectorList for the logical/has/is
21 * family (and `:nth-*-of-type` selector lists), or null when the pseudo is
22 * argument-less. `anPlusB` carries the parsed An+B coefficients for the
23 * nth-* family (always-null otherwise). Free-form string args (e.g.
24 * `:lang(en)`, `:dir(ltr)`) live in `argText`.
25 */
26final readonly class PseudoClassSelector extends SimpleSelector
27{
28    public function __construct(
29        public string $name,
30        public ?SelectorList $arguments = null,
31        public ?AnPlusB $anPlusB = null,
32        public ?string $argText = null,
33    ) {}
34
35    public function specificity(): Specificity
36    {
37        $lower = strtolower($this->name);
38
39        if ($lower === 'where') {
40            return new Specificity();
41        }
42        if (in_array($lower, ['is', 'not', 'has'], true)) {
43            return $this->argumentMaxSpecificity();
44        }
45        $base = new Specificity(0, 1, 0);
46        if (in_array($lower, ['nth-child', 'nth-last-child'], true)) {
47            return $base->add($this->argumentMaxSpecificity());
48        }
49        return $base;
50    }
51
52    private function argumentMaxSpecificity(): Specificity
53    {
54        if ($this->arguments === null || $this->arguments->selectors === []) {
55            return new Specificity();
56        }
57        $max = $this->arguments->selectors[0]->specificity();
58        foreach (array_slice($this->arguments->selectors, 1) as $sel) {
59            $max = $max->max($sel->specificity());
60        }
61        return $max;
62    }
63
64    public function toString(): string
65    {
66        if ($this->arguments !== null) {
67            $parts = [];
68            foreach ($this->arguments->selectors as $sel) {
69                $parts[] = $sel->toString();
70            }
71            return ':' . $this->name . '(' . implode(', ', $parts) . ')';
72        }
73        if ($this->anPlusB !== null) {
74            return ':' . $this->name . '(' . $this->anPlusB->toString() . ')';
75        }
76        if ($this->argText !== null) {
77            return ':' . $this->name . '(' . $this->argText . ')';
78        }
79        return ':' . $this->name;
80    }
81}