Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
77.03% covered (warning)
77.03%
57 / 74
81.82% covered (warning)
81.82%
9 / 11
CRAP
0.00% covered (danger)
0.00%
0 / 1
WritingMode
77.03% covered (warning)
77.03%
57 / 74
81.82% covered (warning)
81.82%
9 / 11
71.65
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
 blockAxis
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 inlineAxis
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 blockDirection
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 inlineDirection
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 isVertical
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isHorizontal
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 isSideways
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 physicalEdge
93.75% covered (success)
93.75%
30 / 32
0.00% covered (danger)
0.00%
0 / 1
20.10
 fromStyle
100.00% covered (success)
100.00%
17 / 17
100.00% covered (success)
100.00%
1 / 1
6
 fromValues
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
42
1<?php
2
3declare(strict_types=1);
4
5namespace Phpdftk\Css\Cascade;
6
7use Phpdftk\Css\Value\Keyword;
8use Phpdftk\Css\Value\Value;
9
10/**
11 * CSS Writing Modes 4 — converts the cascaded `writing-mode` /
12 * `direction` properties into a physical axis mapping so layout
13 * code can ask "which physical axis is the block axis?" without
14 * encoding the horizontal-tb assumption everywhere.
15 *
16 * The four shipped writing modes:
17 *  - `horizontal-tb` — block flows top-to-bottom along Y; inline
18 *    flows left-to-right (`direction: ltr`) or right-to-left
19 *    (`direction: rtl`) along X. The default for HTML.
20 *  - `vertical-rl` — block flows right-to-left along X; inline
21 *    flows top-to-bottom along Y. Used for traditional CJK
22 *    layouts. Inline `direction: rtl` reverses the Y direction.
23 *  - `vertical-lr` — block flows left-to-right along X; inline
24 *    flows top-to-bottom along Y. Mongolian-style.
25 *  - `sideways-rl` / `sideways-lr` — same physical block axis as
26 *    the vertical-* variants, but text glyphs rotate sideways.
27 *    (Distinction matters for the painter; the resolver maps the
28 *    same physical axes.)
29 *
30 * Returns physical-axis tuples so layout can map:
31 *   - inline-start / inline-end / block-start / block-end →
32 *     left / right / top / bottom per the active mode.
33 *   - margin/padding/inset *-block-* and *-inline-* longhands.
34 *   - Block-progression direction (cursor advance per child).
35 *   - Inline-progression direction (per-character / per-glyph
36 *     advance, including bidi flip under `direction: rtl`).
37 */
38final readonly class WritingMode
39{
40    public const HORIZONTAL_TB = 'horizontal-tb';
41    public const VERTICAL_RL = 'vertical-rl';
42    public const VERTICAL_LR = 'vertical-lr';
43    public const SIDEWAYS_RL = 'sideways-rl';
44    public const SIDEWAYS_LR = 'sideways-lr';
45
46    public function __construct(
47        public string $mode = self::HORIZONTAL_TB,
48        public string $direction = 'ltr',
49    ) {}
50
51    /** Block-axis flows along x (vertical modes) or y (horizontal). */
52    public function blockAxis(): string
53    {
54        return $this->isVertical() ? 'x' : 'y';
55    }
56
57    /** Inline-axis flows along the OTHER physical axis. */
58    public function inlineAxis(): string
59    {
60        return $this->isVertical() ? 'y' : 'x';
61    }
62
63    /**
64     * `+1` when the block axis progresses in the positive physical
65     * direction (down for horizontal-tb, right for vertical-lr) or
66     * `-1` for the reverse (right→left for vertical-rl / sideways-rl).
67     */
68    public function blockDirection(): int
69    {
70        return match ($this->mode) {
71            self::VERTICAL_RL, self::SIDEWAYS_RL => -1,
72            default => 1, // horizontal-tb, vertical-lr, sideways-lr
73        };
74    }
75
76    /**
77     * `+1` for ltr inline progression along its physical axis; `-1`
78     * for rtl. For vertical modes the inline axis is y; rtl makes
79     * inlines flow bottom→top.
80     */
81    public function inlineDirection(): int
82    {
83        return $this->direction === 'rtl' ? -1 : 1;
84    }
85
86    public function isVertical(): bool
87    {
88        return $this->mode !== self::HORIZONTAL_TB;
89    }
90
91    public function isHorizontal(): bool
92    {
93        return $this->mode === self::HORIZONTAL_TB;
94    }
95
96    public function isSideways(): bool
97    {
98        return $this->mode === self::SIDEWAYS_RL || $this->mode === self::SIDEWAYS_LR;
99    }
100
101    /**
102     * Resolve `block-start` / `block-end` / `inline-start` /
103     * `inline-end` to one of `top` / `right` / `bottom` / `left`
104     * per the active mode + direction. Used by layout code that
105     * needs to read `margin-block-start` and apply it as a physical
106     * margin edge.
107     */
108    public function physicalEdge(string $logicalEdge): string
109    {
110        return match ([$this->mode, $this->direction, $logicalEdge]) {
111            // horizontal-tb
112            [self::HORIZONTAL_TB, 'ltr', 'block-start'], [self::HORIZONTAL_TB, 'rtl', 'block-start'] => 'top',
113            [self::HORIZONTAL_TB, 'ltr', 'block-end'],   [self::HORIZONTAL_TB, 'rtl', 'block-end']   => 'bottom',
114            [self::HORIZONTAL_TB, 'ltr', 'inline-start']                                              => 'left',
115            [self::HORIZONTAL_TB, 'ltr', 'inline-end']                                                => 'right',
116            [self::HORIZONTAL_TB, 'rtl', 'inline-start']                                              => 'right',
117            [self::HORIZONTAL_TB, 'rtl', 'inline-end']                                                => 'left',
118            // vertical-rl / sideways-rl
119            [self::VERTICAL_RL, 'ltr', 'block-start'], [self::VERTICAL_RL, 'rtl', 'block-start'],
120            [self::SIDEWAYS_RL, 'ltr', 'block-start'], [self::SIDEWAYS_RL, 'rtl', 'block-start'] => 'right',
121            [self::VERTICAL_RL, 'ltr', 'block-end'],   [self::VERTICAL_RL, 'rtl', 'block-end'],
122            [self::SIDEWAYS_RL, 'ltr', 'block-end'],   [self::SIDEWAYS_RL, 'rtl', 'block-end']   => 'left',
123            [self::VERTICAL_RL, 'ltr', 'inline-start'],
124            [self::SIDEWAYS_RL, 'ltr', 'inline-start'] => 'top',
125            [self::VERTICAL_RL, 'ltr', 'inline-end'],
126            [self::SIDEWAYS_RL, 'ltr', 'inline-end']   => 'bottom',
127            [self::VERTICAL_RL, 'rtl', 'inline-start'],
128            [self::SIDEWAYS_RL, 'rtl', 'inline-start'] => 'bottom',
129            [self::VERTICAL_RL, 'rtl', 'inline-end'],
130            [self::SIDEWAYS_RL, 'rtl', 'inline-end']   => 'top',
131            // vertical-lr / sideways-lr
132            [self::VERTICAL_LR, 'ltr', 'block-start'], [self::VERTICAL_LR, 'rtl', 'block-start'],
133            [self::SIDEWAYS_LR, 'ltr', 'block-start'], [self::SIDEWAYS_LR, 'rtl', 'block-start'] => 'left',
134            [self::VERTICAL_LR, 'ltr', 'block-end'],   [self::VERTICAL_LR, 'rtl', 'block-end'],
135            [self::SIDEWAYS_LR, 'ltr', 'block-end'],   [self::SIDEWAYS_LR, 'rtl', 'block-end']   => 'right',
136            [self::VERTICAL_LR, 'ltr', 'inline-start'],
137            [self::SIDEWAYS_LR, 'ltr', 'inline-start'] => 'top',
138            [self::VERTICAL_LR, 'ltr', 'inline-end'],
139            [self::SIDEWAYS_LR, 'ltr', 'inline-end']   => 'bottom',
140            [self::VERTICAL_LR, 'rtl', 'inline-start'],
141            [self::SIDEWAYS_LR, 'rtl', 'inline-start'] => 'bottom',
142            [self::VERTICAL_LR, 'rtl', 'inline-end'],
143            [self::SIDEWAYS_LR, 'rtl', 'inline-end']   => 'top',
144            default => 'top', // unreachable
145        };
146    }
147
148    /**
149     * Resolve the `writing-mode` + `direction` properties from a
150     * CascadedValues into a WritingMode instance. Unknown keywords
151     * fall back to the spec initial values.
152     */
153    public static function fromStyle(CascadedValues $style): self
154    {
155        $modeValue = $style->get('writing-mode');
156        $mode = self::HORIZONTAL_TB;
157        if ($modeValue instanceof Keyword) {
158            $name = strtolower($modeValue->name);
159            if (in_array(
160                $name,
161                [self::HORIZONTAL_TB, self::VERTICAL_RL, self::VERTICAL_LR, self::SIDEWAYS_RL, self::SIDEWAYS_LR],
162                true,
163            )) {
164                $mode = $name;
165            }
166        }
167        $directionValue = $style->get('direction');
168        $direction = 'ltr';
169        if ($directionValue instanceof Keyword) {
170            $name = strtolower($directionValue->name);
171            if ($name === 'rtl' || $name === 'ltr') {
172                $direction = $name;
173            }
174        }
175        return new self($mode, $direction);
176    }
177
178    public static function fromValues(?Value $writingMode, ?Value $direction): self
179    {
180        $mode = self::HORIZONTAL_TB;
181        if ($writingMode instanceof Keyword) {
182            $name = strtolower($writingMode->name);
183            if (in_array(
184                $name,
185                [self::HORIZONTAL_TB, self::VERTICAL_RL, self::VERTICAL_LR, self::SIDEWAYS_RL, self::SIDEWAYS_LR],
186                true,
187            )) {
188                $mode = $name;
189            }
190        }
191        $dir = 'ltr';
192        if ($direction instanceof Keyword) {
193            $name = strtolower($direction->name);
194            if ($name === 'rtl' || $name === 'ltr') {
195                $dir = $name;
196            }
197        }
198        return new self($mode, $dir);
199    }
200}