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