Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.44% covered (success)
97.44%
228 / 234
97.16% covered (success)
97.16%
137 / 141
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComputedStyle
97.44% covered (success)
97.44%
228 / 234
97.16% covered (success)
97.16%
137 / 141
197
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
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCustomProperty
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getUnknown
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundImage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundRepeat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundPosition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundAttachment
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundOrigin
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBackgroundClip
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOpacity
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontFamily
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontSize
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFontWeight
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getLineHeight
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 getTextAlign
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextAlignLast
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextDecorationLine
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextDecorationStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextDecorationColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextDecorationThickness
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextTransform
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextIndent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextJustify
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLetterSpacing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWordSpacing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWhiteSpace
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getWordBreak
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOverflowWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVerticalAlign
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDirection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getUnicodeBidi
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTabSize
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getQuotes
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDisplay
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPosition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTop
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getRight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBottom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getLeft
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getZIndex
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMinWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMinHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMaxHeight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMarginTop
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMarginRight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMarginBottom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getMarginLeft
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPaddingTop
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPaddingRight
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPaddingBottom
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPaddingLeft
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderTopWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderRightWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderBottomWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderLeftWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectBorderWidth
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
7.29
 getBorderTopStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderRightStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderBottomStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderLeftStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderTopColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderRightColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderBottomColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderLeftColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderTopLeftRadius
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderTopRightRadius
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderBottomLeftRadius
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderBottomRightRadius
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBoxSizing
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBoxShadow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOverflow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOverflowX
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOverflowY
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getVisibility
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutlineWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutlineStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutlineColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOutlineOffset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFloat
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClear
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAspectRatio
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlexDirection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlexWrap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getJustifyContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlignItems
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlignContent
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getAlignSelf
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlexGrow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlexShrink
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFlexBasis
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrder
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getRowGap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnGap
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBorderCollapse
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getCaptionSide
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getListStyleType
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getListStylePosition
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getListStyleImage
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getPage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 getBreakBefore
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBreakAfter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBreakInside
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getBoxDecorationBreak
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getOrphans
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getWidows
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 getColumnCount
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 getColumnWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnRuleWidth
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnRuleStyle
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnRuleColor
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnSpan
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getColumnFill
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTextShadow
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getClipPath
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getFilter
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getTransform
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 expectKeyword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 expectColor
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 expectColorOrKeyword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 resolveColorChain
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 resolveColorMix
88.89% covered (warning)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
5.03
 resolveContrastColor
