Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
| Total | |
87.50% |
28 / 32 |
|
50.00% |
2 / 4 |
CRAP | |
0.00% |
0 / 1 |
| CssBridge | |
87.50% |
28 / 32 |
|
50.00% |
2 / 4 |
12.28 | |
0.00% |
0 / 1 |
| __construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
| computeStyle | |
100.00% |
9 / 9 |
|
100.00% |
1 / 1 |
1 | |||
| collectAuthorSheets | |
77.78% |
7 / 9 |
|
0.00% |
0 / 1 |
4.18 | |||
| presentationAttributeSheet | |
84.62% |
11 / 13 |
|
0.00% |
0 / 1 |
6.13 | |||
| 1 | <?php |
| 2 | |
| 3 | declare(strict_types=1); |
| 4 | |
| 5 | namespace Phpdftk\Svg\Css; |
| 6 | |
| 7 | use Phpdftk\Css\Cascade\Cascade; |
| 8 | use Phpdftk\Css\Cascade\CascadedValues; |
| 9 | use Phpdftk\Css\Parser as CssParser; |
| 10 | use Phpdftk\Css\Selector\SelectorParser; |
| 11 | use Phpdftk\Css\Sheet\Declaration; |
| 12 | use Phpdftk\Css\Sheet\Origin; |
| 13 | use Phpdftk\Css\Sheet\StyleRule; |
| 14 | use Phpdftk\Css\Sheet\Stylesheet; |
| 15 | use Phpdftk\Css\ValueParser; |
| 16 | use Phpdftk\Svg\Element; |
| 17 | use Phpdftk\Svg\StyleElement; |
| 18 | use 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 | */ |
| 41 | final 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 | } |