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