1<?php 2/** 3 * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 4 * 5 * This source code is licensed under the GPL license found in the 6 * COPYING file in the root directory of this source tree. 7 * 8 * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 9 * @author ComboStrap <support@combostrap.com> 10 * 11 */ 12 13namespace ComboStrap; 14 15use ComboStrap\Web\Url; 16 17class Bootstrap 18{ 19 const DEFAULT_STYLESHEET_NAME = "bootstrap"; 20 const TAG = self::CANONICAL; 21 public const DEFAULT_MAJOR = 5; 22 public const VERSION_501 = self::DEFAULT_MAJOR . ".0.1"; 23 public const DEFAULT_BOOTSTRAP_4_VERSION = "4.5.0"; 24 public const DEFAULT_BOOTSTRAP_5_VERSION = self::VERSION_501; 25 public const DEFAULT_BOOTSTRAP_STYLESHEET_4_VERSION = self::DEFAULT_BOOTSTRAP_4_VERSION . " - " . self::DEFAULT_STYLESHEET_NAME; 26 public const DEFAULT_BOOTSTRAP_STYLESHEET_5_VERSION = self::DEFAULT_BOOTSTRAP_5_VERSION . " - " . self::DEFAULT_STYLESHEET_NAME; 27 28 private Snippet $jquerySnippet; 29 private Snippet $jsSnippet; 30 private Snippet $popperSnippet; 31 private Snippet $cssSnippet; 32 33 /** 34 * @param string $qualifiedVersion - the bootstrap version separated by the stylesheet 35 */ 36 public function __construct(string $qualifiedVersion) 37 { 38 $bootstrapStyleSheetArray = explode(Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR, $qualifiedVersion); 39 $this->version = $bootstrapStyleSheetArray[0]; 40 /** 41 * In case the input is just the major version 42 */ 43 switch ($this->version) { 44 case "4": 45 $this->version = self::DEFAULT_BOOTSTRAP_4_VERSION; 46 break; 47 case "5": 48 $this->version = self::DEFAULT_BOOTSTRAP_5_VERSION; 49 break; 50 } 51 52 /** 53 * Stylesheet 54 */ 55 if (isset($bootstrapStyleSheetArray[1])) { 56 $this->styleSheetName = $bootstrapStyleSheetArray[1]; 57 } else { 58 $this->styleSheetName = self::DEFAULT_STYLESHEET_NAME; 59 } 60 61 $this->build(); 62 63 } 64 65 66 const BootStrapFiveMajorVersion = 5; 67 const BootStrapFourMajorVersion = 4; 68 const CANONICAL = "bootstrap"; 69 /** 70 * Stylesheet and Boostrap should have the same version 71 * This conf is a mix between the version and the stylesheet 72 * 73 * majorVersion.0.0 - stylesheetname 74 */ 75 public const CONF_BOOTSTRAP_VERSION_STYLESHEET = "bootstrapVersionStylesheet"; 76 /** 77 * The separator in {@link Bootstrap::CONF_BOOTSTRAP_VERSION_STYLESHEET} 78 */ 79 public const BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR = " - "; 80 public const DEFAULT_BOOTSTRAP_VERSION_STYLESHEET = "5.0.1" . Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . "bootstrap"; 81 82 /** 83 * @var mixed|string 84 */ 85 private $version; 86 /** 87 * @var string - the stylesheet name 88 */ 89 private $styleSheetName; 90 91 public static function getDataNamespace() 92 { 93 $dataToggleNamespace = ""; 94 if (self::getBootStrapMajorVersion() == self::BootStrapFiveMajorVersion) { 95 $dataToggleNamespace = "-bs"; 96 } 97 return $dataToggleNamespace; 98 } 99 100 public function getMajorVersion(): int 101 { 102 $bootstrapMajorVersion = $this->getVersion()[0]; 103 try { 104 return DataType::toInteger($bootstrapMajorVersion); 105 } catch (ExceptionBadArgument $e) { 106 LogUtility::internalError("The bootstrap major version ($bootstrapMajorVersion) is not an integer, default taken"); 107 return self::DEFAULT_MAJOR; 108 } 109 110 } 111 112 /** 113 * Utility function that returns the major version 114 * because this is really common in code 115 * @return int - the major version 116 */ 117 public static function getBootStrapMajorVersion(): int 118 { 119 return Bootstrap::getFromContext()->getMajorVersion(); 120 } 121 122 123 public function getVersion(): string 124 { 125 return $this->version; 126 } 127 128 public static function createFromQualifiedVersion(string $boostrapVersion): Bootstrap 129 { 130 return new Bootstrap($boostrapVersion); 131 } 132 133 134 public function getStyleSheetName(): string 135 { 136 return $this->styleSheetName; 137 } 138 139 /** 140 * 141 * @return array - an array of stylesheet tag 142 */ 143 public static function getStyleSheetMetas(): array 144 { 145 146 /** 147 * Standard stylesheet 148 */ 149 $stylesheetsFile = WikiPath::createComboResource(':library:bootstrap:bootstrapStylesheet.json'); 150 try { 151 $styleSheets = Json::createFromPath($stylesheetsFile)->toArray(); 152 } catch (ExceptionNotFound|ExceptionBadSyntax $e) { 153 LogUtility::internalError("An error has occurred reading the file ($stylesheetsFile). Error:{$e->getMessage()}", self::CANONICAL); 154 return []; 155 } 156 157 /** 158 * User defined stylesheet 159 */ 160 $localStyleSheetsFile = WikiPath::createComboResource(':library:bootstrap:bootstrapLocal.json'); 161 try { 162 $localStyleSheets = Json::createFromPath($localStyleSheetsFile)->toArray(); 163 foreach ($localStyleSheets as $bootstrapVersion => &$localStyleSheetData) { 164 $actualStyleSheet = $styleSheets[$bootstrapVersion]; 165 if (isset($actualStyleSheet)) { 166 $styleSheets[$bootstrapVersion] = array_merge($actualStyleSheet, $localStyleSheetData); 167 } else { 168 $styleSheets[$bootstrapVersion] = $localStyleSheetData; 169 } 170 } 171 } catch (ExceptionBadSyntax|ExceptionNotFound $e) { 172 // user file does not exists and that's okay 173 } 174 return $styleSheets; 175 176 177 } 178 179 180 /** 181 * @return array - A list of all available stylesheets 182 * This function is used to build the configuration as a list of files 183 */ 184 public static function getQualifiedVersions(): array 185 { 186 $cssVersionsMetas = Bootstrap::getStyleSheetMetas(); 187 $listVersionStylesheetMeta = array(); 188 foreach ($cssVersionsMetas as $bootstrapVersion => $cssVersionMeta) { 189 foreach ($cssVersionMeta as $fileName => $values) { 190 $listVersionStylesheetMeta[] = $bootstrapVersion . Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . $fileName; 191 } 192 } 193 return $listVersionStylesheetMeta; 194 } 195 196 197 /** 198 * @return Bootstrap 199 */ 200 public static function getFromContext(): Bootstrap 201 { 202 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 203 try { 204 return $executionContext->getRuntimeObject(self::CANONICAL); 205 } catch (ExceptionNotFound $e) { 206 $bootstrapStyleSheetVersion = ExecutionContext::getActualOrCreateFromEnv() 207 ->getConfValue(Bootstrap::CONF_BOOTSTRAP_VERSION_STYLESHEET, Bootstrap::DEFAULT_BOOTSTRAP_VERSION_STYLESHEET); 208 $bootstrap = new Bootstrap($bootstrapStyleSheetVersion); 209 $executionContext->setRuntimeObject(self::CANONICAL, $bootstrap); 210 return $bootstrap; 211 } 212 213 } 214 215 216 /** 217 * @throws ExceptionNotFound 218 */ 219 public function getCssSnippet(): Snippet 220 { 221 if (isset($this->cssSnippet)) { 222 return $this->cssSnippet; 223 } 224 throw new ExceptionNotFound("No css snippet"); 225 } 226 227 /** 228 * @return Snippet[] the js snippets in order 229 */ 230 public function getJsSnippets(): array 231 { 232 233 /** 234 * The javascript snippet order is important 235 */ 236 $snippets = []; 237 try { 238 $snippets[] = $this->getJquerySnippet(); 239 } catch (ExceptionNotFound $e) { 240 // error already send at build time 241 // or just not present 242 } 243 try { 244 $snippets[] = $this->getPopperSnippet(); 245 } catch (ExceptionNotFound $e) { 246 // error already send at build time 247 } 248 try { 249 $snippets[] = $this->getBootstrapJsSnippet(); 250 } catch (ExceptionNotFound $e) { 251 // error already send at build time 252 } 253 return $snippets; 254 255 } 256 257 /** 258 * 259 */ 260 private function build(): void 261 { 262 263 264 $version = $this->getVersion(); 265 266 // Javascript 267 $bootstrapJsonFile = WikiPath::createComboResource(Snippet::LIBRARY_BASE . ":bootstrap:bootstrapJavascript.json"); 268 try { 269 $bootstrapJsonMetas = Json::createFromPath($bootstrapJsonFile)->toArray(); 270 } catch (ExceptionBadSyntax|ExceptionNotFound $e) { 271 // should not happen, no need to advertise it 272 throw new ExceptionRuntimeInternal("Unable to read the file {$bootstrapJsonFile} as json.", self::CANONICAL, 1, $e); 273 } 274 if (!isset($bootstrapJsonMetas[$version])) { 275 throw new ExceptionRuntimeInternal("The bootstrap version ($version) could not be found in the file $bootstrapJsonFile"); 276 } 277 $bootstrapMetas = $bootstrapJsonMetas[$version]; 278 279 // Css 280 $bootstrapMetas["stylesheet"] = $this->getStyleSheetMeta(); 281 282 283 foreach ($bootstrapMetas as $key => $script) { 284 $fileNameWithExtension = $script["file"]; 285 $file = LocalPath::createFromPathString($fileNameWithExtension); 286 287 $path = WikiPath::createComboResource(":library:bootstrap:$version:$fileNameWithExtension"); 288 $snippet = Snippet::createSnippet($path) 289 ->setComponentId(self::TAG); 290 $url = $script["url"] ?? null; 291 if (!empty($url)) { 292 try { 293 $url = Url::createFromString($url); 294 $snippet->setRemoteUrl($url); 295 if (isset($script['integrity'])) { 296 $snippet->setIntegrity($script['integrity']); 297 } 298 } catch (ExceptionBadArgument|ExceptionBadSyntax $e) { 299 LogUtility::internalError("The url ($url) for the bootstrap metadata ($fileNameWithExtension) from the bootstrap dictionary is not valid. Error:{$e->getMessage()}", self::CANONICAL); 300 } 301 } 302 303 try { 304 $extension = $file->getExtension(); 305 } catch (ExceptionNotFound $e) { 306 LogUtility::internalError("No extension was found on the file metadata ($fileNameWithExtension) from the bootstrap dictionary", self::CANONICAL); 307 continue; 308 } 309 switch ($extension) { 310 case Snippet::EXTENSION_JS: 311 $snippet->setCritical(false); 312 switch ($key) { 313 case "jquery": 314 $this->jquerySnippet = $snippet; 315 break; 316 case "js": 317 $this->jsSnippet = $snippet; 318 break; 319 case "popper": 320 $this->popperSnippet = $snippet; 321 break; 322 default: 323 LogUtility::internalError("The snippet key ($key) is unknown for bootstrap", self::CANONICAL); 324 break; 325 } 326 break; 327 case Snippet::EXTENSION_CSS: 328 switch ($key) { 329 case "stylesheet": 330 $this->cssSnippet = $snippet; 331 break; 332 default: 333 LogUtility::internalError("The snippet key ($key) is unknown for bootstrap"); 334 break; 335 } 336 } 337 } 338 339 340 } 341 342 public function getSnippets(): array 343 { 344 345 $snippets = []; 346 try { 347 $snippets[] = $this->getCssSnippet(); 348 } catch (ExceptionNotFound $e) { 349 // error already send at build time 350 } 351 352 /** 353 * The javascript snippet 354 */ 355 return array_merge($snippets, $this->getJsSnippets()); 356 } 357 358 /** 359 * @throws ExceptionNotFound 360 */ 361 public function getPopperSnippet(): Snippet 362 { 363 if (isset($this->popperSnippet)) { 364 return $this->popperSnippet; 365 } 366 throw new ExceptionNotFound("No popper snippet"); 367 } 368 369 /** 370 * @throws ExceptionNotFound 371 */ 372 public function getBootstrapJsSnippet(): Snippet 373 { 374 if (isset($this->jsSnippet)) { 375 return $this->jsSnippet; 376 } 377 throw new ExceptionNotFound("No js snippet"); 378 } 379 380 /** 381 * @throws ExceptionNotFound 382 */ 383 private function getJquerySnippet(): Snippet 384 { 385 if (isset($this->jquerySnippet)) { 386 return $this->jquerySnippet; 387 } 388 throw new ExceptionNotFound("No jquery snippet"); 389 } 390 391 /** 392 * @return array - the stylesheet meta (file, url, ...) for the version 393 */ 394 private function getStyleSheetMeta(): array 395 { 396 397 $styleSheets = self::getStyleSheetMetas(); 398 399 $version = $this->getVersion(); 400 if (!isset($styleSheets[$version])) { 401 LogUtility::internalError("The bootstrap version ($version) could not be found"); 402 return []; 403 } 404 $styleSheetsForVersion = $styleSheets[$version]; 405 406 $styleSheetName = $this->getStyleSheetName(); 407 if (!isset($styleSheetsForVersion[$styleSheetName])) { 408 LogUtility::internalError("The bootstrap stylesheet ($styleSheetName) could not be found for the version ($version) in the distribution or custom configuration files"); 409 return []; 410 } 411 $styleSheetForVersionAndName = $styleSheetsForVersion[$styleSheetName]; 412 413 /** 414 * Select Rtl or Ltr 415 * Stylesheet name may have another level 416 * with direction property of the language 417 * 418 * Bootstrap needs another stylesheet 419 * See https://getbootstrap.com/docs/5.0/getting-started/rtl/ 420 */ 421 try { 422 $direction = Lang::createFromRequestedMarkup()->getDirection(); 423 } catch (ExceptionNotFound $e) { 424 $direction = Site::getLangObject()->getDirection(); 425 } 426 if (isset($styleSheetForVersionAndName[$direction])) { 427 return $styleSheetForVersionAndName[$direction]; 428 } 429 if (!isset($styleSheetForVersionAndName['file'])) { 430 LogUtility::internalError('The file stylesheet attribute is unknown (' . DataType::toString($styleSheetForVersionAndName) . ')'); 431 } 432 return $styleSheetForVersionAndName; 433 } 434} 435