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