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