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