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