xref: /template/strap/ComboStrap/ConditionalLength.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1<?php
2
3namespace ComboStrap;
4
5use ComboStrap\TagAttribute\Align;
6
7/**
8 * Represents a conditional length / value
9 */
10class ConditionalLength
11{
12
13
14    const PERCENTAGE = "%";
15
16    /**
17     * @var string - the length value (may be breakpoint conditional)
18     */
19    private $conditionalLength;
20
21    /**
22     * @var string - the length value without breakpoint
23     */
24    private $length;
25    /**
26     * @var string
27     */
28    private $unitInLength;
29    /**
30     * The number in a length string
31     * @var float
32     */
33    private $numerator;
34    /**
35     * @var string
36     */
37    private $breakpoint;
38    private $defaultBreakpoint = "sm";
39    private $denominator;
40    /**
41     * @var string
42     */
43    private $axis;
44    /**
45     * @var bool
46     */
47    private $isRatio = false;
48
49
50    /**
51     * @throws ExceptionBadArgument
52     */
53    public function __construct($value, $defaultBreakpoint)
54    {
55        $this->conditionalLength = $value;
56        if ($defaultBreakpoint !== null) {
57            $this->defaultBreakpoint = $defaultBreakpoint;
58        }
59
60        /**
61         * Breakpoint Suffix
62         */
63        $this->length = $value;
64        try {
65            $conditionalValue = ConditionalValue::createFrom($value);
66            $this->length = $conditionalValue->getValue();
67            $this->breakpoint = $conditionalValue->getBreakpoint();
68        } catch (ExceptionBadSyntax $e) {
69            // not conditional
70        }
71
72        /**
73         * Axis prefix
74         */
75        $axis = substr($value, 0, 2);
76        switch ($axis) {
77            case "x-":
78                $this->axis = "x";
79                break;
80            case "y-";
81                $this->axis = "y";
82                break;
83        }
84
85        try {
86            $this->parseAsNumberWithOptionalUnit();
87        } catch (ExceptionBadSyntax $e) {
88            try {
89                $this->parseAsRatio();
90            } catch (ExceptionBadSyntax $e) {
91                // string only
92            }
93        }
94
95
96    }
97
98    /**
99     * @throws ExceptionBadArgument
100     */
101    public static function createFromString(string $widthLength, string $defaultBreakpoint = null): ConditionalLength
102    {
103        return new ConditionalLength($widthLength, $defaultBreakpoint);
104    }
105
106    public function getLengthUnit(): ?string
107    {
108        return $this->unitInLength;
109    }
110
111    /**
112     * @throws ExceptionBadArgument
113     */
114    public function toPixelNumber(): int
115    {
116
117        switch ($this->unitInLength) {
118            case "rem":
119                $remValue = ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getRemFontSizeOrDefault();
120                $targetValue = $this->numerator * $remValue;
121                break;
122            case "px":
123            default:
124                $targetValue = $this->numerator;
125        }
126        return DataType::toInteger($targetValue);
127
128    }
129
130    public function getNumerator(): ?float
131    {
132        return $this->numerator;
133    }
134
135    /**
136     * @throws ExceptionBadArgument
137     */
138    public function toColClass(): string
139    {
140
141        $ratio = $this->getRatio();
142        if ($ratio > 1) {
143            throw new ExceptionBadArgument("The length ratio ($ratio) is greater than 1. It should be less than 1 to get a col class.");
144        }
145        $colsNumber = floor(GridTag::GRID_TOTAL_COLUMNS * $this->numerator / $this->denominator);
146        $breakpoint = $this->getBreakpointOrDefault();
147        if ($breakpoint === "xs") {
148            return "col-$colsNumber";
149        }
150        return "col-{$breakpoint}-$colsNumber";
151
152
153    }
154
155    /**
156     * @throws ExceptionBadArgument
157     */
158    public function toRowColsClass(): string
159    {
160
161        if ($this->numerator === null) {
162            if ($this->getLength() === "auto") {
163                if (Bootstrap::getBootStrapMajorVersion() != Bootstrap::BootStrapFiveMajorVersion) {
164                    // row-cols-auto is not in 4.0
165                    PluginUtility::getSnippetManager()->attachCssInternalStyleSheet("row-cols-auto");
166                }
167                return "row-cols-auto";
168            }
169            throw new ExceptionBadArgument("A row col class can be calculated only from a number ({$this}) or from the `auto` value");
170        }
171
172        $colsNumber = intval($this->numerator);
173        $totalColumns = GridTag::GRID_TOTAL_COLUMNS;
174        if ($colsNumber > $totalColumns) {
175            throw new ExceptionBadArgument("A row col class can be calculated only from a number below $totalColumns ({$this}");
176        }
177        $breakpoint = $this->getBreakpointOrDefault();
178        if ($breakpoint === "xs") {
179            return "row-cols-$colsNumber";
180        }
181        return "row-cols-{$breakpoint}-$colsNumber";
182    }
183
184    public
185    function getBreakpoint(): ?string
186    {
187        return $this->breakpoint;
188    }
189
190
191    public
192    function getLength()
193    {
194        return $this->length;
195    }
196
197    public
198    function __toString()
199    {
200        return $this->conditionalLength;
201    }
202
203    /**
204     * For CSS a unit is mandatory (not for HTML or SVG attributes)
205     * @throws ExceptionBadArgument
206     */
207    public
208    function toCssLength()
209    {
210        switch ($this->unitInLength){
211            case "vh":
212            case "wh":
213            case "rem":
214                return $this->length;
215        }
216        /**
217         * A length value may be also `fit-content`
218         * we just check that if there is a number,
219         * we add the pixel
220         */
221        if ($this->numerator !== null) {
222            return $this->toPixelNumber() . "px";
223        } else {
224            if ($this->length === "fit") {
225                return "fit-content";
226            }
227            return $this->length;
228        }
229    }
230
231    public
232    function getBreakpointOrDefault(): string
233    {
234        if ($this->breakpoint !== null) {
235            return $this->breakpoint;
236        }
237        return $this->defaultBreakpoint;
238    }
239
240
241    public
242    function getDenominator(): ?float
243    {
244        return $this->denominator;
245    }
246
247    /**
248     * @throws ExceptionBadSyntax
249     */
250    private
251    function parseAsNumberWithOptionalUnit()
252    {
253        /**
254         * Not a numeric alone
255         * Does the length value has an unit ?
256         */
257        preg_match("/^([0-9.]+)([^0-9]*)$/i", $this->length, $matches, PREG_OFFSET_CAPTURE);
258        if (sizeof($matches) === 0) {
259            throw new ExceptionBadSyntax("Length is not a number with optional unit");
260        }
261        $localNumber = $matches[1][0];
262        try {
263            $this->numerator = DataType::toFloat($localNumber);
264        } catch (ExceptionBadArgument $e) {
265            // should not happen due to the match but yeah
266            throw new ExceptionBadSyntax("The number value ($localNumber) of the length value ($this->length) is not a valid float format.");
267        }
268        $this->denominator = 1;
269
270        $secondMatch = $matches[2][0];
271        if ($secondMatch == "") {
272            return;
273        }
274        $this->unitInLength = $secondMatch;
275        if ($this->unitInLength === self::PERCENTAGE) {
276            $this->denominator = 100;
277        }
278
279    }
280
281    /**
282     * @throws ExceptionBadSyntax
283     */
284    private
285    function parseAsRatio()
286    {
287        preg_match("/^([0-9]+):([0-9]+)$/i", $this->length, $matches, PREG_OFFSET_CAPTURE);
288        if (sizeof($matches) === 0) {
289            throw new ExceptionBadSyntax("Length is not a ratio");
290        }
291        $numerator = $matches[1][0];
292        try {
293            $this->numerator = DataType::toFloat($numerator);
294        } catch (ExceptionBadArgument $e) {
295            // should not happen due to the match but yeah
296            throw new ExceptionBadSyntax("The number value ($numerator) of the length value ($this->length) is not a valid float format.");
297        }
298        $denominator = $matches[2][0];
299        try {
300            $this->denominator = DataType::toFloat($denominator);
301        } catch (ExceptionBadArgument $e) {
302            // should not happen due to the match but yeah
303            throw new ExceptionBadSyntax("The number value ($denominator) of the length value ($this->length) is not a valid float format.");
304        }
305        $this->isRatio = true;
306    }
307
308    /**
309     * @throws ExceptionBadArgument
310     */
311    public
312    function getRatio()
313    {
314        if (!$this->isRatio()) {
315            return null;
316        }
317        if ($this->numerator == null) {
318            return null;
319        }
320        if ($this->denominator == null) {
321            return null;
322        }
323        if ($this->denominator == 0) {
324            throw new ExceptionBadArgument("The denominator of the conditional length ($this) is 0. You can't ask a ratio.");
325        }
326        return $this->numerator / $this->denominator;
327    }
328
329    public function getAxis(): string
330    {
331        return $this->axis;
332    }
333
334    public function getAxisOrDefault(): string
335    {
336        if ($this->axis !== null) {
337            return $this->axis;
338        }
339        return Align::DEFAULT_AXIS;
340    }
341
342    public function isRatio(): bool
343    {
344        if ($this->getLengthUnit() === self::PERCENTAGE) {
345            return true;
346        }
347        return $this->isRatio;
348    }
349
350    /**
351     * @return string - the breakpoint value that should be added into a bootstrap class
352     *
353     * For instance, for ''xs'', you would get ''-xs''
354     * If there is no breakpoint, the empty string is returned
355     */
356    public function getBreakpointForBootstrapClass(): string
357    {
358
359        if ($this->breakpoint !== null) {
360            if($this->breakpoint==="xs"){
361                return "";
362            }
363            return "-{$this->breakpoint}";
364        } else {
365            return "";
366        }
367    }
368
369
370}
371