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