87.50% covered (warning)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
4.03
 relativeLuminance
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
2
 resolveLightDark
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 prefersDarkScheme
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
5
 extractSchemeIdents
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
5.03
 expectLength
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 expectLengthOrKeyword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
3
 expectLengthOrPercentage
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 expectLengthPercentageOrKeyword
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
4
 expectNumber
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Cascade;
6
7use Phpdftk\Css\Value\Color;
8use Phpdftk\Css\Value\CssFunction;
9use Phpdftk\Css\Value\Integer;
10use Phpdftk\Css\Value\ColorMix;
11use Phpdftk\Css\Value\ColorSpace;
12use Phpdftk\Css\Value\ContrastColor;
13use Phpdftk\Css\Value\Keyword;
14use Phpdftk\Css\Value\Length;
15use Phpdftk\Css\Value\LightDark;
16use Phpdftk\Css\Value\Number;
17use Phpdftk\Css\Value\Percentage;
18use Phpdftk\Css\Value\Url;
19use Phpdftk\Css\Value\Value;
20use Phpdftk\Css\Value\ValueList;
21
22/**
23 * Typed accessor surface over a per-element {@see CascadedValues} bag
24 * per `docs/plans/contracts.md` §1D.5. Each getter narrows the
25 * underlying `Value` to the type(s) the property's grammar permits;
26 * properties not explicitly set fall back to the registry's initial
27 * value (via `CascadedValues::get()`).
28 *
29 * The class is intentionally a thin wrapper — it adds no caching or
30 * computation of its own. The cascade resolves percentages, inherits
31 * values, and produces the initial-value fallbacks; ComputedStyle
32 * just exposes them with PHP-static-analysable signatures so
33 * downstream consumers (the box generator, painter, SVG renderer)
34 * don't have to type-check every `?Value` themselves.
35 *
36 * **Type narrowing fallback.** When a property's parsed value doesn't
37 * match its declared type (an invalid `font-weight: foo` slipping
38 * through), the getter returns the property's initial value instead
39 * of throwing — keeping the accessor surface infallible at the cost
40 * of swallowing parser errors that should have been caught earlier.
41 */
42final readonly class ComputedStyle
43{
44    public function __construct(private CascadedValues $values) {}
45
46    // ========================================================================
47    // Generic access (escape hatches per contracts.md)
48    // ========================================================================
49
50    public function get(string $property): ?Value
51    {
52        return $this->values->get($property);
53    }
54
55    public function has(string $property): bool
56    {
57        return $this->values->has($property);
58    }
59
60    /** @return array<string, Value> */
61    public function all(): array
62    {
63        return $this->values->all();
64    }
65
66    public function getCustomProperty(string $name): ?Value
67    {
68        $name = str_starts_with($name, '--') ? $name : '--' . $name;
69        return $this->values->get($name);
70    }
71
72    public function getUnknown(string $name): ?Value
73    {
74        return $this->values->get($name);
75    }
76
77    // ========================================================================
78    // Color & background
79    // ========================================================================
80
81    public function getColor(): Color
82    {
83        return $this->expectColor('color', new Color(0.0, 0.0, 0.0, 1.0));
84    }
85
86    public function getBackgroundColor(): Color|Keyword
87    {
88        return $this->expectColorOrKeyword('background-color', 'transparent');
89    }
90
91    public function getBackgroundImage(): Value
92    {
93        return $this->values->get('background-image') ?? new Keyword('none');
94    }
95
96    public function getBackgroundRepeat(): Keyword
97    {
98        return $this->expectKeyword('background-repeat', 'repeat');
99    }
100
101    public function getBackgroundPosition(): Value
102    {
103        return $this->values->get('background-position') ?? new Keyword('0% 0%');
104    }
105
106    public function getBackgroundSize(): Value
107    {
108        return $this->values->get('background-size') ?? new Keyword('auto');
109    }
110
111    public function getBackgroundAttachment(): Keyword
112    {
113        return $this->expectKeyword('background-attachment', 'scroll');
114    }
115
116    public function getBackgroundOrigin(): Keyword
117    {
118        return $this->expectKeyword('background-origin', 'padding-box');
119    }
120
121    public function getBackgroundClip(): Keyword
122    {
123        return $this->expectKeyword('background-clip', 'border-box');
124    }
125
126    public function getOpacity(): Number
127    {
128        return $this->expectNumber('opacity', 1.0);
129    }
130
131    // ========================================================================
132    // Font & text
133    // ========================================================================
134
135    public function getFontFamily(): Value
136    {
137        return $this->values->get('font-family') ?? new Keyword('serif');
138    }
139
140    public function getFontSize(): Length
141    {
142        return $this->expectLength('font-size', 16.0);
143    }
144
145    public function getFontStyle(): Keyword
146    {
147        return $this->expectKeyword('font-style', 'normal');
148    }
149
150    public function getFontWeight(): Integer|Keyword
151    {
152        $v = $this->values->get('font-weight');
153        return $v instanceof Integer || $v instanceof Keyword ? $v : new Keyword('normal');
154    }
155
156    public function getLineHeight(): Length|Number|Keyword
157    {
158        $v = $this->values->get('line-height');
159        return $v instanceof Length || $v instanceof Number || $v instanceof Keyword
160            ? $v
161            : new Keyword('normal');
162    }
163
164    public function getTextAlign(): Keyword
165    {
166        return $this->expectKeyword('text-align', 'start');
167    }
168
169    public function getTextAlignLast(): Keyword
170    {
171        return $this->expectKeyword('text-align-last', 'auto');
172    }
173
174    public function getTextDecorationLine(): Keyword
175    {
176        return $this->expectKeyword('text-decoration-line', 'none');
177    }
178
179    public function getTextDecorationStyle(): Keyword
180    {
181        return $this->expectKeyword('text-decoration-style', 'solid');
182    }
183
184    public function getTextDecorationColor(): Color
185    {
186        return $this->expectColor('text-decoration-color', new Color(0.0, 0.0, 0.0, 1.0));
187    }
188
189    public function getTextDecorationThickness(): Length|Keyword
190    {
191        return $this->expectLengthOrKeyword('text-decoration-thickness', 'auto');
192    }
193
194    public function getTextTransform(): Keyword
195    {
196        return $this->expectKeyword('text-transform', 'none');
197    }
198
199    public function getTextIndent(): Length
200    {
201        return $this->expectLength('text-indent', 0.0);
202    }
203
204    public function getTextJustify(): Keyword
205    {
206        return $this->expectKeyword('text-justify', 'auto');
207    }
208
209    public function getLetterSpacing(): Length|Keyword
210    {
211        return $this->expectLengthOrKeyword('letter-spacing', 'normal');
212    }
213
214    public function getWordSpacing(): Length|Keyword
215    {
216        return $this->expectLengthOrKeyword('word-spacing', 'normal');
217    }
218
219    public function getWhiteSpace(): Keyword
220    {
221        return $this->expectKeyword('white-space', 'normal');
222    }
223
224    public function getWordBreak(): Keyword
225    {
226        return $this->expectKeyword('word-break', 'normal');
227    }
228
229    public function getOverflowWrap(): Keyword
230    {
231        return $this->expectKeyword('overflow-wrap', 'normal');
232    }
233
234    public function getVerticalAlign(): Length|Keyword
235    {
236        return $this->expectLengthOrKeyword('vertical-align', 'baseline');
237    }
238
239    public function getDirection(): Keyword
240    {
241        return $this->expectKeyword('direction', 'ltr');
242    }
243
244    public function getUnicodeBidi(): Keyword
245    {
246        return $this->expectKeyword('unicode-bidi', 'normal');
247    }
248
249    public function getTabSize(): Length|Integer
250    {
251        $v = $this->values->get('tab-size');
252        return $v instanceof Length || $v instanceof Integer ? $v : new Integer(8);
253    }
254
255    public function getQuotes(): Value
256    {
257        return $this->values->get('quotes') ?? new Keyword('auto');
258    }
259
260    // ========================================================================
261    // Box model
262    // ========================================================================
263
264    public function getDisplay(): Keyword
265    {
266        return $this->expectKeyword('display', 'inline');
267    }
268
269    public function getPosition(): Keyword
270    {
271        return $this->expectKeyword('position', 'static');
272    }
273
274    public function getTop(): Length|Percentage|Keyword
275    {
276        return $this->expectLengthPercentageOrKeyword('top', 'auto');
277    }
278
279    public function getRight(): Length|Percentage|Keyword
280    {
281        return $this->expectLengthPercentageOrKeyword('right', 'auto');
282    }
283
284    public function getBottom(): Length|Percentage|Keyword
285    {
286        return $this->expectLengthPercentageOrKeyword('bottom', 'auto');
287    }
288
289    public function getLeft(): Length|Percentage|Keyword
290    {
291        return $this->expectLengthPercentageOrKeyword('left', 'auto');
292    }
293
294    public function getZIndex(): Integer|Keyword
295    {
296        $v = $this->values->get('z-index');
297        return $v instanceof Integer || $v instanceof Keyword ? $v : new Keyword('auto');
298    }
299
300    public function getWidth(): Length|Percentage|Keyword
301    {
302        return $this->expectLengthPercentageOrKeyword('width', 'auto');
303    }
304
305    public function getHeight(): Length|Percentage|Keyword
306    {
307        return $this->expectLengthPercentageOrKeyword('height', 'auto');
308    }
309
310    public function getMinWidth(): Length|Percentage|Keyword
311    {
312        return $this->expectLengthPercentageOrKeyword('min-width', 'auto');
313    }
314
315    public function getMinHeight(): Length|Percentage|Keyword
316    {
317        return $this->expectLengthPercentageOrKeyword('min-height', 'auto');
318    }
319
320    public function getMaxWidth(): Length|Percentage|Keyword
321    {
322        return $this->expectLengthPercentageOrKeyword('max-width', 'none');
323    }
324
325    public function getMaxHeight(): Length|Percentage|Keyword
326    {
327        return $this->expectLengthPercentageOrKeyword('max-height', 'none');
328    }
329
330    public function getMarginTop(): Length|Percentage|Keyword
331    {
332        return $this->expectLengthPercentageOrKeyword('margin-top', 'auto');
333    }
334
335    public function getMarginRight(): Length|Percentage|Keyword
336    {
337        return $this->expectLengthPercentageOrKeyword('margin-right', 'auto');
338    }
339
340    public function getMarginBottom(): Length|Percentage|Keyword
341    {
342        return $this->expectLengthPercentageOrKeyword('margin-bottom', 'auto');
343    }
344
345    public function getMarginLeft(): Length|Percentage|Keyword
346    {
347        return $this->expectLengthPercentageOrKeyword('margin-left', 'auto');
348    }
349
350    public function getPaddingTop(): Length|Percentage
351    {
352        return $this->expectLengthOrPercentage('padding-top', 0.0);
353    }
354
355    public function getPaddingRight(): Length|Percentage
356    {
357        return $this->expectLengthOrPercentage('padding-right', 0.0);
358    }
359
360    public function getPaddingBottom(): Length|Percentage
361    {
362        return $this->expectLengthOrPercentage('padding-bottom', 0.0);
363    }
364
365    public function getPaddingLeft(): Length|Percentage
366    {
367        return $this->expectLengthOrPercentage('padding-left', 0.0);
368    }
369
370    public function getBorderTopWidth(): Length
371    {
372        return $this->expectBorderWidth('border-top-width');
373    }
374
375    public function getBorderRightWidth(): Length
376    {
377        return $this->expectBorderWidth('border-right-width');
378    }
379
380    public function getBorderBottomWidth(): Length
381    {
382        return $this->expectBorderWidth('border-bottom-width');
383    }
384
385    public function getBorderLeftWidth(): Length
386    {
387        return $this->expectBorderWidth('border-left-width');
388    }
389
390    /**
391     * Resolve `border-*-width` cascaded values to Length per
392     * CSS Backgrounds 3 §4.4. Accepts the named keyword forms
393     * (`thin` = 1px, `medium` = 3px, `thick` = 5px) in addition
394     * to bare Length.
395     */
396    private function expectBorderWidth(string $prop): Length
397    {
398        $v = $this->values->get($prop);
399        if ($v instanceof Length) {
400            return $v;
401        }
402        if ($v instanceof Keyword) {
403            $px = match (strtolower($v->name)) {
404                'thin' => 1.0,
405                'medium' => 3.0,
406                'thick' => 5.0,
407                default => 0.0,
408            };
409            return new Length($px, \Phpdftk\Css\Value\LengthUnit::Px);
410        }
411        return new Length(3.0, \Phpdftk\Css\Value\LengthUnit::Px);
412    }
413
414    public function getBorderTopStyle(): Keyword
415    {
416        return $this->expectKeyword('border-top-style', 'none');
417    }
418
419    public function getBorderRightStyle(): Keyword
420    {
421        return $this->expectKeyword('border-right-style', 'none');
422    }
423
424    public function getBorderBottomStyle(): Keyword
425    {
426        return $this->expectKeyword('border-bottom-style', 'none');
427    }
428
429    public function getBorderLeftStyle(): Keyword
430    {
431        return $this->expectKeyword('border-left-style', 'none');
432    }
433
434    public function getBorderTopColor(): Color
435    {
436        return $this->expectColor('border-top-color', new Color(0.0, 0.0, 0.0, 1.0));
437    }
438
439    public function getBorderRightColor(): Color
440    {
441        return $this->expectColor('border-right-color', new Color(0.0, 0.0, 0.0, 1.0));
442    }
443
444    public function getBorderBottomColor(): Color
445    {
446        return $this->expectColor('border-bottom-color', new Color(0.0, 0.0, 0.0, 1.0));
447    }
448
449    public function getBorderLeftColor(): Color
450    {
451        return $this->expectColor('border-left-color', new Color(0.0, 0.0, 0.0, 1.0));
452    }
453
454    public function getBorderTopLeftRadius(): Value
455    {
456        return $this->values->get('border-top-left-radius') ?? new Length(0.0, \Phpdftk\Css\Value\LengthUnit::Px);
457    }
458
459    public function getBorderTopRightRadius(): Value
460    {
461        return $this->values->get('border-top-right-radius') ?? new Length(0.0, \Phpdftk\Css\Value\LengthUnit::Px);
462    }
463
464    public function getBorderBottomLeftRadius(): Value
465    {
466        return $this->values->get('border-bottom-left-radius') ?? new Length(0.0, \Phpdftk\Css\Value\LengthUnit::Px);
467    }
468
469    public function getBorderBottomRightRadius(): Value
470    {
471        return $this->values->get('border-bottom-right-radius') ?? new Length(0.0, \Phpdftk\Css\Value\LengthUnit::Px);
472    }
473
474    public function getBoxSizing(): Keyword
475    {
476        return $this->expectKeyword('box-sizing', 'content-box');
477    }
478
479    public function getBoxShadow(): Value
480    {
481        return $this->values->get('box-shadow') ?? new Keyword('none');
482    }
483
484    public function getOverflow(): Keyword
485    {
486        return $this->expectKeyword('overflow', 'visible');
487    }
488
489    public function getOverflowX(): Keyword
490    {
491        return $this->expectKeyword('overflow-x', 'visible');
492    }
493
494    public function getOverflowY(): Keyword
495    {
496        return $this->expectKeyword('overflow-y', 'visible');
497    }
498
499    public function getVisibility(): Keyword
500    {
501        return $this->expectKeyword('visibility', 'visible');
502    }
503
504    public function getOutlineWidth(): Length
505    {
506        return $this->expectLength('outline-width', 0.0);
507    }
508
509    public function getOutlineStyle(): Keyword
510    {
511        return $this->expectKeyword('outline-style', 'none');
512    }
513
514    public function getOutlineColor(): Color
515    {
516        return $this->expectColor('outline-color', new Color(0.0, 0.0, 0.0, 1.0));
517    }
518
519    public function getOutlineOffset(): Length
520    {
521        return $this->expectLength('outline-offset', 0.0);
522    }
523
524    public function getFloat(): Keyword
525    {
526        return $this->expectKeyword('float', 'none');
527    }
528
529    public function getClear(): Keyword
530    {
531        return $this->expectKeyword('clear', 'none');
532    }
533
534    public function getAspectRatio(): Value
535    {
536        return $this->values->get('aspect-ratio') ?? new Keyword('auto');
537    }
538
539    // ========================================================================
540    // Flex
541    // ========================================================================
542
543    public function getFlexDirection(): Keyword
544    {
545        return $this->expectKeyword('flex-direction', 'row');
546    }
547
548    public function getFlexWrap(): Keyword
549    {
550        return $this->expectKeyword('flex-wrap', 'nowrap');
551    }
552
553    public function getJustifyContent(): Keyword
554    {
555        return $this->expectKeyword('justify-content', 'flex-start');
556    }
557
558    public function getAlignItems(): Keyword
559    {
560        return $this->expectKeyword('align-items', 'stretch');
561    }
562
563    public function getAlignContent(): Keyword
564    {
565        return $this->expectKeyword('align-content', 'stretch');
566    }
567
568    public function getAlignSelf(): Keyword
569    {
570        return $this->expectKeyword('align-self', 'auto');
571    }
572
573    public function getFlexGrow(): Number
574    {
575        return $this->expectNumber('flex-grow', 0.0);
576    }
577
578    public function getFlexShrink(): Number
579    {
580        return $this->expectNumber('flex-shrink', 1.0);
581    }
582
583    public function getFlexBasis(): Length|Percentage|Keyword
584    {
585        return $this->expectLengthPercentageOrKeyword('flex-basis', 'auto');
586    }
587
588    public function getOrder(): Integer
589    {
590        $v = $this->values->get('order');
591        return $v instanceof Integer ? $v : new Integer(0);
592    }
593
594    public function getRowGap(): Length|Keyword
595    {
596        return $this->expectLengthOrKeyword('row-gap', 'normal');
597    }
598
599    public function getColumnGap(): Length|Keyword
600    {
601        return $this->expectLengthOrKeyword('column-gap', 'normal');
602    }
603
604    // ========================================================================
605    // Tables
606    // ========================================================================
607
608    public function getBorderCollapse(): Keyword
609    {
610        return $this->expectKeyword('border-collapse', 'separate');
611    }
612
613    public function getCaptionSide(): Keyword
614    {
615        return $this->expectKeyword('caption-side', 'top');
616    }
617
618    // ========================================================================
619    // Lists
620    // ========================================================================
621
622    public function getListStyleType(): Keyword|CssFunction
623    {
624        $v = $this->values->get('list-style-type');
625        return $v instanceof Keyword || $v instanceof CssFunction
626            ? $v
627            : new Keyword('disc');
628    }
629
630    public function getListStylePosition(): Keyword
631    {
632        return $this->expectKeyword('list-style-position', 'outside');
633    }
634
635    public function getListStyleImage(): Value
636    {
637        return $this->values->get('list-style-image') ?? new Keyword('none');
638    }
639
640    // ========================================================================
641    // Paged media
642    // ========================================================================
643
644    public function getPage(): Keyword|CssFunction
645    {
646        $v = $this->values->get('page');
647        return $v instanceof Keyword || $v instanceof CssFunction
648            ? $v
649            : new Keyword('auto');
650    }
651
652    public function getBreakBefore(): Keyword
653    {
654        return $this->expectKeyword('break-before', 'auto');
655    }
656
657    public function getBreakAfter(): Keyword
658    {
659        return $this->expectKeyword('break-after', 'auto');
660    }
661
662    public function getBreakInside(): Keyword
663    {
664        return $this->expectKeyword('break-inside', 'auto');
665    }
666
667    public function getBoxDecorationBreak(): Keyword
668    {
669        return $this->expectKeyword('box-decoration-break', 'slice');
670    }
671
672    public function getOrphans(): Integer
673    {
674        $v = $this->values->get('orphans');
675        return $v instanceof Integer ? $v : new Integer(2);
676    }
677
678    public function getWidows(): Integer
679    {
680        $v = $this->values->get('widows');
681        return $v instanceof Integer ? $v : new Integer(2);
682    }
683
684    // ========================================================================
685    // Multi-column
686    // ========================================================================
687
688    public function getColumnCount(): Integer|Keyword
689    {
690        $v = $this->values->get('column-count');
691        return $v instanceof Integer || $v instanceof Keyword ? $v : new Keyword('auto');
692    }
693
694    public function getColumnWidth(): Length|Keyword
695    {
696        return $this->expectLengthOrKeyword('column-width', 'auto');
697    }
698
699    public function getColumnRuleWidth(): Length
700    {
701        return $this->expectLength('column-rule-width', 0.0);
702    }
703
704    public function getColumnRuleStyle(): Keyword
705    {
706        return $this->expectKeyword('column-rule-style', 'none');
707    }
708
709    public function getColumnRuleColor(): Color
710    {
711        return $this->expectColor('column-rule-color', new Color(0.0, 0.0, 0.0, 1.0));
712    }
713
714    public function getColumnSpan(): Keyword
715    {
716        return $this->expectKeyword('column-span', 'none');
717    }
718
719    public function getColumnFill(): Keyword
720    {
721        return $this->expectKeyword('column-fill', 'balance');
722    }
723
724    // ========================================================================
725    // Effects
726    // ========================================================================
727
728    public function getTextShadow(): Value
729    {
730        return $this->values->get('text-shadow') ?? new Keyword('none');
731    }
732
733    public function getClipPath(): Value
734    {
735        return $this->values->get('clip-path') ?? new Keyword('none');
736    }
737
738    public function getFilter(): Value
739    {
740        return $this->values->get('filter') ?? new Keyword('none');
741    }
742
743    public function getTransform(): Value
744    {
745        return $this->values->get('transform') ?? new Keyword('none');
746    }
747
748    // ========================================================================
749    // Type-narrowing helpers
750    // ========================================================================
751
752    private function expectKeyword(string $prop, string $fallback): Keyword
753    {
754        $v = $this->values->get($prop);
755        return $v instanceof Keyword ? $v : new Keyword($fallback);
756    }
757
758    private function expectColor(string $prop, Color $fallback): Color
759    {
760        $v = $this->resolveColorChain($this->values->get($prop));
761        return $v instanceof Color ? $v : $fallback;
762    }
763
764    private function expectColorOrKeyword(string $prop, string $keywordFallback): Color|Keyword
765    {
766        $v = $this->resolveColorChain($this->values->get($prop));
767        return $v instanceof Color || $v instanceof Keyword ? $v : new Keyword($keywordFallback);
768    }
769
770    /**
771     * Walk the color-resolution chain. Each step lowers a typed
772     * higher-level color form into a concrete sRGB Color or passes
773     * through unchanged. Order: ColorMix → LightDark → ContrastColor.
774     */
775    private function resolveColorChain(?Value $v): ?Value
776    {
777        return $this->resolveContrastColor(
778            $this->resolveLightDark(
779                $this->resolveColorMix($v),
780            ),
781        );
782    }
783
784    /**
785     * CSS Color 5 §3 — resolve `color-mix(in srgb, A p%, B q%)` to
786     * the linearly-interpolated sRGB Color. Other spaces (lab, lch,
787     * etc.) are passed through unchanged for now; full per-space
788     * mixing math lands with the color engine (Phase 4E).
789     */
790    private function resolveColorMix(?Value $v): ?Value
791    {
792        if (!$v instanceof ColorMix) {
793            return $v;
794        }
795        // Only sRGB-space mixing is supported here; other spaces
796        // fall through to the renderer's color engine.
797        if ($v->space !== ColorSpace::sRGB && $v->space !== ColorSpace::sRGBLinear) {
798            return $v;
799        }
800        $total = $v->percentage1 + $v->percentage2;
801        if ($total <= 0.0) {
802            return $v;
803        }
804        $t = $v->percentage2 / $total;
805        $mix = static fn(float $a, float $b): float => $a * (1.0 - $t) + $b * $t;
806        $a = $v->color1;
807        $b = $v->color2;
808        $alpha = $mix($a->a, $b->a) * $v->alphaMultiplier;
809        return new Color(
810            $mix($a->r, $b->r),
811            $mix($a->g, $b->g),
812            $mix($a->b, $b->b),
813            max(0.0, min(1.0, $alpha)),
814        );
815    }
816
817    /**
818     * CSS Color 7 §4 — `contrast-color(<color>)`. Resolves to
819     * black or white based on which has higher contrast against
820     * the base color. Computes WCAG 2.x relative luminance and
821     * picks black for the lighter half, white for the darker.
822     *
823     * Non-Color base values (a keyword like `currentcolor`,
824     * a CssFunction the engine can't yet resolve) fall through —
825     * the renderer treats `contrast-color(currentcolor)` as
826     * "currentcolor" rather than throwing.
827     */
828    private function resolveContrastColor(?Value $v): ?Value
829    {
830        if (!$v instanceof ContrastColor) {
831            return $v;
832        }
833        $base = $this->resolveLightDark($v->base);
834        if (!$base instanceof Color) {
835            return $base;
836        }
837        return self::relativeLuminance($base) > 0.5
838            ? new Color(0.0, 0.0, 0.0, 1.0)
839            : new Color(1.0, 1.0, 1.0, 1.0);
840    }
841
842    /**
843     * Compute WCAG 2.x relative luminance of an sRGB Color.
844     * Components linearised per the WCAG formula, then weighted
845     * 0.2126 R + 0.7152 G + 0.0722 B.
846     */
847    private static function relativeLuminance(Color $c): float
848    {
849        $linear = static function (float $v): float {
850            return $v <= 0.03928
851                ? $v / 12.92
852                : (($v + 0.055) / 1.055) ** 2.4;
853        };
854        return 0.2126 * $linear($c->r)
855            + 0.7152 * $linear($c->g)
856            + 0.0722 * $linear($c->b);
857    }
858
859    /**
860     * CSS Color 5 §5 — pick the active branch of a `light-dark()`
861     * call at computed-value time based on the cascaded
862     * `color-scheme` property (CSS Color Adjustment 1 §2.2):
863     *
864     *   - if `color-scheme` lists `dark` and does not list `light`,
865     *     the document opts into the dark branch;
866     *   - otherwise the light branch wins (the document default).
867     *
868     * The original {@see LightDark} value is preserved on the
869     * declaration so a future re-render under a different scheme
870     * is a value-level switch, not a re-cascade.
871     */
872    private function resolveLightDark(?Value $v): ?Value
873    {
874        if (!$v instanceof LightDark) {
875            return $v;
876        }
877        return $this->prefersDarkScheme() ? $v->dark : $v->light;
878    }
879
880    private function prefersDarkScheme(): bool
881    {
882        $scheme = $this->values->get('color-scheme');
883        $hasLight = false;
884        $hasDark = false;
885        foreach ($this->extractSchemeIdents($scheme) as $ident) {
886            $lc = strtolower($ident);
887            if ($lc === 'light') {
888                $hasLight = true;
889            } elseif ($lc === 'dark') {
890                $hasDark = true;
891            }
892        }
893        return $hasDark && !$hasLight;
894    }
895
896    /**
897     * @return list<string>
898     */
899    private function extractSchemeIdents(?Value $value): array
900    {
901        if ($value instanceof Keyword) {
902            return [$value->name];
903        }
904        if ($value instanceof ValueList) {
905            $out = [];
906            foreach ($value->values as $v) {
907                if ($v instanceof Keyword) {
908                    $out[] = $v->name;
909                }
910            }
911            return $out;
912        }
913        return [];
914    }
915
916    private function expectLength(string $prop, float $fallbackPx): Length
917    {
918        $v = $this->values->get($prop);
919        return $v instanceof Length ? $v : new Length($fallbackPx, \Phpdftk\Css\Value\LengthUnit::Px);
920    }
921
922    private function expectLengthOrKeyword(string $prop, string $keywordFallback): Length|Keyword
923    {
924        $v = $this->values->get($prop);
925        return $v instanceof Length || $v instanceof Keyword ? $v : new Keyword($keywordFallback);
926    }
927
928    private function expectLengthOrPercentage(string $prop, float $fallbackPx): Length|Percentage
929    {
930        $v = $this->values->get($prop);
931        return $v instanceof Length || $v instanceof Percentage
932            ? $v
933            : new Length($fallbackPx, \Phpdftk\Css\Value\LengthUnit::Px);
934    }
935
936    private function expectLengthPercentageOrKeyword(string $prop, string $keywordFallback): Length|Percentage|Keyword
937    {
938        $v = $this->values->get($prop);
939        return $v instanceof Length || $v instanceof Percentage || $v instanceof Keyword
940            ? $v
941            : new Keyword($keywordFallback);
942    }
943
944    private function expectNumber(string $prop, float $fallback): Number
945    {
946        $v = $this->values->get($prop);
947        return $v instanceof Number ? $v : new Number($fallback);
948    }
949}