xref: /template/strap/action/snippetsbootstrap.php (revision 04fd306c7c155fa133ebb3669986875d65988276)
1<?php
2/**
3 * DokuWiki Plugin Js Action
4 *
5 */
6
7use ComboStrap\ArrayUtility;
8use ComboStrap\Bootstrap;
9use ComboStrap\ExceptionNotFound;
10use ComboStrap\ExecutionContext;
11use ComboStrap\Identity;
12use ComboStrap\LogUtility;
13use ComboStrap\TemplateForWebPage;
14use ComboStrap\SiteConfig;
15
16
17/**
18 *
19 *
20 */
21class action_plugin_combo_snippetsbootstrap extends DokuWiki_Action_Plugin
22{
23
24
25    public const CONF_PRELOAD_CSS = "preloadCss";
26    /**
27     * Use the Jquery of Dokuwiki and not of Bootstrap
28     */
29    public const CONF_JQUERY_DOKU = 'jQueryDoku';
30    public const CONF_JQUERY_DOKU_DEFAULT = 0;
31
32    /**
33     * Disable the javascript of Dokuwiki
34     * if public
35     * https://combostrap.com/frontend/optimization
36     */
37    public const CONF_DISABLE_BACKEND_JAVASCRIPT = "disableBackendJavascript";
38
39    /**
40     * This is so a bad practice, default to no
41     * but fun to watch
42     */
43    const CONF_PRELOAD_CSS_DEFAULT = 0;
44    const JQUERY_CANONICAL = "jquery";
45    const FRONT_END_OPTIMIZATION_CANONICAL = 'frontend:optimization';
46
47
48    /**
49     * @param Doku_Event $event
50     * @param $param
51     * Function that handle the META HEADER event
52     *   * It will add the Bootstrap Js and CSS
53     *   * Make all script and resources defer
54     * @throws Exception
55     * @noinspection PhpUnused
56     */
57    public static function handle_bootstrap(Doku_Event &$event, $param)
58    {
59
60
61        $newHeaderTypes = array();
62
63        $bootstrap = Bootstrap::getFromContext();
64        $bootStrapMajorVersion = $bootstrap->getMajorVersion();
65        $eventHeaderTypes = $event->data;
66        $executionContextConfig = ExecutionContext::getActualOrCreateFromEnv()->getConfig();
67        foreach ($eventHeaderTypes as $headTagName => $headTagsAsArray) {
68            switch ($headTagName) {
69
70                case "link":
71                    /**
72                     * Link tag processing
73                     * ie index, rss, manifest, search, alternate, stylesheet
74                     */
75                    $headTagsAsArray[] = $bootstrap->getCssSnippet()->toDokuWikiArray();
76
77                    // preload all CSS is an heresy as it creates a FOUC (Flash of non-styled element)
78                    // but we know it only now and this is fun to experience for the user
79                    $cssPreloadConf = $executionContextConfig->getValue(self::CONF_PRELOAD_CSS, self::CONF_PRELOAD_CSS_DEFAULT);
80                    $newLinkData = array();
81                    foreach ($headTagsAsArray as $linkData) {
82                        switch ($linkData['rel']) {
83                            case 'edit':
84                                break;
85                            case 'preload':
86                                /**
87                                 * Preload can be set at the array level with the critical attribute
88                                 * If the preload attribute is present
89                                 * We get that for instance for css animation style sheet
90                                 * that are not needed for rendering
91                                 */
92                                if (isset($linkData["as"])) {
93                                    if ($linkData["as"] === "style") {
94                                        $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData);
95                                        continue 2;
96                                    }
97                                }
98                                $newLinkData[] = $linkData;
99                                break;
100                            case 'stylesheet':
101                                if ($cssPreloadConf) {
102                                    $newLinkData[] = self::captureStylePreloadingAndTransformToPreloadCssTag($linkData);
103                                    continue 2;
104                                }
105                                $newLinkData[] = $linkData;
106                                break;
107                            default:
108                                $newLinkData[] = $linkData;
109                                break;
110                        }
111                    }
112
113                    $newHeaderTypes[$headTagName] = $newLinkData;
114                    break;
115
116                case "script":
117
118                    /**
119                     * Script processing
120                     *
121                     * Do we delete the dokuwiki javascript ?
122                     */
123                    $scriptToDeletes = [];
124                    $disableBackend = SiteConfig::getConfValue(self::CONF_DISABLE_BACKEND_JAVASCRIPT, 0);
125                    if (!Identity::isLoggedIn() && $disableBackend) {
126                        $scriptToDeletes = [
127                            //'JSINFO', Don't delete Jsinfo !! It contains metadata information (that is used to get context)
128                            'js.php'
129                        ];
130                        if ($bootStrapMajorVersion == "5") {
131                            // bs 5 does not depends on jquery
132                            $scriptToDeletes[] = "jquery.php";
133                        }
134                    }
135
136                    /**
137                     * The new script array
138                     * that will replace the actual
139                     */
140                    $newScriptTagAsArray = array();
141                    /**
142                     * Scan:
143                     *   * Capture the Dokuwiki Jquery Tags
144                     *   * Delete for optimization if needed
145                     *
146                     * @var array A variable to hold the Jquery scripts
147                     * jquery-migrate, jquery, jquery-ui ou jquery.php
148                     * see https://www.dokuwiki.org/config:jquerycdn
149                     */
150                    $jqueryDokuScriptsTagsAsArray = array();
151                    foreach ($headTagsAsArray as $scriptData) {
152
153                        foreach ($scriptToDeletes as $scriptToDelete) {
154                            if (isset($scriptData["_data"]) && !empty($scriptData["_data"])) {
155                                $haystack = $scriptData["_data"];
156                            } else {
157                                $haystack = $scriptData["src"];
158                            }
159                            if (preg_match("/$scriptToDelete/i", $haystack)) {
160                                continue 2;
161                            }
162                        }
163
164                        $critical = false;
165                        if (isset($scriptData["critical"])) {
166                            $critical = $scriptData["critical"];
167                            unset($scriptData["critical"]);
168                        }
169
170                        // defer is only for external resource
171                        // if this is not, this is illegal
172                        if (isset($scriptData["src"])) {
173                            if (!$critical) {
174                                $scriptData['defer'] = null;
175                            }
176                        }
177
178                        if (isset($scriptData["type"])) {
179                            $type = strtolower($scriptData["type"]);
180                            if ($type == "text/javascript") {
181                                unset($scriptData["type"]);
182                            }
183                        }
184
185                        // The charset attribute on the script element is obsolete.
186                        if (isset($scriptData["charset"])) {
187                            unset($scriptData["charset"]);
188                        }
189
190                        // Jquery ?
191                        $jqueryFound = false;
192                        // script may also be just an online script without the src attribute
193                        if (array_key_exists('src', $scriptData)) {
194                            $jqueryFound = strpos($scriptData['src'], 'jquery');
195                        }
196                        if ($jqueryFound === false) {
197                            $newScriptTagAsArray[] = $scriptData;
198                        } else {
199                            $jqueryDokuScriptsTagsAsArray[] = $scriptData;
200                        }
201
202                    }
203
204                    /**
205                     * Add Bootstrap scripts
206                     * At the top of the queue
207                     */
208                    if ($bootStrapMajorVersion === 4) {
209                        $useJqueryDoku = ExecutionContext::getActualOrCreateFromEnv()->getConfig()->getBooleanValue(self::CONF_JQUERY_DOKU, self::CONF_JQUERY_DOKU_DEFAULT);
210                        if (
211                            !Identity::isLoggedIn()
212                            && !$useJqueryDoku
213                        ) {
214                            /**
215                             * We take the Javascript of Bootstrap
216                             * (Jquery and others)
217                             */
218                            $boostrapSnippetsAsArray = [];
219                            foreach ($bootstrap->getJsSnippets() as $snippet) {
220                                $boostrapSnippetsAsArray[] = $snippet->toDokuWikiArray();
221                            }
222                            /**
223                             * At the top of the queue
224                             */
225                            $newScriptTagAsArray = array_merge($boostrapSnippetsAsArray, $newScriptTagAsArray);
226                        } else {
227                            // Logged in
228                            // We take the Jqueries of doku and we add Bootstrap
229                            $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js
230                            // We had popper of Bootstrap
231                            $newScriptTagAsArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray();
232                            // We had the js of Bootstrap
233                            $newScriptTagAsArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray();
234                        }
235                    } else {
236
237                        // There is no JQuery in 5
238                        // We had the js of Bootstrap and popper
239                        // Add Jquery before the js.php
240                        $newScriptTagAsArray = array_merge($jqueryDokuScriptsTagsAsArray, $newScriptTagAsArray); // js
241                        // Then add at the top of the top (first of the first) bootstrap
242                        // Why ? Because Jquery should be last to be able to see the missing icon
243                        // https://stackoverflow.com/questions/17367736/jquery-ui-dialog-missing-close-icon
244                        $bootstrapTagArray[] = $bootstrap->getPopperSnippet()->toDokuWikiArray();
245                        $bootstrapTagArray[] = $bootstrap->getBootstrapJsSnippet()->toDokuWikiArray();
246                        $newScriptTagAsArray = array_merge($bootstrapTagArray, $newScriptTagAsArray);
247
248                    }
249
250                    $newHeaderTypes[$headTagName] = $newScriptTagAsArray;
251                    break;
252                case "meta":
253                    $newHeaderData = array();
254                    foreach ($headTagsAsArray as $metaData) {
255                        // Content should never be null
256                        // Name may change
257                        // https://www.w3.org/TR/html4/struct/global.html#edef-META
258                        if (!key_exists("content", $metaData)) {
259                            $message = "The head meta (" . print_r($metaData, true) . ") does not have a content property";
260                            LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL);
261                        } else {
262                            $content = $metaData["content"];
263                            if (empty($content)) {
264                                $messageEmpty = "the below head meta has an empty content property (" . ArrayUtility::formatAsString($metaData) . ")";
265                                LogUtility::error($messageEmpty, self::FRONT_END_OPTIMIZATION_CANONICAL);
266                            } else {
267                                $newHeaderData[] = $metaData;
268                            }
269                        }
270                    }
271                    $newHeaderTypes[$headTagName] = $newHeaderData;
272                    break;
273                case "noscript": // https://github.com/ComboStrap/dokuwiki-plugin-gtm/blob/master/action.php#L32
274                case "style":
275                    $newHeaderTypes[$headTagName] = $headTagsAsArray;
276                    break;
277                default:
278                    $message = "The header type ($headTagName) is unknown and was not controlled.";
279                    $newHeaderTypes[$headTagName] = $headTagsAsArray;
280                    LogUtility::error($message, self::FRONT_END_OPTIMIZATION_CANONICAL);
281
282            }
283        }
284
285        $event->data = $newHeaderTypes;
286
287
288    }
289
290    /**
291     * @param $linkData - an array of link style sheet data
292     * @return array - the array with the preload attributes
293     */
294    public static function captureStylePreloadingAndTransformToPreloadCssTag($linkData): array
295    {
296        /**
297         * Save the stylesheet to load it at the end
298         */
299        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
300        try {
301            $preloadedCss = &$executionContext->getRuntimeObject(TemplateForWebPage::PRELOAD_TAG);
302        } catch (ExceptionNotFound $e) {
303            $preloadedCss = [];
304            $executionContext->setRuntimeObject(TemplateForWebPage::PRELOAD_TAG,$preloadedCss);
305        }
306        $preloadedCss[] = $linkData;
307
308        /**
309         * Modify the actual tag data
310         * Change the loading mechanism to preload
311         */
312        $linkData['rel'] = 'preload';
313        $linkData['as'] = 'style';
314        return $linkData;
315    }
316
317
318
319    /**
320     * Registers our handler for the MANIFEST_SEND event
321     * https://www.dokuwiki.org/devel:event:js_script_list
322     * manipulate the list of JavaScripts that will be concatenated
323     * @param Doku_Event_Handler $controller
324     */
325    public function register(Doku_Event_Handler $controller)
326    {
327
328        $controller->register_hook('TPL_METAHEADER_OUTPUT', 'BEFORE', $this, 'handle_bootstrap');
329
330    }
331
332
333}
334
335