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 1604fd306cSNickeauuse action_plugin_combo_docustom; 1704fd306cSNickeauuse ComboStrap\TagAttribute\StyleAttribute; 1804fd306cSNickeauuse ComboStrap\Web\Url; 19c3437056SNickeauuse JsonSerializable; 2004fd306cSNickeauuse 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 * 3204fd306cSNickeau * A snippet identifier is a {@link Snippet::getLocalUrl() local file} 3304fd306cSNickeau * * if there is content defined, it will be an {@link Snippet::hasInlineContent() inline} 3404fd306cSNickeau * * if not, it will be the local file with the {@link Snippet::getLocalUrl()} 3504fd306cSNickeau * * if not found or if the usage of the cdn is required, the {@link Snippet::getRemoteUrl() url} is used 3604fd306cSNickeau * 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 5504fd306cSNickeau const JSON_URI_PROPERTY = "uri"; // internal uri 5604fd306cSNickeau 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 /** 6404fd306cSNickeau * Not all snippet comes from a component markup 6504fd306cSNickeau * * a menu item may want to add a snippet on a dynamic page 6604fd306cSNickeau * * a snippet may be added just from the head html meta (for anaytics purpose) 6704fd306cSNickeau * * the global css variables 6804fd306cSNickeau * TODO: it should be migrated to the {@link TemplateForWebPage}, ie the request scope is the template scope 6904fd306cSNickeau * has, these is this object that creates pages 704cadd4f8SNickeau */ 7104fd306cSNickeau const REQUEST_SCOPE = "request"; 7204fd306cSNickeau 7304fd306cSNickeau const SLOT_SCOPE = "slot"; 7404fd306cSNickeau const ALL_SCOPE = "all"; 7504fd306cSNickeau public const COMBO_POPOVER = "combo-popover"; 7604fd306cSNickeau const CANONICAL = "snippet"; 7704fd306cSNickeau public const STYLE_TAG = "style"; 7804fd306cSNickeau public const SCRIPT_TAG = "script"; 7904fd306cSNickeau public const LINK_TAG = "link"; 8004fd306cSNickeau /** 8104fd306cSNickeau * Use CDN for local stored library 8204fd306cSNickeau */ 8304fd306cSNickeau public const CONF_USE_CDN = "useCDN"; 844cadd4f8SNickeau 854cadd4f8SNickeau /** 8604fd306cSNickeau * Where to find the file in the combo resources 8704fd306cSNickeau * if any in wiki path syntax 884cadd4f8SNickeau */ 8904fd306cSNickeau public const LIBRARY_BASE = ':library'; // external script, combo library 9004fd306cSNickeau public const SNIPPET_BASE = ":snippet"; // quick internal snippet 9104fd306cSNickeau public const CONF_USE_CDN_DEFAULT = 1; 924cadd4f8SNickeau 9304fd306cSNickeau /** 9404fd306cSNickeau * With a raw format, we do nothing 9504fd306cSNickeau * We take it without any questions 9604fd306cSNickeau */ 9704fd306cSNickeau const RAW_FORMAT = "raw"; 9804fd306cSNickeau /** 9904fd306cSNickeau * With a iife, if the javascript snippet is not critical 10004fd306cSNickeau * It will be wrapped to execute after page load 10104fd306cSNickeau */ 10204fd306cSNickeau const IIFE_FORMAT = "iife"; 10304fd306cSNickeau /** 10404fd306cSNickeau * Javascript es module 10504fd306cSNickeau */ 10604fd306cSNickeau const ES_FORMAT = "es"; 10704fd306cSNickeau /** 10804fd306cSNickeau * Umd module 10904fd306cSNickeau */ 11004fd306cSNickeau const UMD_FORMAT = "umd"; 11104fd306cSNickeau const JSON_FORMAT_PROPERTY = "format"; 11204fd306cSNickeau const DEFAULT_FORMAT = self::RAW_FORMAT; 1134cadd4f8SNickeau 11437748cd8SNickeau 11537748cd8SNickeau /** 11637748cd8SNickeau * @var bool 11737748cd8SNickeau */ 11804fd306cSNickeau private bool $critical; 11937748cd8SNickeau 12037748cd8SNickeau /** 12137748cd8SNickeau * @var string the text script / style (may be null if it's an external resources) 12237748cd8SNickeau */ 12304fd306cSNickeau private string $inlineContent; 1244cadd4f8SNickeau 12504fd306cSNickeau private Url $remoteUrl; 1264cadd4f8SNickeau 12704fd306cSNickeau private string $integrity; 12804fd306cSNickeau 12904fd306cSNickeau private array $htmlAttributes; 13004fd306cSNickeau 1314cadd4f8SNickeau 1324cadd4f8SNickeau /** 1334cadd4f8SNickeau * @var array - the slots that needs this snippet (as key to get only one snippet by scope) 13404fd306cSNickeau * 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 14204fd306cSNickeau * * or {@link Snippet::REQUEST_SCOPE} - never cached 1434cadd4f8SNickeau */ 14404fd306cSNickeau private $elements; 14504fd306cSNickeau 1464cadd4f8SNickeau /** 1474cadd4f8SNickeau * @var bool run as soon as possible 1484cadd4f8SNickeau */ 14904fd306cSNickeau private bool $async; 15004fd306cSNickeau private Path $path; 15104fd306cSNickeau private string $componentId; 15237748cd8SNickeau 15337748cd8SNickeau /** 15404fd306cSNickeau * @var bool a property to track if a snippet has already been asked in a html output 15504fd306cSNickeau * (ie with a to function such as {@link Snippet::toTagAttributes()} or {@link Snippet::toDokuWikiArray()} 15604fd306cSNickeau * We use it to not delete the state of {@link ExecutionContext} in order to check the created snippet 15704fd306cSNickeau * during an execution 15804fd306cSNickeau * 15904fd306cSNickeau * The positive side effect is that even if the snippet is used in multiple markup for a page, 16004fd306cSNickeau * It will be outputted only once. 16137748cd8SNickeau */ 16204fd306cSNickeau private bool $hasHtmlOutputOccurred = false; 16304fd306cSNickeau private string $format = self::DEFAULT_FORMAT; 16437748cd8SNickeau 16504fd306cSNickeau /** 16604fd306cSNickeau * @param Path $path - path mandatory because it's the path of fetch and it's the storage format 16704fd306cSNickeau * use {@link Snippet::getOrCreateFromContext()} 16804fd306cSNickeau */ 16904fd306cSNickeau private function __construct(Path $path) 17037748cd8SNickeau { 17104fd306cSNickeau $this->path = $path; 17237748cd8SNickeau } 17337748cd8SNickeau 17437748cd8SNickeau 17537748cd8SNickeau /** 17604fd306cSNickeau * @throws ExceptionBadArgument 17737748cd8SNickeau */ 17804fd306cSNickeau public static function createCssSnippetFromComponentId($componentId): Snippet 17937748cd8SNickeau { 18004fd306cSNickeau return Snippet::createSnippetFromComponentId($componentId, self::EXTENSION_CSS); 1814cadd4f8SNickeau } 1824cadd4f8SNickeau 18304fd306cSNickeau /** 18404fd306cSNickeau * @throws ExceptionBadArgument 18504fd306cSNickeau */ 18604fd306cSNickeau public static function createSnippetFromComponentId($componentId, $type): Snippet 1874cadd4f8SNickeau { 18804fd306cSNickeau $path = self::getInternalPathFromNameAndExtension($componentId, $type); 18904fd306cSNickeau return Snippet::createSnippetFromPath($path) 19004fd306cSNickeau ->setComponentId($componentId); 19104fd306cSNickeau } 19204fd306cSNickeau 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 19704fd306cSNickeau * @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) 19804fd306cSNickeau * @param string $extension - {@link Snippet::EXTENSION_CSS css} or {@link Snippet::EXTENSION_JS js} 19904fd306cSNickeau * @return Snippet 2004cadd4f8SNickeau */ 20104fd306cSNickeau public static function getOrCreateFromComponentId(string $componentId, string $extension): Snippet 20204fd306cSNickeau { 20304fd306cSNickeau 20404fd306cSNickeau $snippetPath = self::getInternalPathFromNameAndExtension($componentId, $extension); 20504fd306cSNickeau return self::getOrCreateFromContext($snippetPath) 20604fd306cSNickeau ->setComponentId($componentId); 20704fd306cSNickeau 2084cadd4f8SNickeau } 20904fd306cSNickeau 21004fd306cSNickeau 21104fd306cSNickeau /** 21204fd306cSNickeau * 21304fd306cSNickeau * 21404fd306cSNickeau * The order is the order where they were added/created. 21504fd306cSNickeau * 21604fd306cSNickeau * The internal script may be dependent on the external javascript 21704fd306cSNickeau * and vice-versa (for instance, Math-Jax library is dependent 21804fd306cSNickeau * on the config that is an internal inline script) 21904fd306cSNickeau * 22004fd306cSNickeau * @return Snippet[] 22104fd306cSNickeau * 22204fd306cSNickeau */ 22304fd306cSNickeau public static function getSnippets(): array 22404fd306cSNickeau { 22504fd306cSNickeau try { 22604fd306cSNickeau return ExecutionContext::getActualOrCreateFromEnv()->getRuntimeObject(self::CANONICAL); 22704fd306cSNickeau } catch (ExceptionNotFound $e) { 22804fd306cSNickeau return []; 2294cadd4f8SNickeau } 2304cadd4f8SNickeau } 23104fd306cSNickeau 23204fd306cSNickeau 23304fd306cSNickeau /** 23404fd306cSNickeau * @param WikiPath $path - a local path of the snippet (if the path does not exist, a remote url should be given) 23504fd306cSNickeau * @return Snippet 23604fd306cSNickeau */ 23704fd306cSNickeau public static function createSnippet(Path $path): Snippet 23804fd306cSNickeau { 23904fd306cSNickeau return new Snippet($path); 2404cadd4f8SNickeau } 24104fd306cSNickeau 24204fd306cSNickeau 24304fd306cSNickeau /** 24404fd306cSNickeau * @param $snippetId - a logical id 24504fd306cSNickeau * @return string - the class 24604fd306cSNickeau * See also {@link Snippet::getClass()} function 24704fd306cSNickeau */ 24804fd306cSNickeau public static function getClassFromComponentId($snippetId): string 24904fd306cSNickeau { 25004fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet-" . $snippetId); 25104fd306cSNickeau } 25204fd306cSNickeau 25304fd306cSNickeau /** 25404fd306cSNickeau * @throws ExceptionBadArgument 25504fd306cSNickeau */ 25604fd306cSNickeau public static function createSnippetFromPath(WikiPath $path): Snippet 25704fd306cSNickeau { 25804fd306cSNickeau return new Snippet($path); 25904fd306cSNickeau } 26004fd306cSNickeau 26104fd306cSNickeau 26204fd306cSNickeau /** 26304fd306cSNickeau * @param Path $localSnippetPath - the path is the snippet identifier (it's not mandatory that the snippet is locally available 26404fd306cSNickeau * but if it's, it permits to work without any connection by setting the {@link Snippet::CONF_USE_CDN cdn} to off 26504fd306cSNickeau * @return Snippet 26604fd306cSNickeau */ 26704fd306cSNickeau public static function getOrCreateFromContext(Path $localSnippetPath): Snippet 26804fd306cSNickeau { 26904fd306cSNickeau 27004fd306cSNickeau $executionContext = ExecutionContext::getActualOrCreateFromEnv(); 27104fd306cSNickeau try { 27204fd306cSNickeau $snippets = &$executionContext->getRuntimeObject(self::CANONICAL); 27304fd306cSNickeau } catch (ExceptionNotFound $e) { 27404fd306cSNickeau $snippets = []; 27504fd306cSNickeau $executionContext->setRuntimeObject(self::CANONICAL, $snippets); 27604fd306cSNickeau } 27704fd306cSNickeau $snippetGuid = $localSnippetPath->toUriString(); 27804fd306cSNickeau $snippet = &$snippets[$snippetGuid]; 2794cadd4f8SNickeau if ($snippet === null) { 28004fd306cSNickeau $snippet = self::createSnippet($localSnippetPath); 28104fd306cSNickeau /** 28204fd306cSNickeau * 28304fd306cSNickeau * The order is the order where they were added/created. 28404fd306cSNickeau * 28504fd306cSNickeau * The internal script may be dependent on the external javascript 28604fd306cSNickeau * and vice-versa (for instance, Math-Jax library is dependent 28704fd306cSNickeau * on the config that is an internal inline script) 28804fd306cSNickeau * 28904fd306cSNickeau */ 29004fd306cSNickeau $snippets[$snippetGuid] = $snippet; 2914cadd4f8SNickeau } 29204fd306cSNickeau 29304fd306cSNickeau try { 29404fd306cSNickeau $executingFetcher = $executionContext 29504fd306cSNickeau ->getExecutingMarkupHandler(); 29604fd306cSNickeau /** 29704fd306cSNickeau * New way 29804fd306cSNickeau */ 29904fd306cSNickeau $executingFetcher->addSnippet($snippet); 30004fd306cSNickeau try { 30104fd306cSNickeau /** 30204fd306cSNickeau * Old way 30304fd306cSNickeau * @deprecated 30404fd306cSNickeau * but still used to store the snippets 30504fd306cSNickeau */ 30604fd306cSNickeau $wikiId = $executingFetcher->getSourcePath()->toWikiPath()->getWikiId(); 30704fd306cSNickeau $snippet->addElement($wikiId); 30804fd306cSNickeau } catch (ExceptionCast $e) { 30904fd306cSNickeau // not a wiki path 31004fd306cSNickeau } catch (ExceptionNotFound $e) { 31104fd306cSNickeau /** 31204fd306cSNickeau * String/dynamic run 31304fd306cSNickeau * (Example via an {@link \syntax_plugin_combo_iterator}) 31404fd306cSNickeau * The fetcher should have then a parent 31504fd306cSNickeau */ 31604fd306cSNickeau try { 31704fd306cSNickeau $wikiId = $executionContext->getExecutingParentMarkupHandler()->getSourcePath()->toWikiPath()->getWikiId(); 31804fd306cSNickeau $snippet->addElement($wikiId); 31904fd306cSNickeau } catch (ExceptionCast $e) { 32004fd306cSNickeau // not a wiki path 32104fd306cSNickeau } catch (ExceptionNotFound $e) { 32204fd306cSNickeau // no parent found 32304fd306cSNickeau } 32404fd306cSNickeau 32504fd306cSNickeau } 32604fd306cSNickeau } catch (ExceptionNotFound $e) { 32704fd306cSNickeau /** 32804fd306cSNickeau * admin page, page scope or theme is not used 32904fd306cSNickeau * This snippets are not due to the markup 33004fd306cSNickeau */ 33104fd306cSNickeau try { 33204fd306cSNickeau $executingId = $executionContext->getExecutingWikiId(); 33304fd306cSNickeau $snippet->addElement($executingId); 33404fd306cSNickeau } catch (ExceptionNotFound $e) { 33504fd306cSNickeau $snippet->addElement(Snippet::REQUEST_SCOPE); 33604fd306cSNickeau } 33704fd306cSNickeau } 33804fd306cSNickeau 3394cadd4f8SNickeau return $snippet; 3404cadd4f8SNickeau 3414cadd4f8SNickeau } 3424cadd4f8SNickeau 34304fd306cSNickeau /** 34404fd306cSNickeau * Create a snippet from the ComboDrive 34504fd306cSNickeau * @throws ExceptionBadArgument 34604fd306cSNickeau */ 34704fd306cSNickeau public static function createComboSnippet(string $wikiPath): Snippet 3484cadd4f8SNickeau { 34904fd306cSNickeau $wikiPathObject = WikiPath::createComboResource($wikiPath); 35004fd306cSNickeau return self::createSnippetFromPath($wikiPathObject); 3514cadd4f8SNickeau } 3524cadd4f8SNickeau 3534cadd4f8SNickeau /** 35404fd306cSNickeau * @param string $wikiPath - the wiki path should be absolute relative to the library namespace 35504fd306cSNickeau * @return Snippet 35604fd306cSNickeau * 35704fd306cSNickeau * Example: `:bootstrap:4.5.0:bootstrap.min.css` 3584cadd4f8SNickeau */ 35904fd306cSNickeau public static function getOrCreateFromLibraryNamespace(string $wikiPath): Snippet 3604cadd4f8SNickeau { 36104fd306cSNickeau $wikiPathObject = WikiPath::createComboResource(self::LIBRARY_BASE . $wikiPath); 36204fd306cSNickeau return self::getOrCreateFromContext($wikiPathObject); 3634cadd4f8SNickeau } 36404fd306cSNickeau 36504fd306cSNickeau /** 36604fd306cSNickeau * An utility class to create a snippet from a remote url 36704fd306cSNickeau * 36804fd306cSNickeau * If you want to be able to serve the library locally, you 36904fd306cSNickeau * should create the snippet via the {@link Snippet::getOrCreateFromLibraryNamespace() local path} 37004fd306cSNickeau * and set {@link Snippet::setRemoteUrl() remote url} 37104fd306cSNickeau * 37204fd306cSNickeau * @throws ExceptionBadArgument - if the url does not have a file name 37304fd306cSNickeau */ 37404fd306cSNickeau public static function getOrCreateFromRemoteUrl(Url $url): Snippet 37504fd306cSNickeau { 37604fd306cSNickeau 37704fd306cSNickeau try { 37804fd306cSNickeau $libraryName = $url->getLastName(); 37904fd306cSNickeau } catch (ExceptionNotFound $e) { 38004fd306cSNickeau $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."; 38104fd306cSNickeau throw new ExceptionBadArgument($messageFormat); 38204fd306cSNickeau } 38304fd306cSNickeau /** 38404fd306cSNickeau * The file generally does not exists 38504fd306cSNickeau */ 38604fd306cSNickeau $localPath = WikiPath::createComboResource(Snippet::LIBRARY_BASE . ":$libraryName"); 38704fd306cSNickeau try { 38804fd306cSNickeau $localPath->getExtension(); 38904fd306cSNickeau } catch (ExceptionNotFound $e) { 39004fd306cSNickeau $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. "; 39104fd306cSNickeau throw new ExceptionBadArgument($messageFormat); 39204fd306cSNickeau } 39304fd306cSNickeau return self::getOrCreateFromContext($localPath) 39404fd306cSNickeau ->setRemoteUrl($url); 39504fd306cSNickeau 39604fd306cSNickeau 39704fd306cSNickeau } 39804fd306cSNickeau 39904fd306cSNickeau /** 40004fd306cSNickeau * @throws ExceptionBadArgument 40104fd306cSNickeau */ 40204fd306cSNickeau public static function createJavascriptSnippetFromComponentId(string $componentId): Snippet 40304fd306cSNickeau { 40404fd306cSNickeau 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 /** 45304fd306cSNickeau * The content that was set via a string (It should be used 45404fd306cSNickeau * for dynamic content, that's why it's called dynamic) 45537748cd8SNickeau * @return string 45604fd306cSNickeau * @throws ExceptionNotFound 45737748cd8SNickeau */ 45804fd306cSNickeau public function getInternalDynamicContent(): string 45937748cd8SNickeau { 46004fd306cSNickeau if (!isset($this->inlineContent)) { 46104fd306cSNickeau throw new ExceptionNotFound("No inline content set"); 46204fd306cSNickeau } 4634cadd4f8SNickeau return $this->inlineContent; 4644cadd4f8SNickeau } 4654cadd4f8SNickeau 4664cadd4f8SNickeau /** 4674cadd4f8SNickeau * @return string|null 46804fd306cSNickeau * @throws ExceptionNotFound - if not found 4694cadd4f8SNickeau */ 47004fd306cSNickeau public function getInternalFileContent(): string 4714cadd4f8SNickeau { 47204fd306cSNickeau $path = $this->getPath(); 4734cadd4f8SNickeau return FileSystems::getContent($path); 4744cadd4f8SNickeau } 4754cadd4f8SNickeau 47604fd306cSNickeau public function getPath(): Path 4774cadd4f8SNickeau { 47804fd306cSNickeau return $this->path; 47904fd306cSNickeau } 48004fd306cSNickeau 48104fd306cSNickeau public static function getInternalPathFromNameAndExtension($name, $extension): WikiPath 48204fd306cSNickeau { 48304fd306cSNickeau 48404fd306cSNickeau switch ($extension) { 4854cadd4f8SNickeau case self::EXTENSION_CSS: 4864cadd4f8SNickeau $extension = "css"; 48704fd306cSNickeau return TemplateEngine::createFromContext() 48804fd306cSNickeau ->getComponentStylePathByName(strtolower($name) . ".$extension"); 4894cadd4f8SNickeau case self::EXTENSION_JS: 4904cadd4f8SNickeau $extension = "js"; 4914cadd4f8SNickeau $subDirectory = "js"; 49204fd306cSNickeau return WikiPath::createComboResource(self::SNIPPET_BASE) 4934cadd4f8SNickeau ->resolve($subDirectory) 49404fd306cSNickeau ->resolve(strtolower($name) . ".$extension"); 49504fd306cSNickeau default: 49604fd306cSNickeau $message = "Unknown snippet type ($extension)"; 49704fd306cSNickeau throw new ExceptionRuntimeInternal($message); 49804fd306cSNickeau } 49904fd306cSNickeau 50037748cd8SNickeau } 50137748cd8SNickeau 5024cadd4f8SNickeau public function hasSlot($slot): bool 50337748cd8SNickeau { 50404fd306cSNickeau if ($this->elements === null) { 5054cadd4f8SNickeau return false; 50637748cd8SNickeau } 50704fd306cSNickeau return key_exists($slot, $this->elements); 50837748cd8SNickeau } 50937748cd8SNickeau 51037748cd8SNickeau public function __toString() 51137748cd8SNickeau { 51204fd306cSNickeau return $this->path->toUriString(); 51337748cd8SNickeau } 51437748cd8SNickeau 515c3437056SNickeau public function getCritical(): bool 51637748cd8SNickeau { 51704fd306cSNickeau 51804fd306cSNickeau if (isset($this->critical)) { 51904fd306cSNickeau return $this->critical; 52004fd306cSNickeau } 52104fd306cSNickeau try { 52204fd306cSNickeau 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 } 52704fd306cSNickeau } catch (ExceptionNotFound $e) { 52804fd306cSNickeau // no path extension 529c3437056SNickeau } 53004fd306cSNickeau return false; 53104fd306cSNickeau 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 */ 54604fd306cSNickeau try { 54704fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet-" . $this->getComponentId()); 54804fd306cSNickeau } catch (ExceptionNotFound $e) { 54904fd306cSNickeau LogUtility::internalError("A component id was not found for the snippet ($this)", self::CANONICAL); 55004fd306cSNickeau return StyleAttribute::addComboStrapSuffix("snippet"); 55104fd306cSNickeau } 55237748cd8SNickeau 55337748cd8SNickeau } 55437748cd8SNickeau 55504fd306cSNickeau 55637748cd8SNickeau /** 55704fd306cSNickeau * @return string - the component name identifier 55804fd306cSNickeau * All snippet with this component id are from the same component 55904fd306cSNickeau * @throws ExceptionNotFound 56037748cd8SNickeau */ 56104fd306cSNickeau public function getComponentId(): string 56237748cd8SNickeau { 56304fd306cSNickeau if (isset($this->componentId)) { 56404fd306cSNickeau return $this->componentId; 56537748cd8SNickeau } 56604fd306cSNickeau 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 /** 57704fd306cSNickeau * @throws ExceptionCompile 578c3437056SNickeau */ 579c3437056SNickeau public static function createFromJson($array): Snippet 580c3437056SNickeau { 5814cadd4f8SNickeau 582*70bbd7f1Sgerardnico $uri = $array[self::JSON_URI_PROPERTY] ?? null; 58304fd306cSNickeau if ($uri === null) { 58404fd306cSNickeau throw new ExceptionCompile("The snippet uri property was not found in the json array"); 58504fd306cSNickeau } 58604fd306cSNickeau 58704fd306cSNickeau $wikiPath = FileSystems::createPathFromUri($uri); 58804fd306cSNickeau $snippet = Snippet::getOrCreateFromContext($wikiPath); 58904fd306cSNickeau 590*70bbd7f1Sgerardnico $componentName = $array[self::JSON_COMPONENT_PROPERTY] ?? null; 59104fd306cSNickeau if ($componentName !== null) { 59204fd306cSNickeau $snippet->setComponentId($componentName); 59304fd306cSNickeau } 5944cadd4f8SNickeau 595*70bbd7f1Sgerardnico $critical = $array[self::JSON_CRITICAL_PROPERTY] ?? null; 596c3437056SNickeau if ($critical !== null) { 597c3437056SNickeau $snippet->setCritical($critical); 598c3437056SNickeau } 599c3437056SNickeau 600*70bbd7f1Sgerardnico $async = $array[self::JSON_ASYNC_PROPERTY] ?? null; 6014cadd4f8SNickeau if ($async !== null) { 6024cadd4f8SNickeau $snippet->setDoesManipulateTheDomOnRun($async); 6034cadd4f8SNickeau } 6044cadd4f8SNickeau 605*70bbd7f1Sgerardnico $format = $array[self::JSON_FORMAT_PROPERTY] ?? null; 60604fd306cSNickeau if ($format !== null) { 60704fd306cSNickeau $snippet->setFormat($format); 60804fd306cSNickeau } 60904fd306cSNickeau 610*70bbd7f1Sgerardnico $content = $array[self::JSON_CONTENT_PROPERTY] ?? null; 611c3437056SNickeau if ($content !== null) { 6124cadd4f8SNickeau $snippet->setInlineContent($content); 613c3437056SNickeau } 614c3437056SNickeau 615*70bbd7f1Sgerardnico $attributes = $array[self::JSON_HTML_ATTRIBUTES_PROPERTY] ?? null; 6164cadd4f8SNickeau if ($attributes !== null) { 6174cadd4f8SNickeau foreach ($attributes as $name => $value) { 6184cadd4f8SNickeau $snippet->addHtmlAttribute($name, $value); 619c3437056SNickeau } 6204cadd4f8SNickeau } 6214cadd4f8SNickeau 622*70bbd7f1Sgerardnico $integrity = $array[self::JSON_INTEGRITY_PROPERTY] ?? null; 6234cadd4f8SNickeau if ($integrity !== null) { 6244cadd4f8SNickeau $snippet->setIntegrity($integrity); 6254cadd4f8SNickeau } 6264cadd4f8SNickeau 627*70bbd7f1Sgerardnico $remoteUrl = $array[self::JSON_URL_PROPERTY] ?? null; 62804fd306cSNickeau if ($remoteUrl !== null) { 62904fd306cSNickeau $snippet->setRemoteUrl(Url::createFromString($remoteUrl)); 63004fd306cSNickeau } 63104fd306cSNickeau 632c3437056SNickeau return $snippet; 633c3437056SNickeau 634c3437056SNickeau } 6354cadd4f8SNickeau 6364cadd4f8SNickeau public function getExtension() 6374cadd4f8SNickeau { 63804fd306cSNickeau return $this->path->getExtension(); 6394cadd4f8SNickeau } 6404cadd4f8SNickeau 6414cadd4f8SNickeau public function setIntegrity(?string $integrity): Snippet 6424cadd4f8SNickeau { 64304fd306cSNickeau if ($integrity === null) { 64404fd306cSNickeau return $this; 64504fd306cSNickeau } 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 65604fd306cSNickeau public function addElement(string $element): Snippet 6574cadd4f8SNickeau { 65804fd306cSNickeau $this->elements[$element] = 1; 6594cadd4f8SNickeau return $this; 6604cadd4f8SNickeau } 6614cadd4f8SNickeau 66204fd306cSNickeau public function useLocalUrl(): bool 6634cadd4f8SNickeau { 66404fd306cSNickeau 66504fd306cSNickeau /** 66604fd306cSNickeau * use cdn is on and there is a remote url 66704fd306cSNickeau */ 66804fd306cSNickeau $useCdn = ExecutionContext::getActualOrCreateFromEnv()->getConfValue(self::CONF_USE_CDN, self::CONF_USE_CDN_DEFAULT) === 1; 66904fd306cSNickeau if ($useCdn && isset($this->remoteUrl)) { 67004fd306cSNickeau return false; 6714cadd4f8SNickeau } 6724cadd4f8SNickeau 67304fd306cSNickeau /** 67404fd306cSNickeau * use cdn is off and there is a file 67504fd306cSNickeau */ 67604fd306cSNickeau $fileExists = FileSystems::exists($this->path); 67704fd306cSNickeau if ($fileExists) { 67804fd306cSNickeau return true; 6794cadd4f8SNickeau } 6804cadd4f8SNickeau 68104fd306cSNickeau /** 68204fd306cSNickeau * Use cdn is off and there is a remote url 68304fd306cSNickeau */ 68404fd306cSNickeau if (isset($this->remoteUrl)) { 68504fd306cSNickeau return false; 68604fd306cSNickeau } 68704fd306cSNickeau 68804fd306cSNickeau /** 68904fd306cSNickeau * 69004fd306cSNickeau * This is a inline script (no local file then) 69104fd306cSNickeau * 69204fd306cSNickeau * We default to the local url that will return an error 69304fd306cSNickeau * when fetched 69404fd306cSNickeau */ 69504fd306cSNickeau if (!$this->shouldBeInlined()) { 69604fd306cSNickeau 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."); 69704fd306cSNickeau } 69804fd306cSNickeau return false; 69904fd306cSNickeau 70004fd306cSNickeau } 70104fd306cSNickeau 70204fd306cSNickeau /** 70304fd306cSNickeau * 70404fd306cSNickeau */ 70504fd306cSNickeau public function getLocalUrl(): Url 70604fd306cSNickeau { 70704fd306cSNickeau try { 70804fd306cSNickeau $path = WikiPath::createFromPathObject($this->path); 70904fd306cSNickeau return FetcherRawLocalPath::createFromPath($path)->getFetchUrl(); 71004fd306cSNickeau } catch (ExceptionBadArgument $e) { 71104fd306cSNickeau throw new ExceptionRuntimeInternal("The local url should ne asked. use (hasLocalUrl) before calling this function", self::CANONICAL, $e); 71204fd306cSNickeau } 71304fd306cSNickeau 71404fd306cSNickeau } 71504fd306cSNickeau 71604fd306cSNickeau 71704fd306cSNickeau /** 71804fd306cSNickeau * @throws ExceptionNotFound 71904fd306cSNickeau */ 72004fd306cSNickeau public function getRemoteUrl(): Url 72104fd306cSNickeau { 72204fd306cSNickeau if (!isset($this->remoteUrl)) { 72304fd306cSNickeau throw new ExceptionNotFound("No remote url found"); 72404fd306cSNickeau } 72504fd306cSNickeau return $this->remoteUrl; 72604fd306cSNickeau } 72704fd306cSNickeau 72804fd306cSNickeau /** 72904fd306cSNickeau * @throws ExceptionNotFound 73004fd306cSNickeau */ 7314cadd4f8SNickeau public function getIntegrity(): ?string 7324cadd4f8SNickeau { 73304fd306cSNickeau if (!isset($this->integrity)) { 73404fd306cSNickeau throw new ExceptionNotFound("No integrity"); 73504fd306cSNickeau } 7364cadd4f8SNickeau return $this->integrity; 7374cadd4f8SNickeau } 7384cadd4f8SNickeau 73904fd306cSNickeau public function getHtmlAttributes(): array 7404cadd4f8SNickeau { 74104fd306cSNickeau if (!isset($this->htmlAttributes)) { 74204fd306cSNickeau return []; 74304fd306cSNickeau } 7444cadd4f8SNickeau return $this->htmlAttributes; 7454cadd4f8SNickeau } 7464cadd4f8SNickeau 74704fd306cSNickeau 74804fd306cSNickeau /** 74904fd306cSNickeau * @throws ExceptionNotFound 75004fd306cSNickeau */ 75104fd306cSNickeau public function getInternalInlineAndFileContent(): string 7524cadd4f8SNickeau { 7534cadd4f8SNickeau $totalContent = null; 75404fd306cSNickeau try { 75504fd306cSNickeau $totalContent = $this->getInternalFileContent(); 75604fd306cSNickeau } catch (ExceptionNotFound $e) { 75704fd306cSNickeau // no 7584cadd4f8SNickeau } 7594cadd4f8SNickeau 76004fd306cSNickeau try { 76104fd306cSNickeau $totalContent .= $this->getInternalDynamicContent(); 76204fd306cSNickeau } catch (ExceptionNotFound $e) { 76304fd306cSNickeau // no 7644cadd4f8SNickeau } 76504fd306cSNickeau if ($totalContent === null) { 76604fd306cSNickeau throw new ExceptionNotFound("No content"); 7674cadd4f8SNickeau } 7684cadd4f8SNickeau return $totalContent; 7694cadd4f8SNickeau 7704cadd4f8SNickeau } 7714cadd4f8SNickeau 7724cadd4f8SNickeau 77304fd306cSNickeau /** 77404fd306cSNickeau */ 7754cadd4f8SNickeau public function jsonSerialize(): array 7764cadd4f8SNickeau { 77704fd306cSNickeau 7784cadd4f8SNickeau $dataToSerialize = [ 77904fd306cSNickeau self::JSON_URI_PROPERTY => $this->getPath()->toUriString(), 7804cadd4f8SNickeau ]; 78104fd306cSNickeau 78204fd306cSNickeau try { 78304fd306cSNickeau $dataToSerialize[self::JSON_COMPONENT_PROPERTY] = $this->getComponentId(); 78404fd306cSNickeau } catch (ExceptionNotFound $e) { 78504fd306cSNickeau LogUtility::internalError("The component id was not set for the snippet ($this)"); 7864cadd4f8SNickeau } 78704fd306cSNickeau 78804fd306cSNickeau if (isset($this->remoteUrl)) { 78904fd306cSNickeau $dataToSerialize[self::JSON_URL_PROPERTY] = $this->remoteUrl->toString(); 79004fd306cSNickeau } 79104fd306cSNickeau if (isset($this->integrity)) { 7924cadd4f8SNickeau $dataToSerialize[self::JSON_INTEGRITY_PROPERTY] = $this->integrity; 7934cadd4f8SNickeau } 79404fd306cSNickeau if (isset($this->critical)) { 7954cadd4f8SNickeau $dataToSerialize[self::JSON_CRITICAL_PROPERTY] = $this->critical; 7964cadd4f8SNickeau } 79704fd306cSNickeau if (isset($this->async)) { 7984cadd4f8SNickeau $dataToSerialize[self::JSON_ASYNC_PROPERTY] = $this->async; 7994cadd4f8SNickeau } 80004fd306cSNickeau if (isset($this->inlineContent)) { 8014cadd4f8SNickeau $dataToSerialize[self::JSON_CONTENT_PROPERTY] = $this->inlineContent; 8024cadd4f8SNickeau } 80304fd306cSNickeau if ($this->format !== self::DEFAULT_FORMAT) { 80404fd306cSNickeau $dataToSerialize[self::JSON_FORMAT_PROPERTY] = $this->format; 80504fd306cSNickeau } 80604fd306cSNickeau if (isset($this->htmlAttributes)) { 8074cadd4f8SNickeau $dataToSerialize[self::JSON_HTML_ATTRIBUTES_PROPERTY] = $this->htmlAttributes; 8084cadd4f8SNickeau } 8094cadd4f8SNickeau return $dataToSerialize; 8104cadd4f8SNickeau } 81104fd306cSNickeau 81204fd306cSNickeau 81304fd306cSNickeau public function hasInlineContent(): bool 81404fd306cSNickeau { 81504fd306cSNickeau return isset($this->inlineContent); 81604fd306cSNickeau } 81704fd306cSNickeau 81804fd306cSNickeau private 81904fd306cSNickeau function getMaxInlineSize() 82004fd306cSNickeau { 82104fd306cSNickeau return SiteConfig::getConfValue(SiteConfig::HTML_MAX_KB_SIZE_FOR_INLINE_ELEMENT, 2) * 1024; 82204fd306cSNickeau } 82304fd306cSNickeau 82404fd306cSNickeau /** 82504fd306cSNickeau * Returns if the internal snippet should be incorporated 82604fd306cSNickeau * in the page or not 82704fd306cSNickeau * 82804fd306cSNickeau * Requiring a lot of small javascript file adds a penalty to page load 82904fd306cSNickeau * 83004fd306cSNickeau * @return bool 83104fd306cSNickeau */ 83204fd306cSNickeau public function shouldBeInlined(): bool 83304fd306cSNickeau { 83404fd306cSNickeau /** 83504fd306cSNickeau * If there is inline content, true 83604fd306cSNickeau */ 83704fd306cSNickeau if ($this->hasInlineContent()) { 83804fd306cSNickeau return true; 83904fd306cSNickeau } 84004fd306cSNickeau 84104fd306cSNickeau /** 84204fd306cSNickeau * If the file does not exist 84304fd306cSNickeau * and that there is a remote url 84404fd306cSNickeau */ 84504fd306cSNickeau $internalPath = $this->getPath(); 84604fd306cSNickeau if (!FileSystems::exists($internalPath)) { 84704fd306cSNickeau try { 84804fd306cSNickeau $this->getRemoteUrl(); 84904fd306cSNickeau return false; 85004fd306cSNickeau } catch (ExceptionNotFound $e) { 85104fd306cSNickeau // no remote url, no file, no inline: error 85204fd306cSNickeau LogUtility::internalError("The snippet ($this) does not have content defined (the path does not exist, no inline content and no remote url)", self::CANONICAL); 85304fd306cSNickeau return true; 85404fd306cSNickeau } 85504fd306cSNickeau } 85604fd306cSNickeau 85704fd306cSNickeau /** 85804fd306cSNickeau * The file exists 85904fd306cSNickeau * If we can't serve it locally, it should be inlined 86004fd306cSNickeau */ 86104fd306cSNickeau if (!$this->hasLocalUrl()) { 86204fd306cSNickeau return true; 86304fd306cSNickeau } 86404fd306cSNickeau 86504fd306cSNickeau /** 86604fd306cSNickeau * File exists and can be served 86704fd306cSNickeau */ 86804fd306cSNickeau 86904fd306cSNickeau /** 87004fd306cSNickeau * Local Javascript Library ? 87104fd306cSNickeau */ 87204fd306cSNickeau if ($this->getExtension() === Snippet::EXTENSION_JS) { 87304fd306cSNickeau 87404fd306cSNickeau try { 87504fd306cSNickeau $lastName = $internalPath->getLastName(); 87604fd306cSNickeau } catch (ExceptionNotFound $e) { 87704fd306cSNickeau LogUtility::internalError("Every snippet should have a last name"); 87804fd306cSNickeau return false; 87904fd306cSNickeau } 88004fd306cSNickeau 88104fd306cSNickeau /** 88204fd306cSNickeau * If this is a local library don't inline 88304fd306cSNickeau * Why ? 88404fd306cSNickeau * The local combo.min.js library depends on bootstrap.min.js 88504fd306cSNickeau * If we inject it, we get `bootstrap` does not exist. 88604fd306cSNickeau * Other solution, to resolve it, we could: 88704fd306cSNickeau * * inline all javascript 88804fd306cSNickeau * * start a local server and serve the local library 88904fd306cSNickeau * * publish the local library (not really realistic if we test but yeah) 89004fd306cSNickeau */ 89104fd306cSNickeau $libraryExtension = ".min.js"; 89204fd306cSNickeau $isLibrary = substr($lastName, -strlen($libraryExtension)) === $libraryExtension; 89304fd306cSNickeau if (!$this->hasRemoteUrl() && !$isLibrary) { 89404fd306cSNickeau $alwaysInline = ExecutionContext::getActualOrCreateFromEnv() 89504fd306cSNickeau ->getConfig() 89604fd306cSNickeau ->isLocalJavascriptAlwaysInlined(); 89704fd306cSNickeau if ($alwaysInline) { 89804fd306cSNickeau return true; 89904fd306cSNickeau } 90004fd306cSNickeau } 90104fd306cSNickeau } 90204fd306cSNickeau 90304fd306cSNickeau /** 90404fd306cSNickeau * The file exists (inline if small size) 90504fd306cSNickeau */ 90604fd306cSNickeau if (FileSystems::getSize($internalPath) > $this->getMaxInlineSize()) { 90704fd306cSNickeau return false; 90804fd306cSNickeau } 90904fd306cSNickeau 91004fd306cSNickeau return true; 91104fd306cSNickeau 91204fd306cSNickeau } 91304fd306cSNickeau 91404fd306cSNickeau /** 91504fd306cSNickeau * @return array 91604fd306cSNickeau * @throws ExceptionBadArgument 91704fd306cSNickeau * @throws ExceptionBadState - an error where for instance an inline script does not have any content 91804fd306cSNickeau * @throws ExceptionCast 91904fd306cSNickeau * @throws ExceptionNotFound - an error where the source was not found 92004fd306cSNickeau */ 92104fd306cSNickeau public function toDokuWikiArray(): array 92204fd306cSNickeau { 92304fd306cSNickeau $tagAttributes = $this->toTagAttributes(); 92404fd306cSNickeau $array = $tagAttributes->toCallStackArray(); 92504fd306cSNickeau unset($array[TagAttributes::GENERATED_ID_KEY]); 92604fd306cSNickeau return $array; 92704fd306cSNickeau } 92804fd306cSNickeau 92904fd306cSNickeau 93004fd306cSNickeau /** 93104fd306cSNickeau * The HTML tag 93204fd306cSNickeau */ 93304fd306cSNickeau public function getHtmlTag(): string 93404fd306cSNickeau { 93504fd306cSNickeau $extension = $this->getExtension(); 93604fd306cSNickeau switch ($extension) { 93704fd306cSNickeau case Snippet::EXTENSION_JS: 93804fd306cSNickeau return self::SCRIPT_TAG; 93904fd306cSNickeau case Snippet::EXTENSION_CSS: 94004fd306cSNickeau if ($this->shouldBeInlined()) { 94104fd306cSNickeau return Snippet::STYLE_TAG; 94204fd306cSNickeau } else { 94304fd306cSNickeau return Snippet::LINK_TAG; 94404fd306cSNickeau } 94504fd306cSNickeau default: 94604fd306cSNickeau // it should not happen as the devs are the creator of snippet (not the user) 94704fd306cSNickeau LogUtility::internalError("The extension ($extension) is unknown", self::CANONICAL); 94804fd306cSNickeau return ""; 94904fd306cSNickeau } 95004fd306cSNickeau } 95104fd306cSNickeau 95204fd306cSNickeau public function setComponentId(string $componentId): Snippet 95304fd306cSNickeau { 95404fd306cSNickeau $this->componentId = $componentId; 95504fd306cSNickeau return $this; 95604fd306cSNickeau } 95704fd306cSNickeau 95804fd306cSNickeau public function setRemoteUrl(Url $url): Snippet 95904fd306cSNickeau { 96004fd306cSNickeau $this->remoteUrl = $url; 96104fd306cSNickeau return $this; 96204fd306cSNickeau } 96304fd306cSNickeau 96404fd306cSNickeau 96504fd306cSNickeau public function useRemoteUrl(): bool 96604fd306cSNickeau { 96704fd306cSNickeau return !$this->useLocalUrl(); 96804fd306cSNickeau } 96904fd306cSNickeau 97004fd306cSNickeau /** 97104fd306cSNickeau * @return TagAttributes 97204fd306cSNickeau * @throws ExceptionBadArgument 97304fd306cSNickeau * @throws ExceptionBadState - if no content was found 97404fd306cSNickeau * @throws ExceptionCast 97504fd306cSNickeau * @throws ExceptionNotFound - if the file was not found 97604fd306cSNickeau */ 97704fd306cSNickeau public function toTagAttributes(): TagAttributes 97804fd306cSNickeau { 97904fd306cSNickeau 98004fd306cSNickeau if ($this->hasHtmlOutputOccurred) { 98104fd306cSNickeau $message = "The snippet ($this) has already been asked. It may have been added twice to the HTML page"; 98204fd306cSNickeau if (PluginUtility::isTest()) { 98304fd306cSNickeau $message = "Error: you may be running two pages fetch in the same execution context. $message"; 98404fd306cSNickeau } 98504fd306cSNickeau LogUtility::internalError($message); 98604fd306cSNickeau } 98704fd306cSNickeau $this->hasHtmlOutputOccurred = true; 98804fd306cSNickeau 98904fd306cSNickeau $tagAttributes = TagAttributes::createFromCallStackArray($this->getHtmlAttributes()) 99004fd306cSNickeau ->addClassName($this->getClass()); 99104fd306cSNickeau $extension = $this->getExtension(); 99204fd306cSNickeau switch ($extension) { 99304fd306cSNickeau case Snippet::EXTENSION_JS: 99404fd306cSNickeau 99504fd306cSNickeau if ($this->shouldBeInlined()) { 99604fd306cSNickeau 99704fd306cSNickeau try { 99804fd306cSNickeau $tagAttributes->setInnerText($this->getInnerHtml()); 99904fd306cSNickeau return $tagAttributes; 100004fd306cSNickeau } catch (ExceptionNotFound $e) { 100104fd306cSNickeau throw new ExceptionBadState("The internal js snippet ($this) has no content. Skipped", self::CANONICAL); 100204fd306cSNickeau } 100304fd306cSNickeau 100404fd306cSNickeau } else { 100504fd306cSNickeau 100604fd306cSNickeau if ($this->useRemoteUrl()) { 100704fd306cSNickeau $fetchUrl = $this->getRemoteUrl(); 100804fd306cSNickeau } else { 100904fd306cSNickeau $fetchUrl = $this->getLocalUrl(); 101004fd306cSNickeau } 101104fd306cSNickeau 101204fd306cSNickeau /** 101304fd306cSNickeau * Dokuwiki encodes the URL in HTML format 101404fd306cSNickeau */ 101504fd306cSNickeau $tagAttributes 101604fd306cSNickeau ->addOutputAttributeValue("src", $fetchUrl->toString()) 101704fd306cSNickeau ->addOutputAttributeValue("crossorigin", "anonymous"); 101804fd306cSNickeau try { 101904fd306cSNickeau $integrity = $this->getIntegrity(); 102004fd306cSNickeau $tagAttributes->addOutputAttributeValue("integrity", $integrity); 102104fd306cSNickeau } catch (ExceptionNotFound $e) { 102204fd306cSNickeau // ok 102304fd306cSNickeau } 102404fd306cSNickeau $critical = $this->getCritical(); 102504fd306cSNickeau if (!$critical) { 102604fd306cSNickeau $tagAttributes->addBooleanOutputAttributeValue("defer"); 102704fd306cSNickeau // not async: it will run as soon as possible 102804fd306cSNickeau // the dom main not be loaded completely, the script may miss HTML dom element 102904fd306cSNickeau } 103004fd306cSNickeau return $tagAttributes; 103104fd306cSNickeau 103204fd306cSNickeau } 103304fd306cSNickeau 103404fd306cSNickeau case Snippet::EXTENSION_CSS: 103504fd306cSNickeau 103604fd306cSNickeau if ($this->shouldBeInlined()) { 103704fd306cSNickeau 103804fd306cSNickeau try { 103904fd306cSNickeau $tagAttributes->setInnerText($this->getInnerHtml()); 104004fd306cSNickeau return $tagAttributes; 104104fd306cSNickeau } catch (ExceptionNotFound $e) { 104204fd306cSNickeau throw new ExceptionNotFound("The internal css snippet ($this) has no content.", self::CANONICAL); 104304fd306cSNickeau } 104404fd306cSNickeau 104504fd306cSNickeau } else { 104604fd306cSNickeau 104704fd306cSNickeau if ($this->useRemoteUrl()) { 104804fd306cSNickeau $fetchUrl = $this->getRemoteUrl(); 104904fd306cSNickeau } else { 105004fd306cSNickeau $fetchUrl = $this->getLocalUrl(); 105104fd306cSNickeau } 105204fd306cSNickeau 105304fd306cSNickeau /** 105404fd306cSNickeau * Dokuwiki transforms/encode the href in HTML 105504fd306cSNickeau */ 105604fd306cSNickeau $tagAttributes 105704fd306cSNickeau ->addOutputAttributeValue("href", $fetchUrl->toString()) 105804fd306cSNickeau ->addOutputAttributeValue("crossorigin", "anonymous"); 105904fd306cSNickeau 106004fd306cSNickeau try { 106104fd306cSNickeau $integrity = $this->getIntegrity(); 106204fd306cSNickeau $tagAttributes->addOutputAttributeValue("integrity", $integrity); 106304fd306cSNickeau } catch (ExceptionNotFound $e) { 106404fd306cSNickeau // ok 106504fd306cSNickeau } 106604fd306cSNickeau 106704fd306cSNickeau $critical = $this->getCritical(); 106804fd306cSNickeau if (!$critical && action_plugin_combo_docustom::isThemeSystemEnabled()) { 106904fd306cSNickeau $tagAttributes 107004fd306cSNickeau ->addOutputAttributeValue("rel", "preload") 107104fd306cSNickeau ->addOutputAttributeValue('as', self::STYLE_TAG); 107204fd306cSNickeau } else { 107304fd306cSNickeau $tagAttributes->addOutputAttributeValue("rel", "stylesheet"); 107404fd306cSNickeau } 107504fd306cSNickeau 107604fd306cSNickeau return $tagAttributes; 107704fd306cSNickeau 107804fd306cSNickeau } 107904fd306cSNickeau 108004fd306cSNickeau 108104fd306cSNickeau default: 108204fd306cSNickeau throw new ExceptionBadState("The extension ($extension) is unknown", self::CANONICAL); 108304fd306cSNickeau } 108404fd306cSNickeau 108504fd306cSNickeau } 108604fd306cSNickeau 108704fd306cSNickeau /** 108804fd306cSNickeau * @return bool - yes if the function {@link Snippet::toTagAttributes()} 108904fd306cSNickeau * or {@link Snippet::toDokuWikiArray()} has been called 109004fd306cSNickeau * to prevent having the snippet two times (one in the head and one in the body) 109104fd306cSNickeau */ 109204fd306cSNickeau public function hasHtmlOutputAlreadyOccurred(): bool 109304fd306cSNickeau { 109404fd306cSNickeau 109504fd306cSNickeau return $this->hasHtmlOutputOccurred; 109604fd306cSNickeau 109704fd306cSNickeau } 109804fd306cSNickeau 109904fd306cSNickeau private function hasRemoteUrl(): bool 110004fd306cSNickeau { 110104fd306cSNickeau try { 110204fd306cSNickeau $this->getRemoteUrl(); 110304fd306cSNickeau return true; 110404fd306cSNickeau } catch (ExceptionNotFound $e) { 110504fd306cSNickeau return false; 110604fd306cSNickeau } 110704fd306cSNickeau } 110804fd306cSNickeau 110904fd306cSNickeau public function toXhtml(): string 111004fd306cSNickeau { 111104fd306cSNickeau try { 111204fd306cSNickeau $tagAttributes = $this->toTagAttributes(); 111304fd306cSNickeau } catch (\Exception $e) { 111404fd306cSNickeau throw new ExceptionRuntimeInternal("We couldn't output the snippet ($this). Error: {$e->getMessage()}", self::CANONICAL, $e); 111504fd306cSNickeau } 111604fd306cSNickeau $htmlElement = $this->getHtmlTag(); 111704fd306cSNickeau /** 111804fd306cSNickeau * This code runs in editing mode 111904fd306cSNickeau * or if the template is not strap 112004fd306cSNickeau * No preload is then supported 112104fd306cSNickeau */ 112204fd306cSNickeau if ($htmlElement === "link") { 112304fd306cSNickeau try { 112404fd306cSNickeau $relValue = $tagAttributes->getOutputAttribute("rel"); 112504fd306cSNickeau $relAs = $tagAttributes->getOutputAttribute("as"); 112604fd306cSNickeau if ($relValue === "preload") { 112704fd306cSNickeau if ($relAs === "style") { 112804fd306cSNickeau $tagAttributes->removeOutputAttributeIfPresent("rel"); 112904fd306cSNickeau $tagAttributes->addOutputAttributeValue("rel", "stylesheet"); 113004fd306cSNickeau $tagAttributes->removeOutputAttributeIfPresent("as"); 113104fd306cSNickeau } 113204fd306cSNickeau } 113304fd306cSNickeau } catch (ExceptionNotFound $e) { 113404fd306cSNickeau // rel or as was not found 113504fd306cSNickeau } 113604fd306cSNickeau } 113704fd306cSNickeau $xhtmlContent = $tagAttributes->toHtmlEnterTag($htmlElement); 113804fd306cSNickeau 113904fd306cSNickeau try { 114004fd306cSNickeau $xhtmlContent .= $tagAttributes->getInnerText(); 114104fd306cSNickeau } catch (ExceptionNotFound $e) { 114204fd306cSNickeau // ok 114304fd306cSNickeau } 114404fd306cSNickeau $xhtmlContent .= "</$htmlElement>"; 114504fd306cSNickeau return $xhtmlContent; 114604fd306cSNickeau } 114704fd306cSNickeau 114804fd306cSNickeau /** 114904fd306cSNickeau * If is not a wiki path 115004fd306cSNickeau * It can't be served 115104fd306cSNickeau * 115204fd306cSNickeau * Example from theming, ... 115304fd306cSNickeau */ 115404fd306cSNickeau private function hasLocalUrl(): bool 115504fd306cSNickeau { 115604fd306cSNickeau try { 115704fd306cSNickeau $this->getLocalUrl(); 115804fd306cSNickeau return true; 115904fd306cSNickeau } catch (\Exception $e) { 116004fd306cSNickeau return false; 116104fd306cSNickeau } 116204fd306cSNickeau } 116304fd306cSNickeau 116404fd306cSNickeau /** 116504fd306cSNickeau * The format 116604fd306cSNickeau * for javascript as specified by [rollup](https://rollupjs.org/configuration-options/#output-format) 116704fd306cSNickeau * @param string $format 116804fd306cSNickeau * @return Snippet 116904fd306cSNickeau */ 117004fd306cSNickeau public function setFormat(string $format): Snippet 117104fd306cSNickeau { 117204fd306cSNickeau $this->format = $format; 117304fd306cSNickeau return $this; 117404fd306cSNickeau } 117504fd306cSNickeau 117604fd306cSNickeau /** 117704fd306cSNickeau * Retrieve the content and wrap it if necessary 117804fd306cSNickeau * to define the execution time 117904fd306cSNickeau * (ie there is no `defer` option for inline html 118004fd306cSNickeau * @throws ExceptionNotFound 118104fd306cSNickeau */ 118204fd306cSNickeau private function getInnerHtml(): string 118304fd306cSNickeau { 118404fd306cSNickeau $internal = $this->getInternalInlineAndFileContent(); 118504fd306cSNickeau if ( 118604fd306cSNickeau $this->getExtension() === self::EXTENSION_JS 118704fd306cSNickeau && $this->format === self::IIFE_FORMAT 118804fd306cSNickeau && $this->getCritical() === false 118904fd306cSNickeau ) { 119004fd306cSNickeau $internal = <<<EOF 119104fd306cSNickeauwindow.addEventListener('load', function () { $internal }); 119204fd306cSNickeauEOF; 119304fd306cSNickeau } 119404fd306cSNickeau return $internal; 119504fd306cSNickeau } 119604fd306cSNickeau 119704fd306cSNickeau public function getFormat(): string 119804fd306cSNickeau { 119904fd306cSNickeau return $this->format; 120004fd306cSNickeau } 120104fd306cSNickeau 120204fd306cSNickeau 120337748cd8SNickeau} 1204