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