Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
87.50% covered (warning)
87.50%
28 / 32
50.00% covered (danger)
50.00%
2 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
CssBridge
87.50% covered (warning)
87.50%
28 / 32
50.00% covered (danger)
50.00%
2 / 4
12.28
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
 computeStyle
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
1
 collectAuthorSheets
77.78% covered (warning)
77.78%
7 / 9
0.00% covered (danger)
0.00%
0 / 1
4.18
 presentationAttributeSheet
84.62% covered (warning)
84.62%
11 / 13
0.00% covered (danger)
0.00%
0 / 1
6.13
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Svg\Css;
6
7use Phpdftk\Css\Cascade\Cascade;
8use Phpdftk\Css\Cascade\CascadedValues;
9use Phpdftk\Css\Parser as CssParser;
10use Phpdftk\Css\Selector\SelectorParser;
11use Phpdftk\Css\Sheet\Declaration;
12use Phpdftk\Css\Sheet\Origin;
13use Phpdftk\Css\Sheet\StyleRule;
14use Phpdftk\Css\Sheet\Stylesheet;
15use Phpdftk\Css\ValueParser;
16use Phpdftk\Svg\Element;
17use Phpdftk\Svg\StyleElement;
18use Phpdftk\Svg\SvgDocument;
19
20/**
21 * Bridge between an SVG element tree and the `phpdftk/css` cascade.
22 *
23 * `computeStyle()` resolves the effective CSS values for one element by
24 * layering, in increasing precedence:
25 *
26 *  1. SVG presentation attributes — author origin, specificity 0,0,0,0
27 *     per SVG 2 §6.7. Synthesised into a one-rule stylesheet with a `*`
28 *     selector so it matches but doesn't out-rank anything.
29 *  2. `<style>` element CSS from the document — author origin, normal
30 *     selector specificity per Selectors 4.
31 *  3. The element's inline `style=""` attribute — author origin,
32 *     specificity `(1024, 0, 0)` per CSS Cascade 5 §6.4.4. The `Cascade`
33 *     reads this directly from `MatchableElement::getAttributeValue`
34 *     so we don't need a manual overlay.
35 *
36 * The bridge depends on `phpdftk/css`. It lives in its own namespace so
37 * loading `Phpdftk\Svg\Element` doesn't transitively pull the CSS
38 * dependency — callers who don't need the cascade keep working without
39 * `phpdftk/css` installed.
40 */
41final class CssBridge
42{
43    /**
44     * The subset of SVG presentation attributes we synthesise into CSS
45     * declarations. Matches the typed accessors landed in 3E and 3F; the
46     * painter is free to extend the list when it adds support for more.
47     *
48     * @var list<string>
49     */
50    private const array PRESENTATION_ATTRIBUTES = [
51        'fill', 'stroke',
52        'fill-opacity', 'stroke-opacity', 'opacity',
53        'fill-rule',
54        'stroke-width', 'stroke-linecap', 'stroke-linejoin',
55        'stroke-miterlimit', 'stroke-dasharray', 'stroke-dashoffset',
56        'font-family', 'font-size', 'font-weight', 'font-style',
57        'color', 'display', 'visibility',
58    ];
59
60    public function __construct(
61        private readonly Cascade $cascade = new Cascade(),
62        private readonly CssParser $cssParser = new CssParser(),
63        private readonly ValueParser $valueParser = new ValueParser(),
64    ) {}
65
66    /**
67     * Compute the cascade for `$element`. `$parentValues` is the
68     * already-computed result for the element's parent — pass `null` for
69     * the root.
70     */
71    public function computeStyle(
72        Element $element,
73        SvgDocument $document,
74        ?CascadedValues $parentValues = null,
75    ): CascadedValues {
76        $sheets = [
77            $this->presentationAttributeSheet($element),
78            ...$this->collectAuthorSheets($document),
79        ];
80        return $this->cascade->computeFor(
81            $sheets,
82            new MatchableSvgElement($element),
83            $parentValues,
84        );
85    }
86
87    /**
88     * Walk the document and parse every `<style>` element's body into a
89     * Stylesheet. Returned in document order so later sheets shadow
90     * earlier ones on specificity ties, matching how browsers resolve
91     * multiple `<style>` blocks.
92     *
93     * @return list<Stylesheet>
94     */
95    public function collectAuthorSheets(SvgDocument $document): array
96    {
97        $sheets = [];
98        foreach ($document->findByTag('style') as $node) {
99            if (!$node instanceof StyleElement) {
100                continue;
101            }
102            $css = $node->cssText();
103            if (trim($css) === '') {
104                continue;
105            }
106            $sheets[] = $this->cssParser->parseStylesheet($css, Origin::Author);
107        }
108        return $sheets;
109    }
110
111    /**
112     * Build a single-rule Stylesheet whose declarations are the
113     * presentation attributes carried by `$element`. The rule's selector
114     * is `*` so it matches the element (and would match every other
115     * element too, but `computeStyle` only ever passes this sheet for
116     * one specific element).
117     */
118    public function presentationAttributeSheet(Element $element): Stylesheet
119    {
120        $declarations = [];
121        foreach (self::PRESENTATION_ATTRIBUTES as $name) {
122            $raw = $element->getAttribute($name);
123            if ($raw === null || trim($raw) === '') {
124                continue;
125            }
126            try {
127                $value = $this->valueParser->parseFromString($raw);
128            } catch (\Throwable) {
129                continue;
130            }
131            $declarations[] = new Declaration($name, $value, important: false);
132        }
133        if ($declarations === []) {
134            return new Stylesheet([], Origin::Author);
135        }
136        $rule = new StyleRule(SelectorParser::parse('*'), $declarations);
137        return new Stylesheet([$rule], Origin::Author);
138    }
139}