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 15 16use ComboStrap\Web\Url; 17use RuntimeException; 18 19/** 20 * @package ComboStrap 21 * 22 * Public interface of {@link Snippet} 23 * 24 * All plugin/component should use the attach functions to add a internal or external 25 * stylesheet/javascript to a slot or request scoped 26 * 27 * Note: 28 * All function with the suffix 29 * * `ForSlot` are snippets for a bar (ie page, sidebar, ...) - cached 30 * * `ForRequests` are snippets added for the HTTP request - not cached. Example of request component: message, anchor 31 * 32 * 33 * Minification: 34 * Wrapper: https://packagist.org/packages/jalle19/php-yui-compressor 35 * Require Yui compressor: https://packagist.org/packages/nervo/yuicompressor 36 * sudo apt-get install default-jre 37 * 38 */ 39class SnippetSystem 40{ 41 42 43 const CANONICAL = "snippet-system"; 44 45 46 /** 47 * @return SnippetSystem - the global reference 48 * that is set for every run at the end of this file 49 * TODO: migrate the attach function to {@link Snippet} 50 * because Snippet has already a global variable {@link Snippet::getOrCreateFromComponentId()} 51 */ 52 public static function getFromContext(): SnippetSystem 53 { 54 55 $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 56 try { 57 return $executionContext->getRuntimeObject(self::CANONICAL); 58 } catch (ExceptionNotFound $e) { 59 $snippetSystem = new SnippetSystem(); 60 $executionContext->setRuntimeObject(self::CANONICAL, $snippetSystem); 61 return $snippetSystem; 62 } 63 64 } 65 66 /** 67 * @param Snippet[] $snippets 68 * @return string 69 */ 70 public static function toHtmlFromSnippetArray(array $snippets): string 71 { 72 $xhtmlContent = ""; 73 foreach ($snippets as $snippet) { 74 75 if ($snippet->hasHtmlOutputAlreadyOccurred()) { 76 continue; 77 } 78 79 $xhtmlContent .= $snippet->toXhtml(); 80 81 82 } 83 return $xhtmlContent; 84 } 85 86 87 /** 88 * Returns all snippets (request and slot scoped) 89 * 90 * @return Snippet[] of node type and an array of array of html attributes 91 */ 92 public function getAllSnippets(): array 93 { 94 return Snippet::getSnippets(); 95 } 96 97 /** 98 * @return Snippet[] - the slot snippets (not the request snippet) 99 */ 100 private function getSlotSnippets(): array 101 { 102 $snippets = Snippet::getSnippets(); 103 $slotSnippets = []; 104 foreach ($snippets as $snippet) { 105 if ($snippet->hasSlot(Snippet::REQUEST_SCOPE)) { 106 continue; 107 } 108 $slotSnippets[] = $snippet; 109 } 110 return $slotSnippets; 111 } 112 113 114 public static 115 function toJsonArrayFromSlotSnippets($snippetsForSlot): array 116 { 117 118 $jsonSnippets = []; 119 foreach ($snippetsForSlot as $snippet) { 120 $jsonSnippets[] = $snippet->toJsonArray(); 121 } 122 return $jsonSnippets; 123 124 } 125 126 /** 127 * @param array $array 128 * @param string $slot 129 * @return null|Snippet[] 130 * @throws ExceptionCompile 131 */ 132 public 133 function getSlotSnippetsFromJsonArray(array $array, string $slot): ?array 134 { 135 $snippets = null; 136 foreach ($array as $element) { 137 $snippets[] = Snippet::createFromJson($element) 138 ->addElement($slot); 139 } 140 return $snippets; 141 } 142 143 144 /** 145 * @param $componentId 146 * @param string|null $script - the css snippet to add, otherwise it takes the file 147 * @return Snippet a snippet not in a slot 148 * 149 * If you need to split the css by type of action, see {@link \action_plugin_combo_docss::handleCssForDoAction()} 150 */ 151 public 152 function &attachCssInternalStyleSheet($componentId, string $script = null): Snippet 153 { 154 $snippet = Snippet::getOrCreateFromComponentId($componentId, Snippet::EXTENSION_CSS); 155 if ($script !== null) { 156 $snippet->setInlineContent($script); 157 } 158 return $snippet; 159 } 160 161 162 /** 163 * @param $componentId 164 * @param string|null $script 165 * @return Snippet a snippet in a slot 166 */ 167 public function attachJavascriptFromComponentId($componentId, string $script = null): Snippet 168 { 169 $snippet = Snippet::getOrCreateFromComponentId($componentId, Snippet::EXTENSION_JS); 170 if ($script !== null) { 171 try { 172 $content = "{$snippet->getInternalDynamicContent()} $script"; 173 } catch (ExceptionNotFound $e) { 174 $content = $script; 175 } 176 $snippet->setInlineContent($content); 177 } 178 return $snippet; 179 } 180 181 182 public 183 function attachInternalJavascriptFromPathForRequest($componentId, Path $path): Snippet 184 { 185 return Snippet::getOrCreateFromContext($path) 186 ->addElement(Snippet::REQUEST_SCOPE) 187 ->setComponentId($componentId); 188 } 189 190 191 /** 192 * @param $componentId 193 * @return Snippet[] 194 */ 195 public function getSnippetsForComponent($componentId): array 196 { 197 $snippets = []; 198 foreach ($this->getSnippets() as $snippet) { 199 try { 200 if ($snippet->getComponentId() === $componentId) { 201 $snippets[] = $snippet; 202 } 203 } catch (ExceptionNotFound $e) { 204 // 205 } 206 } 207 return $snippets; 208 } 209 210 /** 211 * Utility function used in test 212 * or to show how to test if snippets are present 213 * @param $componentId 214 * @return bool 215 */ 216 public function hasSnippetsForComponent($componentId): bool 217 { 218 return count($this->getSnippetsForComponent($componentId)) > 0; 219 } 220 221 /** 222 * @param $componentId 223 * @param $type 224 * @return Snippet 225 * @deprecated - the slot is now added automatically at creation time via the context system 226 */ 227 private 228 function attachSnippetFromRequest($componentId, $type): Snippet 229 { 230 return Snippet::getOrCreateFromComponentId($componentId, $type) 231 ->addElement(Snippet::REQUEST_SCOPE); 232 } 233 234 235 /** 236 * @param string $snippetId 237 * @param string $pathFromComboDrive 238 * @param string|null $integrity 239 * @return Snippet 240 */ 241 public 242 function attachJavascriptComboResourceForSlot(string $snippetId, string $pathFromComboDrive, string $integrity = null): Snippet 243 { 244 245 $dokuPath = WikiPath::createComboResource($pathFromComboDrive); 246 return Snippet::getOrCreateFromContext($dokuPath) 247 ->setComponentId($snippetId) 248 ->setIntegrity($integrity); 249 250 } 251 252 /** 253 * Add a local javascript script as tag 254 * (ie same as {@link SnippetSystem::attachRemoteJavascriptLibrary()}) 255 * but for local resource combo file (library) 256 * 257 * For instance: 258 * * library:combo:combo.js 259 * * for a file located at dokuwiki_home\lib\plugins\combo\resources\library\combo\combo.js 260 * @return Snippet 261 */ 262 public 263 function attachJavascriptComboLibrary(): Snippet 264 { 265 266 $wikiPath = ":library:combo:combo.min.js"; 267 $componentId = "combo"; 268 return $this->attachSnippetFromComboResourceDrive($wikiPath, $componentId); 269 270 } 271 272 public function attachSnippetFromComboResourceDrive(string $pathFromComboDrive, string $componentId): Snippet 273 { 274 275 $dokuPath = WikiPath::createComboResource($pathFromComboDrive); 276 return Snippet::getOrCreateFromContext($dokuPath) 277 ->setComponentId($componentId); 278 279 } 280 281 /** 282 * @throws ExceptionBadSyntax - bad url 283 * @throws ExceptionBadArgument - the url needs to have a file name 284 */ 285 public 286 function attachRemoteJavascriptLibrary(string $componentId, string $url, string $integrity = null): Snippet 287 { 288 $url = Url::createFromString($url); 289 return Snippet::getOrCreateFromRemoteUrl($url) 290 ->setIntegrity($integrity) 291 ->setComponentId($componentId); 292 } 293 294 /** 295 * Same component as attachRemoteJavascriptLibrary but without error 296 * as the url is a code literal (ie written in the code) 297 * @param string $componentId 298 * @param string $url 299 * @param string|null $integrity 300 * @return Snippet 301 */ 302 public 303 function attachRemoteJavascriptLibraryFromLiteral(string $componentId, string $url, string $integrity = null): Snippet 304 { 305 try { 306 $url = Url::createFromString($url); 307 return Snippet::getOrCreateFromRemoteUrl($url) 308 ->setIntegrity($integrity) 309 ->setComponentId($componentId); 310 } catch (ExceptionBadArgument|ExceptionBadSyntax $e) { 311 throw new RuntimeException("Bad URL (" . $e->getMessage() .")", $e); 312 } 313 314 } 315 316 /** 317 * @param string $componentId - the component id attached to this URL 318 * @param string $url - the external url (The URL should have a file name as last name in the path) 319 * @param string|null $integrity - the file integrity 320 * @return Snippet 321 * @throws ExceptionBadArgument 322 * @throws ExceptionBadSyntax 323 * @throws ExceptionNotFound 324 */ 325 public 326 function attachRemoteCssStyleSheet(string $componentId, string $url, string $integrity = null): Snippet 327 { 328 $url = Url::createFromString($url); 329 330 return Snippet::getOrCreateFromRemoteUrl($url) 331 ->setIntegrity($integrity) 332 ->setRemoteUrl($url) 333 ->setComponentId($componentId); 334 } 335 336 /** 337 * The same as attachRemoteCssStyleSheet but without any exception 338 * as the URL is written in the code, it's to the dev to not messed up 339 * @param string $componentId 340 * @param string $url 341 * @param string|null $integrity 342 * @return Snippet 343 */ 344 public 345 function attachRemoteCssStyleSheetFromLiteral(string $componentId, string $url, string $integrity = null): Snippet 346 { 347 try { 348 $url = Url::createFromString($url); 349 return Snippet::getOrCreateFromRemoteUrl($url) 350 ->setIntegrity($integrity) 351 ->setRemoteUrl($url) 352 ->setComponentId($componentId); 353 } catch (ExceptionBadArgument|ExceptionBadSyntax $e) { 354 throw new RuntimeException("Bad URL (" . $e->getMessage() .")", $e); 355 } 356 } 357 358 359 /** 360 * @return Snippet[] 361 */ 362 public 363 function getSnippets(): array 364 { 365 return Snippet::getSnippets(); 366 } 367 368 private 369 function getRequestSnippets(): array 370 { 371 $snippets = Snippet::getSnippets(); 372 $slotSnippets = []; 373 foreach ($snippets as $snippet) { 374 if (!$snippet->hasSlot(Snippet::REQUEST_SCOPE)) { 375 continue; 376 } 377 $slotSnippets[] = $snippet; 378 } 379 return $slotSnippets; 380 } 381 382 /** 383 * Output the snippet in HTML format 384 * The scope is mandatory: 385 * * {@link Snippet::ALL_SCOPE} 386 * * {@link Snippet::REQUEST_SCOPE} 387 * * {@link Snippet::SLOT_SCOPE} 388 * 389 * @return string - html string 390 */ 391 private 392 function toHtml($scope): string 393 { 394 switch ($scope) { 395 case Snippet::SLOT_SCOPE: 396 $snippets = $this->getSlotSnippets(); 397 break; 398 case Snippet::REQUEST_SCOPE: 399 $snippets = $this->getRequestSnippets(); 400 break; 401 default: 402 case Snippet::ALL_SCOPE: 403 $snippets = $this->getAllSnippets(); 404 if ($scope !== Snippet::ALL_SCOPE) { 405 LogUtility::internalError("Scope ($scope) is unknown, we have defaulted to all"); 406 } 407 break; 408 } 409 410 411 return self::toHtmlFromSnippetArray($snippets); 412 } 413 414 public 415 function toHtmlForAllSnippets(): string 416 { 417 return $this->toHtml(Snippet::ALL_SCOPE); 418 } 419 420 public 421 function toHtmlForSlotSnippets(): string 422 { 423 return $this->toHtml(Snippet::SLOT_SCOPE); 424 } 425 426 public function addPopoverLibrary(): SnippetSystem 427 { 428 $this->attachJavascriptFromComponentId(Snippet::COMBO_POPOVER); 429 $this->attachCssInternalStylesheet(Snippet::COMBO_POPOVER); 430 return $this; 431 } 432 433 /** 434 * @param $slot 435 * @return Snippet[] 436 */ 437 public function getSnippetsForSlot($slot): array 438 { 439 $snippets = Snippet::getSnippets(); 440 return array_filter($snippets, 441 function ($s) use ($slot) { 442 return $s->hasSlot($slot); 443 }); 444 } 445 446 447} 448