xref: /plugin/combo/ComboStrap/Bootstrap.php (revision 70bbd7f1f72440223cc13f3495efdcb2b0a11514)
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
15use action_plugin_combo_snippetsbootstrap;
16use ComboStrap\Web\Url;
17use Exception;
18
19class Bootstrap
20{
21    const DEFAULT_STYLESHEET_NAME = "bootstrap";
22    const TAG = self::CANONICAL;
23    public const DEFAULT_MAJOR = 5;
24    public const VERSION_501 = self::DEFAULT_MAJOR . ".0.1";
25    public const DEFAULT_BOOTSTRAP_4_VERSION = "4.5.0";
26    public const DEFAULT_BOOTSTRAP_5_VERSION = self::VERSION_501;
27    public const DEFAULT_BOOTSTRAP_STYLESHEET_4_VERSION = self::DEFAULT_BOOTSTRAP_4_VERSION . " - " . self::DEFAULT_STYLESHEET_NAME;
28    public const DEFAULT_BOOTSTRAP_STYLESHEET_5_VERSION = self::DEFAULT_BOOTSTRAP_5_VERSION . " - " . self::DEFAULT_STYLESHEET_NAME;
29
30    private Snippet $jquerySnippet;
31    private Snippet $jsSnippet;
32    private Snippet $popperSnippet;
33    private Snippet $cssSnippet;
34
35    /**
36     * @param string $qualifiedVersion - the bootstrap version separated by the stylesheet
37     */
38    public function __construct(string $qualifiedVersion)
39    {
40        $bootstrapStyleSheetArray = explode(Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR, $qualifiedVersion);
41        $this->version = $bootstrapStyleSheetArray[0];
42        /**
43         * In case the input is just the major version
44         */
45        switch ($this->version) {
46            case "4":
47                $this->version = self::DEFAULT_BOOTSTRAP_4_VERSION;
48                break;
49            case "5":
50                $this->version = self::DEFAULT_BOOTSTRAP_5_VERSION;
51                break;
52        }
53
54        /**
55         * Stylesheet
56         */
57        if (isset($bootstrapStyleSheetArray[1])) {
58            $this->styleSheetName = $bootstrapStyleSheetArray[1];
59        } else {
60            $this->styleSheetName = self::DEFAULT_STYLESHEET_NAME;
61        }
62
63        $this->build();
64
65    }
66
67
68    const BootStrapFiveMajorVersion = 5;
69    const BootStrapFourMajorVersion = 4;
70    const CANONICAL = "bootstrap";
71    /**
72     * Stylesheet and Boostrap should have the same version
73     * This conf is a mix between the version and the stylesheet
74     *
75     * majorVersion.0.0 - stylesheetname
76     */
77    public const CONF_BOOTSTRAP_VERSION_STYLESHEET = "bootstrapVersionStylesheet";
78    /**
79     * The separator in {@link Bootstrap::CONF_BOOTSTRAP_VERSION_STYLESHEET}
80     */
81    public const BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR = " - ";
82    public const DEFAULT_BOOTSTRAP_VERSION_STYLESHEET = "5.0.1" . Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . "bootstrap";
83
84    /**
85     * @var mixed|string
86     */
87    private $version;
88    /**
89     * @var string - the stylesheet name
90     */
91    private $styleSheetName;
92
93    public static function getDataNamespace()
94    {
95        $dataToggleNamespace = "";
96        if (self::getBootStrapMajorVersion() == self::BootStrapFiveMajorVersion) {
97            $dataToggleNamespace = "-bs";
98        }
99        return $dataToggleNamespace;
100    }
101
102    public function getMajorVersion(): int
103    {
104        $bootstrapMajorVersion = $this->getVersion()[0];
105        try {
106            return DataType::toInteger($bootstrapMajorVersion);
107        } catch (ExceptionBadArgument $e) {
108            LogUtility::internalError("The bootstrap major version ($bootstrapMajorVersion) is not an integer, default taken");
109            return self::DEFAULT_MAJOR;
110        }
111
112    }
113
114    /**
115     * Utility function that returns the major version
116     * because this is really common in code
117     * @return int - the major version
118     */
119    public static function getBootStrapMajorVersion(): int
120    {
121        return Bootstrap::getFromContext()->getMajorVersion();
122    }
123
124
125    public function getVersion(): string
126    {
127        return $this->version;
128    }
129
130    public static function createFromQualifiedVersion(string $boostrapVersion): Bootstrap
131    {
132        return new Bootstrap($boostrapVersion);
133    }
134
135
136    public function getStyleSheetName(): string
137    {
138        return $this->styleSheetName;
139    }
140
141    /**
142     *
143     * @return array - an array of stylesheet tag
144     */
145    public static function getStyleSheetMetas(): array
146    {
147
148        /**
149         * Standard stylesheet
150         */
151        $stylesheetsFile = WikiPath::createComboResource(':library:bootstrap:bootstrapStylesheet.json');
152        try {
153            $styleSheets = Json::createFromPath($stylesheetsFile)->toArray();
154        } catch (ExceptionNotFound|ExceptionBadSyntax $e) {
155            LogUtility::internalError("An error has occurred reading the file ($stylesheetsFile). Error:{$e->getMessage()}", self::CANONICAL);
156            return [];
157        }
158
159        /**
160         * User defined stylesheet
161         */
162        $localStyleSheetsFile = WikiPath::createComboResource(':library:bootstrap:bootstrapLocal.json');
163        try {
164            $localStyleSheets = Json::createFromPath($localStyleSheetsFile)->toArray();
165            foreach ($localStyleSheets as $bootstrapVersion => &$localStyleSheetData) {
166                $actualStyleSheet = $styleSheets[$bootstrapVersion];
167                if (isset($actualStyleSheet)) {
168                    $styleSheets[$bootstrapVersion] = array_merge($actualStyleSheet, $localStyleSheetData);
169                } else {
170                    $styleSheets[$bootstrapVersion] = $localStyleSheetData;
171                }
172            }
173        } catch (ExceptionBadSyntax|ExceptionNotFound $e) {
174            // user file does not exists and that's okay
175        }
176        return $styleSheets;
177
178
179    }
180
181
182    /**
183     * @return array - A list of all available stylesheets
184     * This function is used to build the configuration as a list of files
185     */
186    public static function getQualifiedVersions(): array
187    {
188        $cssVersionsMetas = Bootstrap::getStyleSheetMetas();
189        $listVersionStylesheetMeta = array();
190        foreach ($cssVersionsMetas as $bootstrapVersion => $cssVersionMeta) {
191            foreach ($cssVersionMeta as $fileName => $values) {
192                $listVersionStylesheetMeta[] = $bootstrapVersion . Bootstrap::BOOTSTRAP_VERSION_STYLESHEET_SEPARATOR . $fileName;
193            }
194        }
195        return $listVersionStylesheetMeta;
196    }
197
198
199    /**
200     * @return Bootstrap
201     */
202    public static function getFromContext(): Bootstrap
203    {
204        $executionContext = ExecutionContext::getActualOrCreateFromEnv();
205        try {
206            return $executionContext->getRuntimeObject(self::CANONICAL);
207        } catch (ExceptionNotFound $e) {
208            $bootstrapStyleSheetVersion = ExecutionContext::getActualOrCreateFromEnv()
209                ->getConfValue(Bootstrap::CONF_BOOTSTRAP_VERSION_STYLESHEET, Bootstrap::DEFAULT_BOOTSTRAP_VERSION_STYLESHEET);
210            $bootstrap = new Bootstrap($bootstrapStyleSheetVersion);
211            $executionContext->setRuntimeObject(self::CANONICAL, $bootstrap);
212            return $bootstrap;
213        }
214
215    }
216
217
218    /**
219     * @throws ExceptionNotFound
220     */
221    public function getCssSnippet(): Snippet
222    {
223        if (isset($this->cssSnippet)) {
224            return $this->cssSnippet;
225        }
226        throw new ExceptionNotFound("No css snippet");
227    }
228
229    /**
230     * @return Snippet[] the js snippets in order
231     */
232    public function getJsSnippets(): array
233    {
234
235        /**
236         * The javascript snippet order is important
237         */
238        $snippets = [];
239        try {
240            $snippets[] = $this->getJquerySnippet();
241        } catch (ExceptionNotFound $e) {
242            // error already send at build time
243            // or just not present
244        }
245        try {
246            $snippets[] = $this->getPopperSnippet();
247        } catch (ExceptionNotFound $e) {
248            // error already send at build time
249        }
250        try {
251            $snippets[] = $this->getBootstrapJsSnippet();
252        } catch (ExceptionNotFound $e) {
253            // error already send at build time
254        }
255        return $snippets;
256
257    }
258
259    /**
260     *
261     */
262    private function build(): void
263    {
264
265
266        $version = $this->getVersion();
267
268        // Javascript
269        $bootstrapJsonFile = WikiPath::createComboResource(Snippet::LIBRARY_BASE . ":bootstrap:bootstrapJavascript.json");
270        try {
271            $bootstrapJsonMetas = Json::createFromPath($bootstrapJsonFile)->toArray();
272        } catch (ExceptionBadSyntax|ExceptionNotFound $e) {
273            // should not happen, no need to advertise it
274            throw new ExceptionRuntimeInternal("Unable to read the file {$bootstrapJsonFile} as json.", self::CANONICAL, 1, $e);
275        }
276        if (!isset($bootstrapJsonMetas[$version])) {
277            throw new ExceptionRuntimeInternal("The bootstrap version ($version) could not be found in the file $bootstrapJsonFile");
278        }
279        $bootstrapMetas = $bootstrapJsonMetas[$version];
280
281        // Css
282        $bootstrapMetas["stylesheet"] = $this->getStyleSheetMeta();
283
284
285        foreach ($bootstrapMetas as $key => $script) {
286            $fileNameWithExtension = $script["file"];
287            $file = LocalPath::createFromPathString($fileNameWithExtension);
288
289            $path = WikiPath::createComboResource(":library:bootstrap:$version:$fileNameWithExtension");
290            $snippet = Snippet::createSnippet($path)
291                ->setComponentId(self::TAG);
292            $url = $script["url"] ?? null;
293            if (!empty($url)) {
294                try {
295                    $url = Url::createFromString($url);
296                    $snippet->setRemoteUrl($url);
297                    if (isset($script['integrity'])) {
298                        $snippet->setIntegrity($script['integrity']);
299                    }
300                } catch (ExceptionBadArgument|ExceptionBadSyntax $e) {
301                    LogUtility::internalError("The url ($url) for the bootstrap metadata ($fileNameWithExtension) from the bootstrap dictionary is not valid. Error:{$e->getMessage()}", self::CANONICAL);
302                }
303            }
304
305            try {
306                $extension = $file->getExtension();
307            } catch (ExceptionNotFound $e) {
308                LogUtility::internalError("No extension was found on the file metadata ($fileNameWithExtension) from the bootstrap dictionary", self::CANONICAL);
309                continue;
310            }
311            switch ($extension) {
312                case Snippet::EXTENSION_JS:
313                    $snippet->setCritical(false);
314                    switch ($key) {
315                        case "jquery":
316                            $this->jquerySnippet = $snippet;
317                            break;
318                        case "js":
319                            $this->jsSnippet = $snippet;
320                            break;
321                        case "popper":
322                            $this->popperSnippet = $snippet;
323                            break;
324                        default:
325                            LogUtility::internalError("The snippet key ($key) is unknown for bootstrap", self::CANONICAL);
326                            break;
327                    }
328                    break;
329                case Snippet::EXTENSION_CSS:
330                    switch ($key) {
331                        case "stylesheet":
332                            $this->cssSnippet = $snippet;
333                            break;
334                        default:
335                            LogUtility::internalError("The snippet key ($key) is unknown for bootstrap");
336                            break;
337                    }
338            }
339        }
340
341
342    }
343
344    public function getSnippets(): array
345    {
346
347        $snippets = [];
348        try {
349            $snippets[] = $this->getCssSnippet();
350        } catch (ExceptionNotFound $e) {
351            // error already send at build time
352        }
353
354        /**
355         * The javascript snippet
356         */
357        return array_merge($snippets, $this->getJsSnippets());
358    }
359
360    /**
361     * @throws ExceptionNotFound
362     */
363    public function getPopperSnippet(): Snippet
364    {
365        if (isset($this->popperSnippet)) {
366            return $this->popperSnippet;
367        }
368        throw new ExceptionNotFound("No popper snippet");
369    }
370
371    /**
372     * @throws ExceptionNotFound
373     */
374    public function getBootstrapJsSnippet(): Snippet
375    {
376        if (isset($this->jsSnippet)) {
377            return $this->jsSnippet;
378        }
379        throw new ExceptionNotFound("No js snippet");
380    }
381
382    /**
383     * @throws ExceptionNotFound
384     */
385    private function getJquerySnippet(): Snippet
386    {
387        if (isset($this->jquerySnippet)) {
388            return $this->jquerySnippet;
389        }
390        throw new ExceptionNotFound("No jquery snippet");
391    }
392
393    /**
394     * @return array - the stylesheet meta (file, url, ...) for the version
395     */
396    private function getStyleSheetMeta(): array
397    {
398
399        $styleSheets = self::getStyleSheetMetas();
400
401        $version = $this->getVersion();
402        if (!isset($styleSheets[$version])) {
403            LogUtility::internalError("The bootstrap version ($version) could not be found");
404            return [];
405        }
406        $styleSheetsForVersion = $styleSheets[$version];
407
408        $styleSheetName = $this->getStyleSheetName();
409        if (!isset($styleSheetsForVersion[$styleSheetName])) {
410            LogUtility::internalError("The bootstrap stylesheet ($styleSheetName) could not be found for the version ($version) in the distribution or custom configuration files");
411            return [];
412        }
413        $styleSheetForVersionAndName = $styleSheetsForVersion[$styleSheetName];
414
415        /**
416         * Select Rtl or Ltr
417         * Stylesheet name may have another level
418         * with direction property of the language
419         *
420         * Bootstrap needs another stylesheet
421         * See https://getbootstrap.com/docs/5.0/getting-started/rtl/
422         */
423        try {
424            $direction = Lang::createFromRequestedMarkup()->getDirection();
425        } catch (ExceptionNotFound $e) {
426            $direction = Site::getLangObject()->getDirection();
427        }
428        if (isset($styleSheetForVersionAndName[$direction])) {
429            return $styleSheetForVersionAndName[$direction];
430        }
431        if (!isset($styleSheetForVersionAndName['file'])) {
432            LogUtility::internalError('The file stylesheet attribute is unknown (' . DataType::toString($styleSheetForVersionAndName) . ')');
433        }
434        return $styleSheetForVersionAndName;
435    }
436}
437