137748cd8SNickeau<?php 237748cd8SNickeau/** 337748cd8SNickeau * Copyright (c) 2021. ComboStrap, Inc. and its affiliates. All Rights Reserved. 437748cd8SNickeau * 537748cd8SNickeau * This source code is licensed under the GPL license found in the 637748cd8SNickeau * COPYING file in the root directory of this source tree. 737748cd8SNickeau * 837748cd8SNickeau * @license GPL 3 (https://www.gnu.org/licenses/gpl-3.0.en.html) 937748cd8SNickeau * @author ComboStrap <support@combostrap.com> 1037748cd8SNickeau * 1137748cd8SNickeau */ 1237748cd8SNickeau 1337748cd8SNickeaunamespace ComboStrap; 1437748cd8SNickeau 1537748cd8SNickeau 16*04fd306cSNickeauuse action_plugin_combo_docustom; 17*04fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 18*04fd306cSNickeauuse ComboStrap\Web\Url; 19c3437056SNickeauuse JsonSerializable; 20*04fd306cSNickeauuse splitbrain\slika\Exception; 21c3437056SNickeau 224cadd4f8SNickeau/** 234cadd4f8SNickeau * Class Snippet 244cadd4f8SNickeau * @package ComboStrap 254cadd4f8SNickeau * A HTML tag: 264cadd4f8SNickeau * * CSS: link for href or style with content 274cadd4f8SNickeau * * Javascript: script 284cadd4f8SNickeau * 294cadd4f8SNickeau * A component to manage the extra HTML that 304cadd4f8SNickeau * comes from components and that should come in the head HTML node 314cadd4f8SNickeau * 32*04fd306cSNickeau * A snippet identifier is a {@link Snippet::getLocalUrl() local file} 33*04fd306cSNickeau * * if there is content defined, it will be an {@link Snippet::hasInlineContent() inline} 34*04fd306cSNickeau * * if not, it will be the local file with the {@link Snippet::getLocalUrl()} 35*04fd306cSNickeau * * if not found or if the usage of the cdn is required, the {@link Snippet::getRemoteUrl() url} is used 36*04fd306cSNickeau * 374cadd4f8SNickeau */ 38c3437056SNickeauclass Snippet implements JsonSerializable 3937748cd8SNickeau{ 4037748cd8SNickeau /** 4137748cd8SNickeau * The head in css format 4237748cd8SNickeau * We need to add the style node 4337748cd8SNickeau */ 444cadd4f8SNickeau const EXTENSION_CSS = "css"; 4537748cd8SNickeau /** 4637748cd8SNickeau * The head in javascript 4737748cd8SNickeau * We need to wrap it in a script node 4837748cd8SNickeau */ 494cadd4f8SNickeau const EXTENSION_JS = "js"; 504cadd4f8SNickeau 5137748cd8SNickeau /** 524cadd4f8SNickeau * Properties of the JSON array 5337748cd8SNickeau */ 544cadd4f8SNickeau const JSON_COMPONENT_PROPERTY = "component"; // mandatory 55*04fd306cSNickeau const JSON_URI_PROPERTY = "uri"; // internal uri 56*04fd306cSNickeau const JSON_URL_PROPERTY = "url"; // external url 57c3437056SNickeau const JSON_CRITICAL_PROPERTY = "critical"; 584cadd4f8SNickeau const JSON_ASYNC_PROPERTY = "async"; 59c3437056SNickeau const JSON_CONTENT_PROPERTY = "content"; 604cadd4f8SNickeau const JSON_INTEGRITY_PROPERTY = "integrity"; 614cadd4f8SNickeau const JSON_HTML_ATTRIBUTES_PROPERTY = "attributes"; 624cadd4f8SNickeau 634cadd4f8SNickeau /** 64*04fd306cSNickeau * Not all snippet comes from a component markup 65*04fd306cSNickeau * * a menu item may want to add a snippet on a dynamic page 66*04fd306cSNickeau * * a snippet may be added just from the head html meta (for anaytics purpose) 67*04fd306cSNickeau * * the global css variables 68*04fd306cSNickeau * TODO: it should be migrated to the {@link TemplateForWebPage}, ie the request scope is the template scope 69*04fd306cSNickeau * has, these is this object that creates pages 704cadd4f8SNickeau */ 71*04fd306cSNickeau const REQUEST_SCOPE = "request"; 72*04fd306cSNickeau 73*04fd306cSNickeau const SLOT_SCOPE = "slot"; 74*04fd306cSNickeau const ALL_SCOPE = "all"; 75*04fd306cSNickeau public const COMBO_POPOVER = "combo-popover"; 76*04fd306cSNickeau const CANONICAL = "snippet"; 77*04fd306cSNickeau public const STYLE_TAG = "style"; 78*04fd306cSNickeau public const SCRIPT_TAG = "script"; 79*04fd306cSNickeau public const LINK_TAG = "link"; 80*04fd306cSNickeau /** 81*04fd306cSNickeau * Use CDN for local stored library 82*04fd306cSNickeau */ 83*04fd306cSNickeau public const CONF_USE_CDN = "useCDN"; 844cadd4f8SNickeau 854cadd4f8SNickeau /** 86*04fd306cSNickeau * Where to find the file in the combo resources 87*04fd306cSNickeau * if any in wiki path syntax 884cadd4f8SNickeau */ 89*04fd306cSNickeau public const LIBRARY_BASE = ':library'; // external script, combo library 90*04fd306cSNickeau public const SNIPPET_BASE = ":snippet"; // quick internal snippet 91*04fd306cSNickeau public const CONF_USE_CDN_DEFAULT = 1; 924cadd4f8SNickeau 93*04fd306cSNickeau /** 94*04fd306cSNickeau * With a raw format, we do nothing 95*04fd306cSNickeau * We take it without any questions 96*04fd306cSNickeau */ 97*04fd306cSNickeau const RAW_FORMAT = "raw"; 98*04fd306cSNickeau /** 99*04fd306cSNickeau * With a iife, if the javascript snippet is not critical 100*04fd306cSNickeau * It will be wrapped to execute after page load 101*04fd306cSNickeau */ 102*04fd306cSNickeau const IIFE_FORMAT = "iife"; 103*04fd306cSNickeau /** 104*04fd306cSNickeau * Javascript es module 105*04fd306cSNickeau */ 106*04fd306cSNickeau const ES_FORMAT = "es"; 107*04fd306cSNickeau /** 108*04fd306cSNickeau * Umd module 109*04fd306cSNickeau */ 110*04fd306cSNickeau const UMD_FORMAT = "umd"; 111*04fd306cSNickeau const JSON_FORMAT_PROPERTY = "format"; 112*04fd306cSNickeau const DEFAULT_FORMAT = self::RAW_FORMAT; 1134cadd4f8SNickeau 11437748cd8SNickeau 11537748cd8SNickeau /** 11637748cd8SNickeau * @var bool 11737748cd8SNickeau */ 118*04fd306cSNickeau private bool $critical; 11937748cd8SNickeau 12037748cd8SNickeau /** 12137748cd8SNickeau * @var string the text script / style (may be null if it's an external resources) 12237748cd8SNickeau */ 123*04fd306cSNickeau private string $inlineContent; 1244cadd4f8SNickeau 125*04fd306cSNickeau private Url $remoteUrl; 1264cadd4f8SNickeau 127*04fd306cSNickeau private string $integrity; 128*04fd306cSNickeau 129*04fd306cSNickeau private array $htmlAttributes; 130*04fd306cSNickeau 1314cadd4f8SNickeau 1324cadd4f8SNickeau /** 1334cadd4f8SNickeau * @var array - the slots that needs this snippet (as key to get only one snippet by scope) 134*04fd306cSNickeau * A special slot exists for {@link Snippet::REQUEST_SCOPE} 1354cadd4f8SNickeau * where a snippet is for the whole requested page 1364cadd4f8SNickeau * 1374cadd4f8SNickeau * It's also used in the cache because not all bars 1384cadd4f8SNickeau * may render at the same time due to the other been cached. 1394cadd4f8SNickeau * 1404cadd4f8SNickeau * There is two scope: 1414cadd4f8SNickeau * * a slot - cached along the HTML 142*04fd306cSNickeau * * or {@link Snippet::REQUEST_SCOPE} - never cached 1434cadd4f8SNickeau */ 144*04fd306cSNickeau private $elements; 145*04fd306cSNickeau 1464cadd4f8SNickeau /** 1474cadd4f8SNickeau * @var bool run as soon as possible 1484cadd4f8SNickeau */ 149*04fd306cSNickeau private bool $async; 150*04fd306cSNickeau private Path $path; 151*04fd306cSNickeau private string $componentId; 15237748cd8SNickeau 15337748cd8SNickeau /** 154*04fd306cSNickeau * @var bool a property to track if a snippet has already been asked in a html output 155*04fd306cSNickeau * (ie with a to function such as {@link Snippet::toTagAttributes()} or {@link Snippet::toDokuWikiArray()} 156*04fd306cSNickeau * We use it to not delete the state of {@link ExecutionContext} in order to check the created snippet 157*04fd306cSNickeau * during an execution 158*04fd306cSNickeau * 159*04fd306cSNickeau * The positive side effect is that even if the snippet is used in multiple markup for a page, 160*04fd306cSNickeau * It will be outputted only once. 16137748cd8SNickeau */ 162*04fd306cSNickeau private bool $hasHtmlOutputOccurred = false; 163*04fd306cSNickeau private string $format = self::DEFAULT_FORMAT; 16437748cd8SNickeau 165*04fd306cSNickeau /** 166*04fd306cSNickeau * @param Path $path - path mandatory because it's the path of fetch and it's the storage format 167*04fd306cSNickeau * use {@link Snippet::getOrCreateFromContext()} 168*04fd306cSNickeau */ 169*04fd306cSNickeau private function __construct(Path $path) 17037748cd8SNickeau { 171*04fd306cSNickeau $this->path = $path; 17237748cd8SNickeau } 17337748cd8SNickeau 17437748cd8SNickeau 17537748cd8SNickeau /** 176*04fd306cSNickeau * @throws ExceptionBadArgument 17737748cd8SNickeau */ 178*04fd306cSNickeau public static function createCssSnippetFromComponentId($componentId): Snippet 17937748cd8SNickeau { 180*04fd306cSNickeau return Snippet::createSnippetFromComponentId($componentId, self::EXTENSION_CSS); 1814cadd4f8SNickeau } 1824cadd4f8SNickeau 183*04fd306cSNickeau /** 184*04fd306cSNickeau * @throws ExceptionBadArgument 185*04fd306cSNickeau */ 186*04fd306cSNickeau public static function createSnippetFromComponentId($componentId, $type): Snippet 1874cadd4f8SNickeau { 188*04fd306cSNickeau $path = self::getInternalPathFromNameAndExtension($componentId, $type); 189*04fd306cSNickeau return Snippet::createSnippetFromPath($path) 190*04fd306cSNickeau ->setComponentId($componentId); 191*04fd306cSNickeau } 192*04fd306cSNickeau 1934cadd4f8SNickeau 1944cadd4f8SNickeau /** 1954cadd4f8SNickeau * The snippet id is the url for external resources (ie external javascript / stylesheet) 1964cadd4f8SNickeau * otherwise if it's internal, it's the component id and it's type 197*04fd306cSNickeau * @param string $componentId - the component id is a short id that you will found in the class (for internal snippet, it helps also resolve the file) 198*04fd306cSNickeau * @param string $extension - {@link Snippet::EXTENSION_CSS css} or {@link Snippet::EXTENSION_JS js} 199*04fd306cSNickeau * @return Snippet 2004cadd4f8SNickeau */ 201*04fd306cSNickeau public static function getOrCreateFromComponentId(string $componentId, string $extension): Snippet 202*04fd306cSNickeau { 203*04fd306cSNickeau 204*04fd306cSNickeau $snippetPath = self::getInternalPathFromNameAndExtension($componentId, $extension); 205*04fd306cSNickeau return self::getOrCreateFromContext($snippetPath) 206*04fd306cSNickeau ->setComponentId($componentId); 207*04fd306cSNickeau 2084cadd4f8SNickeau } 209*04fd306cSNickeau 210*04fd306cSNickeau 211*04fd306cSNickeau /** 212*04fd306cSNickeau * 213*04fd306cSNickeau * 214*04fd306cSNickeau * The order is the order where they were added/created. 215*04fd306cSNickeau * 216*04fd306cSNickeau * The internal script may be dependent on the external javascript 217*04fd306cSNickeau * and vice-versa (for instance, Math-Jax library is dependent 218*04fd306cSNickeau * on the config that is an internal inline script) 219*04fd306cSNickeau * 220*04fd306cSNickeau * @return Snippet[] 221*04fd306cSNickeau * 222*04fd306cSNickeau */ 223*04fd306cSNickeau public static function getSnippets(): array 224*04fd306cSNickeau { 225*04fd306cSNickeau try { 226*04fd306cSNickeau return ExecutionContext::getActualOrCreateFromEnv()->getRuntimeObject(self::CANONICAL); 227*04fd306cSNickeau } catch (ExceptionNotFound $e) { 228*04fd306cSNickeau return []; 2294cadd4f8SNickeau } 2304cadd4f8SNickeau } 231*04fd306cSNickeau 232*04fd306cSNickeau 233*04fd306cSNickeau /** 234*04fd306cSNickeau * @param WikiPath $path - a local path of the snippet (if the path does not exist, a remote url should be given) 235*04fd306cSNickeau * @return Snippet 236*04fd306cSNickeau */ 237*04fd306cSNickeau public static function createSnippet(Path $path): Snippet 238*04fd306cSNickeau { 239*04fd306cSNickeau return new Snippet($path); 2404cadd4f8SNickeau } 241*04fd306cSNickeau 242*04fd306cSNickeau 243*04fd306cSNickeau /** 244*04fd306cSNickeau * @param $snippetId - a logical id 245*04fd306cSNickeau * @return string - the class 246*04fd306cSNickeau * See also {@link Snippet::getClass()} function 247*04fd306cSNickeau */ 248*04fd306cSNickeau public static function getClassFromComponentId($snippetId): string 249*04fd306cSNickeau { 250*04fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet-" . $snippetId); 251*04fd306cSNickeau } 252*04fd306cSNickeau 253*04fd306cSNickeau /** 254*04fd306cSNickeau * @throws ExceptionBadArgument 255*04fd306cSNickeau */ 256*04fd306cSNickeau public static function createSnippetFromPath(WikiPath $path): Snippet 257*04fd306cSNickeau { 258*04fd306cSNickeau return new Snippet($path); 259*04fd306cSNickeau } 260*04fd306cSNickeau 261*04fd306cSNickeau 262*04fd306cSNickeau /** 263*04fd306cSNickeau * @param Path $localSnippetPath - the path is the snippet identifier (it's not mandatory that the snippet is locally available 264*04fd306cSNickeau * but if it's, it permits to work without any connection by setting the {@link Snippet::CONF_USE_CDN cdn} to off 265*04fd306cSNickeau * @return Snippet 266*04fd306cSNickeau */ 267*04fd306cSNickeau public static function getOrCreateFromContext(Path $localSnippetPath): Snippet 268*04fd306cSNickeau { 269*04fd306cSNickeau 270*04fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 271*04fd306cSNickeau try { 272*04fd306cSNickeau $snippets = &$executionContext->getRuntimeObject(self::CANONICAL); 273*04fd306cSNickeau } catch (ExceptionNotFound $e) { 274*04fd306cSNickeau $snippets = []; 275*04fd306cSNickeau $executionContext->setRuntimeObject(self::CANONICAL, $snippets); 276*04fd306cSNickeau } 277*04fd306cSNickeau $snippetGuid = $localSnippetPath->toUriString(); 278*04fd306cSNickeau $snippet = &$snippets[$snippetGuid]; 2794cadd4f8SNickeau if ($snippet === null) { 280*04fd306cSNickeau $snippet = self::createSnippet($localSnippetPath); 281*04fd306cSNickeau /** 282*04fd306cSNickeau * 283*04fd306cSNickeau * The order is the order where they were added/created. 284*04fd306cSNickeau * 285*04fd306cSNickeau * The internal script may be dependent on the external javascript 286*04fd306cSNickeau * and vice-versa (for instance, Math-Jax library is dependent 287*04fd306cSNickeau * on the config that is an internal inline script) 288*04fd306cSNickeau * 289*04fd306cSNickeau */ 290*04fd306cSNickeau $snippets[$snippetGuid] = $snippet; 2914cadd4f8SNickeau } 292*04fd306cSNickeau 293*04fd306cSNickeau try { 294*04fd306cSNickeau $executingFetcher = $executionContext 295*04fd306cSNickeau ->getExecutingMarkupHandler(); 296*04fd306cSNickeau /** 297*04fd306cSNickeau * New way 298*04fd306cSNickeau */ 299*04fd306cSNickeau $executingFetcher->addSnippet($snippet); 300*04fd306cSNickeau try { 301*04fd306cSNickeau /** 302*04fd306cSNickeau * Old way 303*04fd306cSNickeau * @deprecated 304*04fd306cSNickeau * but still used to store the snippets 305*04fd306cSNickeau */ 306*04fd306cSNickeau $wikiId = $executingFetcher->getSourcePath()->toWikiPath()->getWikiId(); 307*04fd306cSNickeau $snippet->addElement($wikiId); 308*04fd306cSNickeau } catch (ExceptionCast $e) { 309*04fd306cSNickeau // not a wiki path 310*04fd306cSNickeau } catch (ExceptionNotFound $e) { 311*04fd306cSNickeau /** 312*04fd306cSNickeau * String/dynamic run 313*04fd306cSNickeau * (Example via an {@link \syntax_plugin_combo_iterator}) 314*04fd306cSNickeau * The fetcher should have then a parent 315*04fd306cSNickeau */ 316*04fd306cSNickeau try { 317*04fd306cSNickeau $wikiId = $executionContext->getExecutingParentMarkupHandler()->getSourcePath()->toWikiPath()->getWikiId(); 318*04fd306cSNickeau $snippet->addElement($wikiId); 319*04fd306cSNickeau } catch (ExceptionCast $e) { 320*04fd306cSNickeau // not a wiki path 321*04fd306cSNickeau } catch (ExceptionNotFound $e) { 322*04fd306cSNickeau // no parent found 323*04fd306cSNickeau } 324*04fd306cSNickeau 325*04fd306cSNickeau } 326*04fd306cSNickeau } catch (ExceptionNotFound $e) { 327*04fd306cSNickeau /** 328*04fd306cSNickeau * admin page, page scope or theme is not used 329*04fd306cSNickeau * This snippets are not due to the markup 330*04fd306cSNickeau */ 331*04fd306cSNickeau try { 332*04fd306cSNickeau $executingId = $executionContext->getExecutingWikiId(); 333*04fd306cSNickeau $snippet->addElement($executingId); 334*04fd306cSNickeau } catch (ExceptionNotFound $e) { 335*04fd306cSNickeau $snippet->addElement(Snippet::REQUEST_SCOPE); 336*04fd306cSNickeau } 337*04fd306cSNickeau } 338*04fd306cSNickeau 3394cadd4f8SNickeau return $snippet; 3404cadd4f8SNickeau 3414cadd4f8SNickeau } 3424cadd4f8SNickeau 343*04fd306cSNickeau /** 344*04fd306cSNickeau * Create a snippet from the ComboDrive 345*04fd306cSNickeau * @throws ExceptionBadArgument 346*04fd306cSNickeau */ 347*04fd306cSNickeau public static function createComboSnippet(string $wikiPath): Snippet 3484cadd4f8SNickeau { 349*04fd306cSNickeau $wikiPathObject = WikiPath::createComboResource($wikiPath); 350*04fd306cSNickeau return self::createSnippetFromPath($wikiPathObject); 3514cadd4f8SNickeau } 3524cadd4f8SNickeau 3534cadd4f8SNickeau /** 354*04fd306cSNickeau * @param string $wikiPath - the wiki path should be absolute relative to the library namespace 355*04fd306cSNickeau * @return Snippet 356*04fd306cSNickeau * 357*04fd306cSNickeau * Example: `:bootstrap:4.5.0:bootstrap.min.css` 3584cadd4f8SNickeau */ 359*04fd306cSNickeau public static function getOrCreateFromLibraryNamespace(string $wikiPath): Snippet 3604cadd4f8SNickeau { 361*04fd306cSNickeau $wikiPathObject = WikiPath::createComboResource(self::LIBRARY_BASE . $wikiPath); 362*04fd306cSNickeau return self::getOrCreateFromContext($wikiPathObject); 3634cadd4f8SNickeau } 364*04fd306cSNickeau 365*04fd306cSNickeau /** 366*04fd306cSNickeau * An utility class to create a snippet from a remote url 367*04fd306cSNickeau * 368*04fd306cSNickeau * If you want to be able to serve the library locally, you 369*04fd306cSNickeau * should create the snippet via the {@link Snippet::getOrCreateFromLibraryNamespace() local path} 370*04fd306cSNickeau * and set {@link Snippet::setRemoteUrl() remote url} 371*04fd306cSNickeau * 372*04fd306cSNickeau * @throws ExceptionBadArgument - if the url does not have a file name 373*04fd306cSNickeau */ 374*04fd306cSNickeau public static function getOrCreateFromRemoteUrl(Url $url): Snippet 375*04fd306cSNickeau { 376*04fd306cSNickeau 377*04fd306cSNickeau try { 378*04fd306cSNickeau $libraryName = $url->getLastName(); 379*04fd306cSNickeau } catch (ExceptionNotFound $e) { 380*04fd306cSNickeau $messageFormat = "The following url ($url) does not have a file name. To create a snippet from a remote url, the url should have a path where the last is the name of the library file."; 381*04fd306cSNickeau throw new ExceptionBadArgument($messageFormat); 382*04fd306cSNickeau } 383*04fd306cSNickeau /** 384*04fd306cSNickeau * The file generally does not exists 385*04fd306cSNickeau */ 386*04fd306cSNickeau $localPath = WikiPath::createComboResource(Snippet::LIBRARY_BASE . ":$libraryName"); 387*04fd306cSNickeau try { 388*04fd306cSNickeau $localPath->getExtension(); 389*04fd306cSNickeau } catch (ExceptionNotFound $e) { 390*04fd306cSNickeau $messageFormat = "The url has a file name ($libraryName) that does not have any extension. To create a snippet from a remote url, the url should have a path where the last is the name of the library file. "; 391*04fd306cSNickeau throw new ExceptionBadArgument($messageFormat); 392*04fd306cSNickeau } 393*04fd306cSNickeau return self::getOrCreateFromContext($localPath) 394*04fd306cSNickeau ->setRemoteUrl($url); 395*04fd306cSNickeau 396*04fd306cSNickeau 397*04fd306cSNickeau } 398*04fd306cSNickeau 399*04fd306cSNickeau /** 400*04fd306cSNickeau * @throws ExceptionBadArgument 401*04fd306cSNickeau */ 402*04fd306cSNickeau public static function createJavascriptSnippetFromComponentId(string $componentId): Snippet 403*04fd306cSNickeau { 404*04fd306cSNickeau return Snippet::createSnippetFromComponentId($componentId, self::EXTENSION_JS); 40537748cd8SNickeau } 40637748cd8SNickeau 40737748cd8SNickeau 40837748cd8SNickeau /** 40937748cd8SNickeau * @param $bool - if the snippet is critical, it would not be deferred or preloaded 41037748cd8SNickeau * @return Snippet for chaining 41137748cd8SNickeau * All css that are for animation or background for instance 41237748cd8SNickeau * should not be set as critical as they are not needed to paint 41337748cd8SNickeau * exactly the page 4144cadd4f8SNickeau * 4154cadd4f8SNickeau * If a snippet is critical, it should not be deferred 4164cadd4f8SNickeau * 4174cadd4f8SNickeau * By default: 4184cadd4f8SNickeau * * all css are critical (except animation or background stylesheet) 4194cadd4f8SNickeau * * all javascript are not critical 4204cadd4f8SNickeau * 4214cadd4f8SNickeau * This attribute is passed in the dokuwiki array 4224cadd4f8SNickeau * The value is stored in the {@link Snippet::getCritical()} 42337748cd8SNickeau */ 424c3437056SNickeau public function setCritical($bool): Snippet 42537748cd8SNickeau { 42637748cd8SNickeau $this->critical = $bool; 42737748cd8SNickeau return $this; 42837748cd8SNickeau } 42937748cd8SNickeau 43037748cd8SNickeau /** 4314cadd4f8SNickeau * If the library does not manipulate the DOM, 4324cadd4f8SNickeau * it can be ran as soon as possible (ie async) 4334cadd4f8SNickeau * @param $bool 4344cadd4f8SNickeau * @return $this 4354cadd4f8SNickeau */ 4364cadd4f8SNickeau public function setDoesManipulateTheDomOnRun($bool): Snippet 4374cadd4f8SNickeau { 4384cadd4f8SNickeau $this->async = !$bool; 4394cadd4f8SNickeau return $this; 4404cadd4f8SNickeau } 4414cadd4f8SNickeau 4424cadd4f8SNickeau /** 4434cadd4f8SNickeau * @param $inlineContent - Set an inline content for a script or stylesheet 44437748cd8SNickeau * @return Snippet for chaining 44537748cd8SNickeau */ 4464cadd4f8SNickeau public function setInlineContent($inlineContent): Snippet 44737748cd8SNickeau { 4484cadd4f8SNickeau $this->inlineContent = $inlineContent; 44937748cd8SNickeau return $this; 45037748cd8SNickeau } 45137748cd8SNickeau 45237748cd8SNickeau /** 453*04fd306cSNickeau * The content that was set via a string (It should be used 454*04fd306cSNickeau * for dynamic content, that's why it's called dynamic) 45537748cd8SNickeau * @return string 456*04fd306cSNickeau * @throws ExceptionNotFound 45737748cd8SNickeau */ 458*04fd306cSNickeau public function getInternalDynamicContent(): string 45937748cd8SNickeau { 460*04fd306cSNickeau if (!isset($this->inlineContent)) { 461*04fd306cSNickeau throw new ExceptionNotFound("No inline content set"); 462*04fd306cSNickeau } 4634cadd4f8SNickeau return $this->inlineContent; 4644cadd4f8SNickeau } 4654cadd4f8SNickeau 4664cadd4f8SNickeau /** 4674cadd4f8SNickeau * @return string|null 468*04fd306cSNickeau * @throws ExceptionNotFound - if not found 4694cadd4f8SNickeau */ 470*04fd306cSNickeau public function getInternalFileContent(): string 4714cadd4f8SNickeau { 472*04fd306cSNickeau $path = $this->getPath(); 4734cadd4f8SNickeau return FileSystems::getContent($path); 4744cadd4f8SNickeau } 4754cadd4f8SNickeau 476*04fd306cSNickeau public function getPath(): Path 4774cadd4f8SNickeau { 478*04fd306cSNickeau return $this->path; 479*04fd306cSNickeau } 480*04fd306cSNickeau 481*04fd306cSNickeau public static function getInternalPathFromNameAndExtension($name, $extension): WikiPath 482*04fd306cSNickeau { 483*04fd306cSNickeau 484*04fd306cSNickeau switch ($extension) { 4854cadd4f8SNickeau case self::EXTENSION_CSS: 4864cadd4f8SNickeau $extension = "css"; 487*04fd306cSNickeau return TemplateEngine::createFromContext() 488*04fd306cSNickeau ->getComponentStylePathByName(strtolower($name) . ".$extension"); 4894cadd4f8SNickeau case self::EXTENSION_JS: 4904cadd4f8SNickeau $extension = "js"; 4914cadd4f8SNickeau $subDirectory = "js"; 492*04fd306cSNickeau return WikiPath::createComboResource(self::SNIPPET_BASE) 4934cadd4f8SNickeau ->resolve($subDirectory) 494*04fd306cSNickeau ->resolve(strtolower($name) . ".$extension"); 495*04fd306cSNickeau default: 496*04fd306cSNickeau $message = "Unknown snippet type ($extension)"; 497*04fd306cSNickeau throw new ExceptionRuntimeInternal($message); 498*04fd306cSNickeau } 499*04fd306cSNickeau 50037748cd8SNickeau } 50137748cd8SNickeau 5024cadd4f8SNickeau public function hasSlot($slot): bool 50337748cd8SNickeau { 504*04fd306cSNickeau if ($this->elements === null) { 5054cadd4f8SNickeau return false; 50637748cd8SNickeau } 507*04fd306cSNickeau return key_exists($slot, $this->elements); 50837748cd8SNickeau } 50937748cd8SNickeau 51037748cd8SNickeau public function __toString() 51137748cd8SNickeau { 512*04fd306cSNickeau return $this->path->toUriString(); 51337748cd8SNickeau } 51437748cd8SNickeau 515c3437056SNickeau public function getCritical(): bool 51637748cd8SNickeau { 517*04fd306cSNickeau 518*04fd306cSNickeau if (isset($this->critical)) { 519*04fd306cSNickeau return $this->critical; 520*04fd306cSNickeau } 521*04fd306cSNickeau try { 522*04fd306cSNickeau if ($this->path->getExtension() === self::EXTENSION_CSS) { 523c3437056SNickeau // All CSS should be loaded first 524c3437056SNickeau // The CSS animation / background can set this to false 525c3437056SNickeau return true; 526c3437056SNickeau } 527*04fd306cSNickeau } catch (ExceptionNotFound $e) { 528*04fd306cSNickeau // no path extension 529c3437056SNickeau } 530*04fd306cSNickeau return false; 531*04fd306cSNickeau 53237748cd8SNickeau } 53337748cd8SNickeau 534c3437056SNickeau public function getClass(): string 53537748cd8SNickeau { 53637748cd8SNickeau /** 53737748cd8SNickeau * The class for the snippet is just to be able to identify them 53837748cd8SNickeau * 53937748cd8SNickeau * The `snippet` prefix was added to be sure that the class 54037748cd8SNickeau * name will not conflict with a css class 54137748cd8SNickeau * Example: if you set the class to `combo-list` 54237748cd8SNickeau * and that you use it in a inline `style` tag with 54337748cd8SNickeau * the same class name, the inline `style` tag is not applied 54437748cd8SNickeau * 54537748cd8SNickeau */ 546*04fd306cSNickeau try { 547*04fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet-" . $this->getComponentId()); 548*04fd306cSNickeau } catch (ExceptionNotFound $e) { 549*04fd306cSNickeau LogUtility::internalError("A component id was not found for the snippet ($this)", self::CANONICAL); 550*04fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet"); 551*04fd306cSNickeau } 55237748cd8SNickeau 55337748cd8SNickeau } 55437748cd8SNickeau 555*04fd306cSNickeau 55637748cd8SNickeau /** 557*04fd306cSNickeau * @return string - the component name identifier 558*04fd306cSNickeau * All snippet with this component id are from the same component 559*04fd306cSNickeau * @throws ExceptionNotFound 56037748cd8SNickeau */ 561*04fd306cSNickeau public function getComponentId(): string 56237748cd8SNickeau { 563*04fd306cSNickeau if (isset($this->componentId)) { 564*04fd306cSNickeau return $this->componentId; 56537748cd8SNickeau } 566*04fd306cSNickeau throw new ExceptionNotFound("No component id was set"); 56737748cd8SNickeau } 56837748cd8SNickeau 56937748cd8SNickeau 5704cadd4f8SNickeau public function toJsonArray(): array 571c3437056SNickeau { 5724cadd4f8SNickeau return $this->jsonSerialize(); 573c3437056SNickeau 574c3437056SNickeau } 575c3437056SNickeau 576c3437056SNickeau /** 577*04fd306cSNickeau * @throws ExceptionCompile 578c3437056SNickeau */ 579c3437056SNickeau public static function createFromJson($array): Snippet 580c3437056SNickeau { 5814cadd4f8SNickeau 582*04fd306cSNickeau $uri = $array[self::JSON_URI_PROPERTY]; 583*04fd306cSNickeau if ($uri === null) { 584*04fd306cSNickeau throw new ExceptionCompile("The snippet uri property was not found in the json array"); 585*04fd306cSNickeau } 586*04fd306cSNickeau 587*04fd306cSNickeau $wikiPath = FileSystems::createPathFromUri($uri); 588*04fd306cSNickeau $snippet = Snippet::getOrCreateFromContext($wikiPath); 589*04fd306cSNickeau 590*04fd306cSNickeau $componentName = $array[self::JSON_COMPONENT_PROPERTY]; 591*04fd306cSNickeau if ($componentName !== null) { 592*04fd306cSNickeau $snippet->setComponentId($componentName); 593*04fd306cSNickeau } 5944cadd4f8SNickeau 595c3437056SNickeau $critical = $array[self::JSON_CRITICAL_PROPERTY]; 596c3437056SNickeau if ($critical !== null) { 597c3437056SNickeau $snippet->setCritical($critical); 598c3437056SNickeau } 599c3437056SNickeau 6004cadd4f8SNickeau $async = $array[self::JSON_ASYNC_PROPERTY]; 6014cadd4f8SNickeau if ($async !== null) { 6024cadd4f8SNickeau $snippet->setDoesManipulateTheDomOnRun($async); 6034cadd4f8SNickeau } 6044cadd4f8SNickeau 605*04fd306cSNickeau $format = $array[self::JSON_FORMAT_PROPERTY]; 606*04fd306cSNickeau if ($format !== null) { 607*04fd306cSNickeau $snippet->setFormat($format); 608*04fd306cSNickeau } 609*04fd306cSNickeau 610c3437056SNickeau $content = $array[self::JSON_CONTENT_PROPERTY]; 611c3437056SNickeau if ($content !== null) { 6124cadd4f8SNickeau $snippet->setInlineContent($content); 613c3437056SNickeau } 614c3437056SNickeau 6154cadd4f8SNickeau $attributes = $array[self::JSON_HTML_ATTRIBUTES_PROPERTY]; 6164cadd4f8SNickeau if ($attributes !== null) { 6174cadd4f8SNickeau foreach ($attributes as $name => $value) { 6184cadd4f8SNickeau $snippet->addHtmlAttribute($name, $value); 619c3437056SNickeau } 6204cadd4f8SNickeau } 6214cadd4f8SNickeau 6224cadd4f8SNickeau $integrity = $array[self::JSON_INTEGRITY_PROPERTY]; 6234cadd4f8SNickeau if ($integrity !== null) { 6244cadd4f8SNickeau $snippet->setIntegrity($integrity); 6254cadd4f8SNickeau } 6264cadd4f8SNickeau 627*04fd306cSNickeau $remoteUrl = $array[self::JSON_URL_PROPERTY]; 628*04fd306cSNickeau if ($remoteUrl !== null) { 629*04fd306cSNickeau $snippet->setRemoteUrl(Url::createFromString($remoteUrl)); 630*04fd306cSNickeau } 631*04fd306cSNickeau 632c3437056SNickeau return $snippet; 633c3437056SNickeau 634c3437056SNickeau } 6354cadd4f8SNickeau 6364cadd4f8SNickeau public function getExtension() 6374cadd4f8SNickeau { 638*04fd306cSNickeau return $this->path->getExtension(); 6394cadd4f8SNickeau } 6404cadd4f8SNickeau 6414cadd4f8SNickeau public function setIntegrity(?string $integrity): Snippet 6424cadd4f8SNickeau { 643*04fd306cSNickeau if ($integrity === null) { 644*04fd306cSNickeau return $this; 645*04fd306cSNickeau } 6464cadd4f8SNickeau $this->integrity = $integrity; 6474cadd4f8SNickeau return $this; 6484cadd4f8SNickeau } 6494cadd4f8SNickeau 6504cadd4f8SNickeau public function addHtmlAttribute(string $name, string $value): Snippet 6514cadd4f8SNickeau { 6524cadd4f8SNickeau $this->htmlAttributes[$name] = $value; 6534cadd4f8SNickeau return $this; 6544cadd4f8SNickeau } 6554cadd4f8SNickeau 656*04fd306cSNickeau public function addElement(string $element): Snippet 6574cadd4f8SNickeau { 658*04fd306cSNickeau $this->elements[$element] = 1; 6594cadd4f8SNickeau return $this; 6604cadd4f8SNickeau } 6614cadd4f8SNickeau 662*04fd306cSNickeau public function useLocalUrl(): bool 6634cadd4f8SNickeau { 664*04fd306cSNickeau 665*04fd306cSNickeau /** 666*04fd306cSNickeau * use cdn is on and there is a remote url 667*04fd306cSNickeau */ 668*04fd306cSNickeau $useCdn = ExecutionContext::getActualOrCreateFromEnv()->getConfValue(self::CONF_USE_CDN, self::CONF_USE_CDN_DEFAULT) === 1; 669*04fd306cSNickeau if ($useCdn && isset($this->remoteUrl)) { 670*04fd306cSNickeau return false; 6714cadd4f8SNickeau } 6724cadd4f8SNickeau 673*04fd306cSNickeau /** 674*04fd306cSNickeau * use cdn is off and there is a file 675*04fd306cSNickeau */ 676*04fd306cSNickeau $fileExists = FileSystems::exists($this->path); 677*04fd306cSNickeau if ($fileExists) { 678*04fd306cSNickeau return true; 6794cadd4f8SNickeau } 6804cadd4f8SNickeau 681*04fd306cSNickeau /** 682*04fd306cSNickeau * Use cdn is off and there is a remote url 683*04fd306cSNickeau */ 684*04fd306cSNickeau if (isset($this->remoteUrl)) { 685*04fd306cSNickeau return false; 686*04fd306cSNickeau } 687*04fd306cSNickeau 688*04fd306cSNickeau /** 689*04fd306cSNickeau * 690*04fd306cSNickeau * This is a inline script (no local file then) 691*04fd306cSNickeau * 692*04fd306cSNickeau * We default to the local url that will return an error 693*04fd306cSNickeau * when fetched 694*04fd306cSNickeau */ 695*04fd306cSNickeau if (!$this->shouldBeInlined()) { 696*04fd306cSNickeau LogUtility::internalError("The snippet ($this) is not a inline script, it has a path ($this->path) that does not exists and does not have any external url."); 697*04fd306cSNickeau } 698*04fd306cSNickeau return false; 699*04fd306cSNickeau 700*04fd306cSNickeau } 701*04fd306cSNickeau 702*04fd306cSNickeau /** 703*04fd306cSNickeau * 704*04fd306cSNickeau */ 705*04fd306cSNickeau public function getLocalUrl(): Url 706*04fd306cSNickeau { 707*04fd306cSNickeau try { 708*04fd306cSNickeau $path = WikiPath::createFromPathObject($this->path); 709*04fd306cSNickeau return FetcherRawLocalPath::createFromPath($path)->getFetchUrl(); 710*04fd306cSNickeau } catch (ExceptionBadArgument $e) { 711*04fd306cSNickeau throw new ExceptionRuntimeInternal("The local url should ne asked. use (hasLocalUrl) before calling this function", self::CANONICAL, $e); 712*04fd306cSNickeau } 713*04fd306cSNickeau 714*04fd306cSNickeau } 715*04fd306cSNickeau 716*04fd306cSNickeau 717*04fd306cSNickeau /** 718*04fd306cSNickeau * @throws ExceptionNotFound 719*04fd306cSNickeau */ 720*04fd306cSNickeau public function getRemoteUrl(): Url 721*04fd306cSNickeau { 722*04fd306cSNickeau if (!isset($this->remoteUrl)) { 723*04fd306cSNickeau throw new ExceptionNotFound("No remote url found"); 724*04fd306cSNickeau } 725*04fd306cSNickeau return $this->remoteUrl; 726*04fd306cSNickeau } 727*04fd306cSNickeau 728*04fd306cSNickeau /** 729*04fd306cSNickeau * @throws ExceptionNotFound 730*04fd306cSNickeau */ 7314cadd4f8SNickeau public function getIntegrity(): ?string 7324cadd4f8SNickeau { 733*04fd306cSNickeau if (!isset($this->integrity)) { 734*04fd306cSNickeau throw new ExceptionNotFound("No integrity"); 735*04fd306cSNickeau } 7364cadd4f8SNickeau return $this->integrity; 7374cadd4f8SNickeau } 7384cadd4f8SNickeau 739*04fd306cSNickeau public function getHtmlAttributes(): array 7404cadd4f8SNickeau { 741*04fd306cSNickeau if (!isset($this->htmlAttributes)) { 742*04fd306cSNickeau return []; 743*04fd306cSNickeau } 7444cadd4f8SNickeau return $this->htmlAttributes; 7454cadd4f8SNickeau } 7464cadd4f8SNickeau 747*04fd306cSNickeau 748*04fd306cSNickeau /** 749*04fd306cSNickeau * @throws ExceptionNotFound 750*04fd306cSNickeau */ 751*04fd306cSNickeau public function getInternalInlineAndFileContent(): string 7524cadd4f8SNickeau { 7534cadd4f8SNickeau $totalContent = null; 754*04fd306cSNickeau try { 755*04fd306cSNickeau $totalContent = $this->getInternalFileContent(); 756*04fd306cSNickeau } catch (ExceptionNotFound $e) { 757*04fd306cSNickeau // no 7584cadd4f8SNickeau } 7594cadd4f8SNickeau 760*04fd306cSNickeau try { 761*04fd306cSNickeau $totalContent .= $this->getInternalDynamicContent(); 762*04fd306cSNickeau } catch (ExceptionNotFound $e) { 763*04fd306cSNickeau // no 7644cadd4f8SNickeau } 765*04fd306cSNickeau if ($totalContent === null) { 766*04fd306cSNickeau throw new ExceptionNotFound("No content"); 7674cadd4f8SNickeau } 7684cadd4f8SNickeau return $totalContent; 7694cadd4f8SNickeau 7704cadd4f8SNickeau } 7714cadd4f8SNickeau 7724cadd4f8SNickeau 773*04fd306cSNickeau /** 774*04fd306cSNickeau */ 7754cadd4f8SNickeau public function jsonSerialize(): array 7764cadd4f8SNickeau { 777*04fd306cSNickeau 7784cadd4f8SNickeau $dataToSerialize = [ 779*04fd306cSNickeau self::JSON_URI_PROPERTY => $this->getPath()->toUriString(), 7804cadd4f8SNickeau ]; 781*04fd306cSNickeau 782*04fd306cSNickeau try { 783*04fd306cSNickeau $dataToSerialize[self::JSON_COMPONENT_PROPERTY] = $this->getComponentId(); 784*04fd306cSNickeau } catch (ExceptionNotFound $e) { 785*04fd306cSNickeau LogUtility::internalError("The component id was not set for the snippet ($this)"); 7864cadd4f8SNickeau } 787*04fd306cSNickeau 788*04fd306cSNickeau if (isset($this->remoteUrl)) { 789*04fd306cSNickeau $dataToSerialize[self::JSON_URL_PROPERTY] = $this->remoteUrl->toString(); 790*04fd306cSNickeau } 791*04fd306cSNickeau if (isset($this->integrity)) { 7924cadd4f8SNickeau $dataToSerialize[self::JSON_INTEGRITY_PROPERTY] = $this->integrity; 7934cadd4f8SNickeau } 794*04fd306cSNickeau if (isset($this->critical)) { 7954cadd4f8SNickeau $dataToSerialize[self::JSON_CRITICAL_PROPERTY] = $this->critical; 7964cadd4f8SNickeau } 797*04fd306cSNickeau if (isset($this->async)) { 7984cadd4f8SNickeau $dataToSerialize[self::JSON_ASYNC_PROPERTY] = $this->async; 7994cadd4f8SNickeau } 800*04fd306cSNickeau if (isset($this->inlineContent)) { 8014cadd4f8SNickeau $dataToSerialize[self::JSON_CONTENT_PROPERTY] = $this->inlineContent; 8024cadd4f8SNickeau } 803*04fd306cSNickeau if ($this->format!==self::DEFAULT_FORMAT) { 804*04fd306cSNickeau $dataToSerialize[self::JSON_FORMAT_PROPERTY] = $this->format; 805*04fd306cSNickeau } 806*04fd306cSNickeau if (isset($this->htmlAttributes)) { 8074cadd4f8SNickeau $dataToSerialize[self::JSON_HTML_ATTRIBUTES_PROPERTY] = $this->htmlAttributes; 8084cadd4f8SNickeau } 8094cadd4f8SNickeau return $dataToSerialize; 8104cadd4f8SNickeau } 811*04fd306cSNickeau 812*04fd306cSNickeau 813*04fd306cSNickeau public function hasInlineContent(): bool 814*04fd306cSNickeau { 815*04fd306cSNickeau return isset($this->inlineContent); 816*04fd306cSNickeau } 817*04fd306cSNickeau 818*04fd306cSNickeau private 819*04fd306cSNickeau function getMaxInlineSize() 820*04fd306cSNickeau { 821*04fd306cSNickeau return SiteConfig::getConfValue(SiteConfig::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT, 2) * 1024; 822*04fd306cSNickeau } 823*04fd306cSNickeau 824*04fd306cSNickeau /** 825*04fd306cSNickeau * Returns if the internal snippet should be incorporated 826*04fd306cSNickeau * in the page or not 827*04fd306cSNickeau * 828*04fd306cSNickeau * Requiring a lot of small javascript file adds a penalty to page load 829*04fd306cSNickeau * 830*04fd306cSNickeau * @return bool 831*04fd306cSNickeau */ 832*04fd306cSNickeau public function shouldBeInlined(): bool 833*04fd306cSNickeau { 834*04fd306cSNickeau /** 835*04fd306cSNickeau * If there is inline content, true 836*04fd306cSNickeau */ 837*04fd306cSNickeau if ($this->hasInlineContent()) { 838*04fd306cSNickeau return true; 839*04fd306cSNickeau } 840*04fd306cSNickeau 841*04fd306cSNickeau /** 842*04fd306cSNickeau * If the file does not exist 843*04fd306cSNickeau * and that there is a remote url 844*04fd306cSNickeau */ 845*04fd306cSNickeau $internalPath = $this->getPath(); 846*04fd306cSNickeau if (!FileSystems::exists($internalPath)) { 847*04fd306cSNickeau try { 848*04fd306cSNickeau $this->getRemoteUrl(); 849*04fd306cSNickeau return false; 850*04fd306cSNickeau } catch (ExceptionNotFound $e) { 851*04fd306cSNickeau // no remote url, no file, no inline: error 852*04fd306cSNickeau LogUtility::internalError("The snippet ($this) does not have content defined (the path does not exist, no inline content and no remote url)", self::CANONICAL); 853*04fd306cSNickeau return true; 854*04fd306cSNickeau } 855*04fd306cSNickeau } 856*04fd306cSNickeau 857*04fd306cSNickeau /** 858*04fd306cSNickeau * The file exists 859*04fd306cSNickeau * If we can't serve it locally, it should be inlined 860*04fd306cSNickeau */ 861*04fd306cSNickeau if (!$this->hasLocalUrl()) { 862*04fd306cSNickeau return true; 863*04fd306cSNickeau } 864*04fd306cSNickeau 865*04fd306cSNickeau /** 866*04fd306cSNickeau * File exists and can be served 867*04fd306cSNickeau */ 868*04fd306cSNickeau 869*04fd306cSNickeau /** 870*04fd306cSNickeau * Local Javascript Library ? 871*04fd306cSNickeau */ 872*04fd306cSNickeau if ($this->getExtension() === Snippet::EXTENSION_JS) { 873*04fd306cSNickeau 874*04fd306cSNickeau try { 875*04fd306cSNickeau $lastName = $internalPath->getLastName(); 876*04fd306cSNickeau } catch (ExceptionNotFound $e) { 877*04fd306cSNickeau LogUtility::internalError("Every snippet should have a last name"); 878*04fd306cSNickeau return false; 879*04fd306cSNickeau } 880*04fd306cSNickeau 881*04fd306cSNickeau /** 882*04fd306cSNickeau * If this is a local library don't inline 883*04fd306cSNickeau * Why ? 884*04fd306cSNickeau * The local combo.min.js library depends on bootstrap.min.js 885*04fd306cSNickeau * If we inject it, we get `bootstrap` does not exist. 886*04fd306cSNickeau * Other solution, to resolve it, we could: 887*04fd306cSNickeau * * inline all javascript 888*04fd306cSNickeau * * start a local server and serve the local library 889*04fd306cSNickeau * * publish the local library (not really realistic if we test but yeah) 890*04fd306cSNickeau */ 891*04fd306cSNickeau $libraryExtension = ".min.js"; 892*04fd306cSNickeau $isLibrary = substr($lastName, -strlen($libraryExtension)) === $libraryExtension; 893*04fd306cSNickeau if (!$this->hasRemoteUrl() && !$isLibrary) { 894*04fd306cSNickeau $alwaysInline = ExecutionContext::getActualOrCreateFromEnv() 895*04fd306cSNickeau ->getConfig() 896*04fd306cSNickeau ->isLocalJavascriptAlwaysInlined(); 897*04fd306cSNickeau if ($alwaysInline) { 898*04fd306cSNickeau return true; 899*04fd306cSNickeau } 900*04fd306cSNickeau } 901*04fd306cSNickeau } 902*04fd306cSNickeau 903*04fd306cSNickeau /** 904*04fd306cSNickeau * The file exists (inline if small size) 905*04fd306cSNickeau */ 906*04fd306cSNickeau if (FileSystems::getSize($internalPath) > $this->getMaxInlineSize()) { 907*04fd306cSNickeau return false; 908*04fd306cSNickeau } 909*04fd306cSNickeau 910*04fd306cSNickeau return true; 911*04fd306cSNickeau 912*04fd306cSNickeau } 913*04fd306cSNickeau 914*04fd306cSNickeau /** 915*04fd306cSNickeau * @return array 916*04fd306cSNickeau * @throws ExceptionBadArgument 917*04fd306cSNickeau * @throws ExceptionBadState - an error where for instance an inline script does not have any content 918*04fd306cSNickeau * @throws ExceptionCast 919*04fd306cSNickeau * @throws ExceptionNotFound - an error where the source was not found 920*04fd306cSNickeau */ 921*04fd306cSNickeau public function toDokuWikiArray(): array 922*04fd306cSNickeau { 923*04fd306cSNickeau $tagAttributes = $this->toTagAttributes(); 924*04fd306cSNickeau $array = $tagAttributes->toCallStackArray(); 925*04fd306cSNickeau unset($array[TagAttributes::GENERATED_ID_KEY]); 926*04fd306cSNickeau return $array; 927*04fd306cSNickeau } 928*04fd306cSNickeau 929*04fd306cSNickeau 930*04fd306cSNickeau /** 931*04fd306cSNickeau * The HTML tag 932*04fd306cSNickeau */ 933*04fd306cSNickeau public function getHtmlTag(): string 934*04fd306cSNickeau { 935*04fd306cSNickeau $extension = $this->getExtension(); 936*04fd306cSNickeau switch ($extension) { 937*04fd306cSNickeau case Snippet::EXTENSION_JS: 938*04fd306cSNickeau return self::SCRIPT_TAG; 939*04fd306cSNickeau case Snippet::EXTENSION_CSS: 940*04fd306cSNickeau if ($this->shouldBeInlined()) { 941*04fd306cSNickeau return Snippet::STYLE_TAG; 942*04fd306cSNickeau } else { 943*04fd306cSNickeau return Snippet::LINK_TAG; 944*04fd306cSNickeau } 945*04fd306cSNickeau default: 946*04fd306cSNickeau // it should not happen as the devs are the creator of snippet (not the user) 947*04fd306cSNickeau LogUtility::internalError("The extension ($extension) is unknown", self::CANONICAL); 948*04fd306cSNickeau return ""; 949*04fd306cSNickeau } 950*04fd306cSNickeau } 951*04fd306cSNickeau 952*04fd306cSNickeau public function setComponentId(string $componentId): Snippet 953*04fd306cSNickeau { 954*04fd306cSNickeau $this->componentId = $componentId; 955*04fd306cSNickeau return $this; 956*04fd306cSNickeau } 957*04fd306cSNickeau 958*04fd306cSNickeau public function setRemoteUrl(Url $url): Snippet 959*04fd306cSNickeau { 960*04fd306cSNickeau $this->remoteUrl = $url; 961*04fd306cSNickeau return $this; 962*04fd306cSNickeau } 963*04fd306cSNickeau 964*04fd306cSNickeau 965*04fd306cSNickeau public function useRemoteUrl(): bool 966*04fd306cSNickeau { 967*04fd306cSNickeau return !$this->useLocalUrl(); 968*04fd306cSNickeau } 969*04fd306cSNickeau 970*04fd306cSNickeau /** 971*04fd306cSNickeau * @return TagAttributes 972*04fd306cSNickeau * @throws ExceptionBadArgument 973*04fd306cSNickeau * @throws ExceptionBadState - if no content was found 974*04fd306cSNickeau * @throws ExceptionCast 975*04fd306cSNickeau * @throws ExceptionNotFound - if the file was not found 976*04fd306cSNickeau */ 977*04fd306cSNickeau public function toTagAttributes(): TagAttributes 978*04fd306cSNickeau { 979*04fd306cSNickeau 980*04fd306cSNickeau if ($this->hasHtmlOutputOccurred) { 981*04fd306cSNickeau $message = "The snippet ($this) has already been asked. It may have been added twice to the HTML page"; 982*04fd306cSNickeau if (PluginUtility::isTest()) { 983*04fd306cSNickeau $message = "Error: you may be running two pages fetch in the same execution context. $message"; 984*04fd306cSNickeau } 985*04fd306cSNickeau LogUtility::internalError($message); 986*04fd306cSNickeau } 987*04fd306cSNickeau $this->hasHtmlOutputOccurred = true; 988*04fd306cSNickeau 989*04fd306cSNickeau $tagAttributes = TagAttributes::createFromCallStackArray($this->getHtmlAttributes()) 990*04fd306cSNickeau ->addClassName($this->getClass()); 991*04fd306cSNickeau $extension = $this->getExtension(); 992*04fd306cSNickeau switch ($extension) { 993*04fd306cSNickeau case Snippet::EXTENSION_JS: 994*04fd306cSNickeau 995*04fd306cSNickeau if ($this->shouldBeInlined()) { 996*04fd306cSNickeau 997*04fd306cSNickeau try { 998*04fd306cSNickeau $tagAttributes->setInnerText($this->getInnerHtml()); 999*04fd306cSNickeau return $tagAttributes; 1000*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1001*04fd306cSNickeau throw new ExceptionBadState("The internal js snippet ($this) has no content. Skipped", self::CANONICAL); 1002*04fd306cSNickeau } 1003*04fd306cSNickeau 1004*04fd306cSNickeau } else { 1005*04fd306cSNickeau 1006*04fd306cSNickeau if ($this->useRemoteUrl()) { 1007*04fd306cSNickeau $fetchUrl = $this->getRemoteUrl(); 1008*04fd306cSNickeau } else { 1009*04fd306cSNickeau $fetchUrl = $this->getLocalUrl(); 1010*04fd306cSNickeau } 1011*04fd306cSNickeau 1012*04fd306cSNickeau /** 1013*04fd306cSNickeau * Dokuwiki encodes the URL in HTML format 1014*04fd306cSNickeau */ 1015*04fd306cSNickeau $tagAttributes 1016*04fd306cSNickeau ->addOutputAttributeValue("src", $fetchUrl->toString()) 1017*04fd306cSNickeau ->addOutputAttributeValue("crossorigin", "anonymous"); 1018*04fd306cSNickeau try { 1019*04fd306cSNickeau $integrity = $this->getIntegrity(); 1020*04fd306cSNickeau $tagAttributes->addOutputAttributeValue("integrity", $integrity); 1021*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1022*04fd306cSNickeau // ok 1023*04fd306cSNickeau } 1024*04fd306cSNickeau $critical = $this->getCritical(); 1025*04fd306cSNickeau if (!$critical) { 1026*04fd306cSNickeau $tagAttributes->addBooleanOutputAttributeValue("defer"); 1027*04fd306cSNickeau // not async: it will run as soon as possible 1028*04fd306cSNickeau // the dom main not be loaded completely, the script may miss HTML dom element 1029*04fd306cSNickeau } 1030*04fd306cSNickeau return $tagAttributes; 1031*04fd306cSNickeau 1032*04fd306cSNickeau } 1033*04fd306cSNickeau 1034*04fd306cSNickeau case Snippet::EXTENSION_CSS: 1035*04fd306cSNickeau 1036*04fd306cSNickeau if ($this->shouldBeInlined()) { 1037*04fd306cSNickeau 1038*04fd306cSNickeau try { 1039*04fd306cSNickeau $tagAttributes->setInnerText($this->getInnerHtml()); 1040*04fd306cSNickeau return $tagAttributes; 1041*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1042*04fd306cSNickeau throw new ExceptionNotFound("The internal css snippet ($this) has no content.", self::CANONICAL); 1043*04fd306cSNickeau } 1044*04fd306cSNickeau 1045*04fd306cSNickeau } else { 1046*04fd306cSNickeau 1047*04fd306cSNickeau if ($this->useRemoteUrl()) { 1048*04fd306cSNickeau $fetchUrl = $this->getRemoteUrl(); 1049*04fd306cSNickeau } else { 1050*04fd306cSNickeau $fetchUrl = $this->getLocalUrl(); 1051*04fd306cSNickeau } 1052*04fd306cSNickeau 1053*04fd306cSNickeau /** 1054*04fd306cSNickeau * Dokuwiki transforms/encode the href in HTML 1055*04fd306cSNickeau */ 1056*04fd306cSNickeau $tagAttributes 1057*04fd306cSNickeau ->addOutputAttributeValue("href", $fetchUrl->toString()) 1058*04fd306cSNickeau ->addOutputAttributeValue("crossorigin", "anonymous"); 1059*04fd306cSNickeau 1060*04fd306cSNickeau try { 1061*04fd306cSNickeau $integrity = $this->getIntegrity(); 1062*04fd306cSNickeau $tagAttributes->addOutputAttributeValue("integrity", $integrity); 1063*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1064*04fd306cSNickeau // ok 1065*04fd306cSNickeau } 1066*04fd306cSNickeau 1067*04fd306cSNickeau $critical = $this->getCritical(); 1068*04fd306cSNickeau if (!$critical && action_plugin_combo_docustom::isThemeSystemEnabled()) { 1069*04fd306cSNickeau $tagAttributes 1070*04fd306cSNickeau ->addOutputAttributeValue("rel", "preload") 1071*04fd306cSNickeau ->addOutputAttributeValue('as', self::STYLE_TAG); 1072*04fd306cSNickeau } else { 1073*04fd306cSNickeau $tagAttributes->addOutputAttributeValue("rel", "stylesheet"); 1074*04fd306cSNickeau } 1075*04fd306cSNickeau 1076*04fd306cSNickeau return $tagAttributes; 1077*04fd306cSNickeau 1078*04fd306cSNickeau } 1079*04fd306cSNickeau 1080*04fd306cSNickeau 1081*04fd306cSNickeau default: 1082*04fd306cSNickeau throw new ExceptionBadState("The extension ($extension) is unknown", self::CANONICAL); 1083*04fd306cSNickeau } 1084*04fd306cSNickeau 1085*04fd306cSNickeau } 1086*04fd306cSNickeau 1087*04fd306cSNickeau /** 1088*04fd306cSNickeau * @return bool - yes if the function {@link Snippet::toTagAttributes()} 1089*04fd306cSNickeau * or {@link Snippet::toDokuWikiArray()} has been called 1090*04fd306cSNickeau * to prevent having the snippet two times (one in the head and one in the body) 1091*04fd306cSNickeau */ 1092*04fd306cSNickeau public function hasHtmlOutputAlreadyOccurred(): bool 1093*04fd306cSNickeau { 1094*04fd306cSNickeau 1095*04fd306cSNickeau return $this->hasHtmlOutputOccurred; 1096*04fd306cSNickeau 1097*04fd306cSNickeau } 1098*04fd306cSNickeau 1099*04fd306cSNickeau private function hasRemoteUrl(): bool 1100*04fd306cSNickeau { 1101*04fd306cSNickeau try { 1102*04fd306cSNickeau $this->getRemoteUrl(); 1103*04fd306cSNickeau return true; 1104*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1105*04fd306cSNickeau return false; 1106*04fd306cSNickeau } 1107*04fd306cSNickeau } 1108*04fd306cSNickeau 1109*04fd306cSNickeau public function toXhtml(): string 1110*04fd306cSNickeau { 1111*04fd306cSNickeau try { 1112*04fd306cSNickeau $tagAttributes = $this->toTagAttributes(); 1113*04fd306cSNickeau } catch (\Exception $e) { 1114*04fd306cSNickeau throw new ExceptionRuntimeInternal("We couldn't output the snippet ($this). Error: {$e->getMessage()}", self::CANONICAL, $e); 1115*04fd306cSNickeau } 1116*04fd306cSNickeau $htmlElement = $this->getHtmlTag(); 1117*04fd306cSNickeau /** 1118*04fd306cSNickeau * This code runs in editing mode 1119*04fd306cSNickeau * or if the template is not strap 1120*04fd306cSNickeau * No preload is then supported 1121*04fd306cSNickeau */ 1122*04fd306cSNickeau if ($htmlElement === "link") { 1123*04fd306cSNickeau try { 1124*04fd306cSNickeau $relValue = $tagAttributes->getOutputAttribute("rel"); 1125*04fd306cSNickeau $relAs = $tagAttributes->getOutputAttribute("as"); 1126*04fd306cSNickeau if ($relValue === "preload") { 1127*04fd306cSNickeau if ($relAs === "style") { 1128*04fd306cSNickeau $tagAttributes->removeOutputAttributeIfPresent("rel"); 1129*04fd306cSNickeau $tagAttributes->addOutputAttributeValue("rel", "stylesheet"); 1130*04fd306cSNickeau $tagAttributes->removeOutputAttributeIfPresent("as"); 1131*04fd306cSNickeau } 1132*04fd306cSNickeau } 1133*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1134*04fd306cSNickeau // rel or as was not found 1135*04fd306cSNickeau } 1136*04fd306cSNickeau } 1137*04fd306cSNickeau $xhtmlContent = $tagAttributes->toHtmlEnterTag($htmlElement); 1138*04fd306cSNickeau 1139*04fd306cSNickeau try { 1140*04fd306cSNickeau $xhtmlContent .= $tagAttributes->getInnerText(); 1141*04fd306cSNickeau } catch (ExceptionNotFound $e) { 1142*04fd306cSNickeau // ok 1143*04fd306cSNickeau } 1144*04fd306cSNickeau $xhtmlContent .= "</$htmlElement>"; 1145*04fd306cSNickeau return $xhtmlContent; 1146*04fd306cSNickeau } 1147*04fd306cSNickeau 1148*04fd306cSNickeau /** 1149*04fd306cSNickeau * If is not a wiki path 1150*04fd306cSNickeau * It can't be served 1151*04fd306cSNickeau * 1152*04fd306cSNickeau * Example from theming, ... 1153*04fd306cSNickeau */ 1154*04fd306cSNickeau private function hasLocalUrl(): bool 1155*04fd306cSNickeau { 1156*04fd306cSNickeau try { 1157*04fd306cSNickeau $this->getLocalUrl(); 1158*04fd306cSNickeau return true; 1159*04fd306cSNickeau } catch (\Exception $e) { 1160*04fd306cSNickeau return false; 1161*04fd306cSNickeau } 1162*04fd306cSNickeau } 1163*04fd306cSNickeau 1164*04fd306cSNickeau /** 1165*04fd306cSNickeau * The format 1166*04fd306cSNickeau * for javascript as specified by [rollup](https://rollupjs.org/configuration-options/#output-format) 1167*04fd306cSNickeau * @param string $format 1168*04fd306cSNickeau * @return Snippet 1169*04fd306cSNickeau */ 1170*04fd306cSNickeau public function setFormat(string $format): Snippet 1171*04fd306cSNickeau { 1172*04fd306cSNickeau $this->format = $format; 1173*04fd306cSNickeau return $this; 1174*04fd306cSNickeau } 1175*04fd306cSNickeau 1176*04fd306cSNickeau /** 1177*04fd306cSNickeau * Retrieve the content and wrap it if necessary 1178*04fd306cSNickeau * to define the execution time 1179*04fd306cSNickeau * (ie there is no `defer` option for inline html 1180*04fd306cSNickeau * @throws ExceptionNotFound 1181*04fd306cSNickeau */ 1182*04fd306cSNickeau private function getInnerHtml(): string 1183*04fd306cSNickeau { 1184*04fd306cSNickeau $internal = $this->getInternalInlineAndFileContent(); 1185*04fd306cSNickeau if ( 1186*04fd306cSNickeau $this->getExtension() === self::EXTENSION_JS 1187*04fd306cSNickeau && $this->format === self::IIFE_FORMAT 1188*04fd306cSNickeau && $this->getCritical() === false 1189*04fd306cSNickeau ) { 1190*04fd306cSNickeau $internal = <<<EOF 1191*04fd306cSNickeauwindow.addEventListener('load', function () { $internal }); 1192*04fd306cSNickeauEOF; 1193*04fd306cSNickeau } 1194*04fd306cSNickeau return $internal; 1195*04fd306cSNickeau } 1196*04fd306cSNickeau 1197*04fd306cSNickeau public function getFormat(): string 1198*04fd306cSNickeau { 1199*04fd306cSNickeau return $this->format; 1200*04fd306cSNickeau } 1201*04fd306cSNickeau 1202*04fd306cSNickeau 120337748cd8SNickeau} 1204