Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
98.80% covered (success)
98.80%
165 / 167
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
PropertyRegistry
98.80% covered (success)
98.80%
165 / 167
60.00% covered (warning)
60.00%
3 / 5
6
0.00% covered (danger)
0.00%
0 / 1
 register
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 get
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 has
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 all
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 default
100.00% covered (success)
100.00%
160 / 160
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Cascade;
6
7use Phpdftk\Css\Value\Color;
8use Phpdftk\Css\Value\ColorSpace;
9use Phpdftk\Css\Value\Keyword;
10use Phpdftk\Css\Value\Length;
11use Phpdftk\Css\Value\LengthUnit;
12use Phpdftk\Css\Value\ListSeparator;
13use Phpdftk\Css\Value\Number;
14use Phpdftk\Css\Value\Percentage;
15use Phpdftk\Css\Value\StringValue;
16use Phpdftk\Css\Value\Value;
17use Phpdftk\Css\Value\ValueList;
18
19/**
20 * Registry of CSS properties recognised by the cascade. Each entry binds a
21 * lower-cased property name to its `PropertyDefinition` (initial value,
22 * inherits flag).
23 *
24 * The cascade consults this registry for two things:
25 *  1. When the cascade encounters `initial`, return the initial value here.
26 *  2. When walking inheritance, properties with `inherits=false` reset to
27 *     initial on each element rather than copying the parent's value.
28 *
29 * Phase 1D.3 ships a pragmatic subset (~50 properties — colour, font,
30 * box-model, text, basic decoration). Sub-phase 1E onwards will add
31 * properties as box-generation / layout grows. The registry is additive;
32 * registering a duplicate name throws.
33 */
34final class PropertyRegistry
35{
36    /** @var array<string, PropertyDefinition> */
37    private array $definitions = [];
38
39    public function register(PropertyDefinition $def): void
40    {
41        $name = strtolower($def->name);
42        if (isset($this->definitions[$name])) {
43            throw new \LogicException("Property '$name' already registered");
44        }
45        $this->definitions[$name] = $def;
46    }
47
48    public function get(string $name): ?PropertyDefinition
49    {
50        return $this->definitions[strtolower($name)] ?? null;
51    }
52
53    public function has(string $name): bool
54    {
55        return isset($this->definitions[strtolower($name)]);
56    }
57
58    /** @return array<string, PropertyDefinition> */
59    public function all(): array
60    {
61        return $this->definitions;
62    }
63
64    /**
65     * Default registry populated with the Phase-1 MVP property surface.
66     * Sufficient to cascade the invoice fixture; additional properties are
67     * added by host code calling `register()` for now (1E sub-phases will
68     * formalise the surface).
69     */
70    public static function default(): self
71    {
72        $r = new self();
73        $black = new Color(0, 0, 0, 1, ColorSpace::sRGB);
74        $transparent = new Color(0, 0, 0, 0, ColorSpace::sRGB);
75        $zero = new Length(0, LengthUnit::Px);
76        $initial = static fn(string $name, Value $v, bool $inh = false): PropertyDefinition
77            => new PropertyDefinition($name, $v, $inh);
78
79        // Color & background.
80        $r->register($initial('color', $black, true));
81        $r->register($initial('background-color', $transparent));
82        $r->register($initial('background-image', new Keyword('none')));
83        $r->register($initial('background-repeat', new Keyword('repeat')));
84        $r->register($initial('background-position', new ValueList([], ListSeparator::Space)));
85        $r->register($initial('background-size', new Keyword('auto')));
86        // CSS Backgrounds 3 §3.5 — `background-clip` & `background-origin`.
87        // `clip` controls the painted area; `origin` controls the
88        // positioning reference. Both initial to `border-box` and
89        // `padding-box` respectively per spec. Neither inherits.
90        $r->register($initial('background-clip', new Keyword('border-box')));
91        $r->register($initial('background-origin', new Keyword('padding-box')));
92        // Print-irrelevant but registered so author CSS isn't dropped.
93        $r->register($initial('background-attachment', new Keyword('scroll')));
94        $r->register($initial('opacity', new Number(1.0)));
95
96        // Font & text.
97        $r->register($initial('font-family', new ValueList([new StringValue('serif')], ListSeparator::Comma), true));
98        $r->register($initial('font-size', new Length(16.0, LengthUnit::Px), true));
99        $r->register($initial('font-style', new Keyword('normal'), true));
100        $r->register($initial('font-weight', new Keyword('normal'), true));
101        $r->register($initial('line-height', new Keyword('normal'), true));
102        $r->register($initial('text-align', new Keyword('start'), true));
103        // CSS Text 3 §7.4 — `text-align-last` controls the alignment of
104        // the last line in a justified block. `auto` (initial) defers
105        // to the spec's default: start-aligned when text-align: justify
106        // / start / end / left / right; matches text-align otherwise.
107        // Inherits per spec.
108        $r->register($initial('text-align-last', new Keyword('auto'), true));
109        $r->register($initial('text-decoration', new Keyword('none')));
110        $r->register($initial('text-decoration-line', new Keyword('none')));
111        // CSS Text Decoration 4 §3: the initial value is `currentColor`.
112        // Consumers resolve the keyword against the element's `color` at
113        // use-time so an `<a>` with `color: blue` paints a blue underline
114        // even though `text-decoration-color` was never explicitly set.
115        $r->register($initial('text-decoration-color', new Keyword('currentcolor')));
116        $r->register($initial('text-decoration-style', new Keyword('solid')));
117        // CSS Text Decoration 4 §4 — `auto` defers to the font's
118        // OS/2 underline metrics; an explicit `<length>` (or
119        // `<percentage>` relative to the font size) overrides.
120        $r->register($initial('text-decoration-thickness', new Keyword('auto')));
121        $r->register($initial('text-underline-offset', new Keyword('auto')));
122        $r->register($initial('text-shadow', new Keyword('none'), true));
123        $r->register($initial('text-transform', new Keyword('none'), true));
124        // CSS Text 3 §7.5: `text-justify` controls the justification
125        // algorithm used when `text-align: justify`. `auto` (initial)
126        // lets the UA pick a reasonable default; `none` disables
127        // justification entirely (the line falls back to its declared
128        // text-align direction). `inter-word` / `inter-character`
129        // accepted but resolve as `auto` at Phase 1.
130        $r->register($initial('text-justify', new Keyword('auto'), true));
131        $r->register($initial('text-indent', $zero, true));
132        $r->register($initial('letter-spacing', new Keyword('normal'), true));
133        $r->register($initial('word-spacing', new Keyword('normal'), true));
134        $r->register($initial('white-space', new Keyword('normal'), true));
135        $r->register($initial('direction', new Keyword('ltr'), true));
136        $r->register($initial('unicode-bidi', new Keyword('normal')));
137        $r->register($initial('writing-mode', new Keyword('horizontal-tb'), true));
138        $r->register($initial('text-orientation', new Keyword('mixed'), true));
139        $r->register($initial('vertical-align', new Keyword('baseline')));
140        // CSS UI 4 — pointer, user-select, cursor are runtime-only on
141        // print but author CSS shouldn't be dropped at cascade time.
142        $r->register($initial('cursor', new Keyword('auto'), true));
143        $r->register($initial('user-select', new Keyword('auto')));
144        $r->register($initial('pointer-events', new Keyword('auto'), true));
145        // CSS UI 4 — `caret-color` (text caret colour) and CSS UI 4
146        // `accent-color` (UA-widget accent). Both are runtime-only
147        // for print but register so author CSS isn't dropped at the
148        // cascade. `caret-color` inherits per spec.
149        $r->register($initial('caret-color', new Keyword('auto'), true));
150        $r->register($initial('accent-color', new Keyword('auto')));
151
152        // Box model.
153        $r->register($initial('display', new Keyword('inline')));
154        $r->register($initial('position', new Keyword('static')));
155        $r->register($initial('top', new Keyword('auto')));
156        $r->register($initial('right', new Keyword('auto')));
157        $r->register($initial('bottom', new Keyword('auto')));
158        $r->register($initial('left', new Keyword('auto')));
159        $r->register($initial('z-index', new Keyword('auto')));
160        $r->register($initial('width', new Keyword('auto')));
161        $r->register($initial('height', new Keyword('auto')));
162        $r->register($initial('min-width', $zero));
163        $r->register($initial('min-height', $zero));
164        $r->register($initial('max-width', new Keyword('none')));
165        $r->register($initial('max-height', new Keyword('none')));
166        $r->register($initial('margin-top', $zero));
167        $r->register($initial('margin-right', $zero));
168        $r->register($initial('margin-bottom', $zero));
169        $r->register($initial('margin-left', $zero));
170        $r->register($initial('padding-top', $zero));
171        $r->register($initial('padding-right', $zero));
172        $r->register($initial('padding-bottom', $zero));
173        $r->register($initial('padding-left', $zero));
174        $r->register($initial('border-top-width', new Length(3.0, LengthUnit::Px))); // 'medium' = 3px
175        $r->register($initial('border-right-width', new Length(3.0, LengthUnit::Px)));
176        $r->register($initial('border-bottom-width', new Length(3.0, LengthUnit::Px)));
177        $r->register($initial('border-left-width', new Length(3.0, LengthUnit::Px)));
178        $r->register($initial('border-top-style', new Keyword('none')));
179        $r->register($initial('border-right-style', new Keyword('none')));
180        $r->register($initial('border-bottom-style', new Keyword('none')));
181        $r->register($initial('border-left-style', new Keyword('none')));
182        $r->register($initial('border-top-color', $black));
183        $r->register($initial('border-right-color', $black));
184        $r->register($initial('border-bottom-color', $black));
185        $r->register($initial('border-left-color', $black));
186        // Border-radius (CSS Backgrounds 3 §6) — Phase 1 reads a uniform
187        // radius from the shorthand and the four corner longhands.
188        $zero = new Length(0.0, LengthUnit::Px);
189        $r->register($initial('border-top-left-radius', $zero));
190        $r->register($initial('border-top-right-radius', $zero));
191        $r->register($initial('border-bottom-right-radius', $zero));
192        $r->register($initial('border-bottom-left-radius', $zero));
193
194        // CSS UI 3 §4 outline — like border but doesn't take part in
195        // layout (drawn outside the border edge).
196        $r->register($initial('outline-color', $black));
197        $r->register($initial('outline-style', new Keyword('none')));
198        $r->register($initial('outline-width', new Length(3.0, LengthUnit::Px)));
199        $r->register($initial('outline-offset', $zero));
200        $r->register($initial('box-sizing', new Keyword('content-box')));
201        // CSS Sizing 4 §4.2 — `aspect-ratio` constrains the box's
202        // width-to-height ratio. `auto` (initial, non-inheriting)
203        // means no constraint; `<ratio>` form is honoured by
204        // BlockLayout when width OR height is auto.
205        $r->register($initial('aspect-ratio', new Keyword('auto')));
206        $r->register($initial('overflow', new Keyword('visible')));
207        // CSS Overflow 3 §3.1 — per-axis longhands. `overflow` is
208        // the shorthand (CSS Overflow 3 §3.2). Painter clips if
209        // EITHER axis is non-visible.
210        $r->register($initial('overflow-x', new Keyword('visible')));
211        $r->register($initial('overflow-y', new Keyword('visible')));
212        $r->register($initial('visibility', new Keyword('visible'), true));
213        // CSS Images 3 §5.4 — `image-rendering` controls how the UA
214        // scales raster images. Print rendering treats them as `auto`
215        // regardless, but register so author CSS isn't dropped.
216        $r->register($initial('image-rendering', new Keyword('auto')));
217        // CSS Fonts 4 §6.5 — `font-kerning` toggles OpenType kern.
218        // `auto` (initial) means the UA decides; inherits.
219        $r->register($initial('font-kerning', new Keyword('auto'), true));
220        $r->register($initial('font-feature-settings', new Keyword('normal'), true));
221        $r->register($initial('font-variation-settings', new Keyword('normal'), true));
222        // CSS Compositing 1 — `isolation` controls stacking context;
223        // print medium has no blending compositing layers but register
224        // so cascade keeps the value.
225        $r->register($initial('isolation', new Keyword('auto')));
226        $r->register($initial('mix-blend-mode', new Keyword('normal')));
227        // CSS Color Adjustment 1 — `print-color-adjust` (initial `economy`)
228        // tells the UA whether it may adjust colours for print
229        // (downsampling, removing bg). `color-scheme` hints
230        // light / dark preference. `forced-color-adjust` opts out of
231        // forced-colors (high contrast) on Windows. All print-irrelevant
232        // for our default-economy output but registered so author CSS
233        // isn't dropped. All inherit per spec.
234        $r->register($initial('print-color-adjust', new Keyword('economy'), true));
235        $r->register($initial('color-scheme', new Keyword('normal'), true));
236        $r->register($initial('forced-color-adjust', new Keyword('auto'), true));
237
238        // CSS Images 3 §5: `object-fit` controls how a replaced element
239        // (currently `<img>`) scales within its declared width × height.
240        // Initial `fill` matches the legacy "stretch to box" behaviour.
241        $r->register($initial('object-fit', new Keyword('fill')));
242        $r->register($initial('object-position', new Keyword('center')));
243
244        // Generated content (CSS Generated Content 3) — only the host's
245        // `::before` / `::after` pseudo-elements read `content`.
246        $r->register($initial('content', new Keyword('normal')));
247        $r->register($initial('counter-reset', new Keyword('none')));
248        $r->register($initial('counter-increment', new Keyword('none')));
249        // CSS Generated Content 3 §3.1: `quotes` controls the strings
250        // emitted by `open-quote` / `close-quote` in `content`.
251        // Initial `auto` — UA picks typographic defaults; explicit value
252        // is a space-separated list of paired strings. Inherits.
253        $r->register($initial('quotes', new Keyword('auto'), true));
254
255        // CSS UI 3 §6.2 text-overflow — when the content overflows the
256        // single-line box, replace the visible tail with U+2026 HORIZONTAL
257        // ELLIPSIS.
258        $r->register($initial('text-overflow', new Keyword('clip')));
259
260        // CSS Tables 3 §10. Phase-1 doesn't act on these values yet
261        // (cells always paint their own borders, no spacing), but
262        // registering them prevents author CSS from being dropped at
263        // computed-value time.
264        $r->register($initial('border-collapse', new Keyword('separate'), true));
265        $r->register($initial('border-spacing', new Length(0.0, LengthUnit::Px), true));
266        $r->register($initial('table-layout', new Keyword('auto')));
267        $r->register($initial('caption-side', new Keyword('top'), true));
268        $r->register($initial('empty-cells', new Keyword('show'), true));
269
270        // CSS Text 3 §11.2 — `tab-size` integer (number of advance
271        // spaces) or length. Initial value 8, inheriting.
272        $r->register($initial('tab-size', new \Phpdftk\Css\Value\Integer(8), true));
273
274        // CSS Text 3 §5 word-break / overflow-wrap.
275        $r->register($initial('word-break', new Keyword('normal'), true));
276        $r->register($initial('overflow-wrap', new Keyword('normal'), true));
277        $r->register($initial('word-wrap', new Keyword('normal'), true));
278
279        // CSS Fragmentation 4 §3 + legacy `page-break-*` aliases.
280        $r->register($initial('break-before', new Keyword('auto')));
281        $r->register($initial('break-after', new Keyword('auto')));
282        $r->register($initial('break-inside', new Keyword('auto')));
283        $r->register($initial('page-break-before', new Keyword('auto')));
284        $r->register($initial('page-break-after', new Keyword('auto')));
285        $r->register($initial('page-break-inside', new Keyword('auto')));
286        // CSS Fragmentation 4 §5.5: `box-decoration-break` controls
287        // how a box's borders / padding / backgrounds / box-shadow
288        // render across fragments. `slice` (initial) treats the box as
289        // one rectangle clipped by the fragmentainer — only the
290        // outermost edges paint at fragment seams. `clone` paints full
291        // decorations on every fragment. Non-inherited per spec.
292        $r->register($initial('box-decoration-break', new Keyword('slice')));
293        // CSS Paged Media 3 §3.4: `page` names a page type defined by
294        // an `@page <name>` at-rule. When a block has a non-`auto`
295        // `page` value, its first fragment forces a page break and
296        // that page picks up the named rule's margins / background /
297        // margin-boxes. Non-inherited per spec.
298        $r->register($initial('page', new Keyword('auto')));
299        // CSS Transforms 2 §6 — `transform` accepts a list of
300        // transform functions (translate / rotate / scale / skew /
301        // matrix and 3D variants); `transform-origin` picks the
302        // pivot point. Both are non-inherited per spec. Phase-2
303        // implementation honours the 2D subset.
304        $r->register($initial('transform', new Keyword('none')));
305        $r->register($initial('transform-origin', new ValueList(
306            [new Percentage(50.0), new Percentage(50.0)],
307            ListSeparator::Space,
308        )));
309        // CSS Fragmentation 4 §4: orphans / widows. Initial 2, both
310        // inherit; layout uses them to gate where a paragraph may split
311        // across a page boundary.
312        $r->register($initial('orphans', new \Phpdftk\Css\Value\Integer(2), true));
313        $r->register($initial('widows', new \Phpdftk\Css\Value\Integer(2), true));
314
315        // Lists. All three inherit per CSS Lists 3 §1.
316        $r->register($initial('list-style-type', new Keyword('disc'), true));
317        $r->register($initial('list-style-position', new Keyword('outside'), true));
318        $r->register($initial('list-style-image', new Keyword('none'), true));
319
320        // CSS Backgrounds 3 §6 — `box-shadow` doesn't inherit.
321        $r->register($initial('box-shadow', new Keyword('none')));
322
323        // CSS 2.1 §9.5 — floats. Neither inherits.
324        $r->register($initial('float', new Keyword('none')));
325        $r->register($initial('clear', new Keyword('none')));
326
327        // CSS Flexible Box Layout 1 — flex container + item
328        // properties. None inherit per spec.
329        $r->register($initial('flex-direction', new Keyword('row')));
330        $r->register($initial('flex-wrap', new Keyword('nowrap')));
331        $r->register($initial('justify-content', new Keyword('flex-start')));
332        $r->register($initial('align-items', new Keyword('stretch')));
333        $r->register($initial('align-self', new Keyword('auto')));
334        $r->register($initial('align-content', new Keyword('stretch')));
335        $r->register($initial('flex-grow', new Number(0)));
336        $r->register($initial('flex-shrink', new Number(1)));
337        $r->register($initial('flex-basis', new Keyword('auto')));
338        $r->register($initial('order', new \Phpdftk\Css\Value\Integer(0)));
339
340        // CSS Multi-column 1 §2-3. None inherit. `column-gap` initial is
341        // `normal`, which Multi-column 1 §3.1 resolves to `1em`.
342        $r->register($initial('column-count', new Keyword('auto')));
343        $r->register($initial('column-width', new Keyword('auto')));
344        $r->register($initial('column-gap', new Keyword('normal')));
345        // CSS Box Alignment 3 §8.3 — `row-gap` for flex / grid /
346        // multi-column rows. Initial `normal`, non-inheriting. The
347        // `gap` shorthand sets both row-gap and column-gap (handled
348        // in ShorthandExpander).
349        $r->register($initial('row-gap', new Keyword('normal')));
350        $r->register($initial('column-rule-width', new Length(3.0, LengthUnit::Px))); // medium
351        $r->register($initial('column-rule-style', new Keyword('none')));
352        $r->register($initial('column-rule-color', new Keyword('currentcolor')));
353        $r->register($initial('column-fill', new Keyword('balance')));
354        $r->register($initial('column-span', new Keyword('none')));
355
356        return $r;
357    }
358}