Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.17% covered (success)
98.17%
215 / 219
97.87% covered (success)
97.87%
138 / 141
CRAP
0.00% covered (danger)
0.00%
0 / 1
ComputedStyle
98.17% covered (success)
98.17%
215 / 219
97.87% covered (success)
97.87%
138 / 141
194
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
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 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 <space>, A p%, B q%)` to a
786     * concrete sRGB Color by interpolating the arguments in the named space
787     * (polar spaces travel the hue direction). Delegates to
788     * {@see ColorMix::evaluate()}, which drives the shared
789     * {@see ColorConverter::interpolate()} engine.
790     */
791    private function resolveColorMix(?Value $v): ?Value
792    {
793        if (!$v instanceof ColorMix) {
794            return $v;
795        }
796        return $v->evaluate();
797    }
798
799    /**
800     * CSS Color 7 §4 — `contrast-color(<color>)`. Resolves to
801     * black or white based on which has higher contrast against
802     * the base color. Computes WCAG 2.x relative luminance and
803     * picks black for the lighter half, white for the darker.
804     *
805     * Non-Color base values (a keyword like `currentcolor`,
806     * a CssFunction the engine can't yet resolve) fall through —
807     * the renderer treats `contrast-color(currentcolor)` as
808     * "currentcolor" rather than throwing.
809     */
810    private function resolveContrastColor(?Value $v): ?Value
811    {
812        if (!$v instanceof ContrastColor) {
813            return $v;
814        }
815        $base = $this->resolveLightDark($v->base);
816        if (!$base instanceof Color) {
817            return $base;
818        }
819        return self::relativeLuminance($base) > 0.5
820            ? new Color(0.0, 0.0, 0.0, 1.0)
821            : new Color(1.0, 1.0, 1.0, 1.0);
822    }
823
824    /**
825     * Compute WCAG 2.x relative luminance of an sRGB Color.
826     * Components linearised per the WCAG formula, then weighted
827     * 0.2126 R + 0.7152 G + 0.0722 B.
828     */
829    private static function relativeLuminance(Color $c): float
830    {
831        $linear = static function (float $v): float {
832            return $v <= 0.03928
833                ? $v / 12.92
834                : (($v + 0.055) / 1.055) ** 2.4;
835        };
836        return 0.2126 * $linear($c->r)
837            + 0.7152 * $linear($c->g)
838            + 0.0722 * $linear($c->b);
839    }
840
841    /**
842     * CSS Color 5 §5 — pick the active branch of a `light-dark()`
843     * call at computed-value time based on the cascaded
844     * `color-scheme` property (CSS Color Adjustment 1 §2.2):
845     *
846     *   - if `color-scheme` lists `dark` and does not list `light`,
847     *     the document opts into the dark branch;
848     *   - otherwise the light branch wins (the document default).
849     *
850     * The original {@see LightDark} value is preserved on the
851     * declaration so a future re-render under a different scheme
852     * is a value-level switch, not a re-cascade.
853     */
854    private function resolveLightDark(?Value $v): ?Value
855    {
856        if (!$v instanceof LightDark) {
857            return $v;
858        }
859        return $this->prefersDarkScheme() ? $v->dark : $v->light;
860    }
861
862    private function prefersDarkScheme(): bool
863    {
864        $scheme = $this->values->get('color-scheme');
865        $hasLight = false;
866        $hasDark = false;
867        foreach ($this->extractSchemeIdents($scheme) as $ident) {
868            $lc = strtolower($ident);
869            if ($lc === 'light') {
870                $hasLight = true;
871            } elseif ($lc === 'dark') {
872                $hasDark = true;
873            }
874        }
875        return $hasDark && !$hasLight;
876    }
877
878    /**
879     * @return list<string>
880     */
881    private function extractSchemeIdents(?Value $value): array
882    {
883        if ($value instanceof Keyword) {
884            return [$value->name];
885        }
886        if ($value instanceof ValueList) {
887            $out = [];
888            foreach ($value->values as $v) {
889                if ($v instanceof Keyword) {
890                    $out[] = $v->name;
891                }
892            }
893            return $out;
894        }
895        return [];
896    }
897
898    private function expectLength(string $prop, float $fallbackPx): Length
899    {
900        $v = $this->values->get($prop);
901        return $v instanceof Length ? $v : new Length($fallbackPx, \Phpdftk\Css\Value\LengthUnit::Px);
902    }
903
904    private function expectLengthOrKeyword(string $prop, string $keywordFallback): Length|Keyword
905    {
906        $v = $this->values->get($prop);
907        return $v instanceof Length || $v instanceof Keyword ? $v : new Keyword($keywordFallback);
908    }
909
910    private function expectLengthOrPercentage(string $prop, float $fallbackPx): Length|Percentage
911    {
912        $v = $this->values->get($prop);
913        return $v instanceof Length || $v instanceof Percentage
914            ? $v
915            : new Length($fallbackPx, \Phpdftk\Css\Value\LengthUnit::Px);
916    }
917
918    private function expectLengthPercentageOrKeyword(string $prop, string $keywordFallback): Length|Percentage|Keyword
919    {
920        $v = $this->values->get($prop);
921        return $v instanceof Length || $v instanceof Percentage || $v instanceof Keyword
922            ? $v
923            : new Keyword($keywordFallback);
924    }
925
926    private function expectNumber(string $prop, float $fallback): Number
927    {
928        $v = $this->values->get($prop);
929        return $v instanceof Number ? $v : new Number($fallback);
930    }
931}