xref: /plugin/combo/ComboStrap/Snippet.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
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