1<?php
2
3
4use ComboStrap\Call;
5use ComboStrap\CallStack;
6use ComboStrap\Dimension;
7use ComboStrap\ExceptionCombo;
8use ComboStrap\LogUtility;
9use ComboStrap\MediaLink;
10use ComboStrap\PluginUtility;
11use ComboStrap\TagAttributes;
12
13require_once(__DIR__ . '/../ComboStrap/PluginUtility.php');
14
15
16/**
17 * Carrousel
18 *
19 * We loved
20 * https://github.com/OwlCarousel2/OwlCarousel2
21 * but it's deprecated and
22 * send us to
23 * https://github.com/ganlanyuan/tiny-slider
24 * But it used as gutter the padding not the margin (http://ganlanyuan.github.io/tiny-slider/demo/#gutter_wrapper)
25 * Then we found
26 * https://glidejs.com/
27 *
28 *
29 *
30 */
31class syntax_plugin_combo_carrousel extends DokuWiki_Syntax_Plugin
32{
33
34
35    const TAG = 'carrousel';
36    const CANONICAL = self::TAG;
37    const ELEMENT_WIDTH_ATTRIBUTE = "element-width";
38    const CONTROL_ATTRIBUTE = "control";
39    const GLIDE_SLIDE_CLASS = "glide__slide";
40
41    /**
42     * The number of element
43     * (we get it by scanning the element or
44     * via the {@link syntax_plugin_combo_iterator} that set it up)
45     */
46    const ELEMENT_COUNT = "bullet-count";
47
48    /**
49     * To center the image inside a link in a carrousel
50     */
51    const MEDIA_CENTER_LINK_CLASS = "justify-content-center align-items-center d-flex";
52    const ELEMENTS_MIN_ATTRIBUTE = "elements-min";
53    const ELEMENTS_MIN_DEFAULT = 3;
54
55    private static function isCarrousel($data, TagAttributes $tagAttributes): bool
56    {
57        $elementCount = $data[self::ELEMENT_COUNT];
58        $elementWidth = $tagAttributes->getValue(self::ELEMENT_WIDTH_ATTRIBUTE);
59        if ($elementWidth !== null) {
60            $elementsMin = $tagAttributes->getValue(self::ELEMENTS_MIN_ATTRIBUTE, self::ELEMENTS_MIN_DEFAULT);
61            if ($elementCount < $elementsMin) {
62                return false;
63            }
64        }
65        return true;
66    }
67
68
69    private static function madeChildElementCarrouselAware(?Call $childCarrouselElement)
70    {
71        $tagName = $childCarrouselElement->getTagName();
72        if ($tagName === syntax_plugin_combo_media::TAG) {
73            $childCarrouselElement->setAttribute(syntax_plugin_combo_media::LINK_CLASS_ATTRIBUTE, self::GLIDE_SLIDE_CLASS . " " . self::MEDIA_CENTER_LINK_CLASS);
74        } else {
75            $childCarrouselElement->addClassName(self::GLIDE_SLIDE_CLASS);
76        }
77
78    }
79
80    /**
81     * Glide copy the HTML element and lozad does not see element that are not visible
82     * The element non-visible are not processed by lozad
83     * We set lazy loading to HTML loading attribute
84     */
85    private static function setLazyLoadToHtmlOnImageTagUntilTheEndOfTheStack(CallStack $callStack)
86    {
87        while ($actualCall = $callStack->next()) {
88            if ($actualCall->getState() === DOKU_LEXER_SPECIAL && in_array($actualCall->getTagName(), Call::IMAGE_TAGS)) {
89                $actualCall->addAttribute(
90                    MediaLink::LAZY_LOAD_METHOD,
91                    MediaLink::LAZY_LOAD_METHOD_HTML_VALUE
92                );
93            }
94        }
95    }
96
97
98    function getType(): string
99    {
100        return 'container';
101    }
102
103    /**
104     * How DokuWiki will add P element
105     *
106     *  * 'normal' - The plugin can be used inside paragraphs
107     *  * 'block'  - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs
108     *  * 'stack'  - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs
109     *
110     * @see DokuWiki_Syntax_Plugin::getPType()
111     */
112    function getPType(): string
113    {
114        return 'block';
115    }
116
117    /**
118     * @return array
119     * Allow which kind of plugin inside
120     *
121     * No one of array('baseonly','container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs')
122     * because we manage self the content and we call self the parser
123     *
124     * Return an array of one or more of the mode types {@link $PARSER_MODES} in Parser.php
125     */
126    function getAllowedTypes(): array
127    {
128        return array('baseonly', 'container', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs');
129    }
130
131    function getSort(): int
132    {
133        return 199;
134    }
135
136    public function accepts($mode): bool
137    {
138        return syntax_plugin_combo_preformatted::disablePreformatted($mode);
139    }
140
141
142    function connectTo($mode)
143    {
144
145
146        $pattern = PluginUtility::getContainerTagPattern(self::TAG);
147        $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent()));
148
149
150    }
151
152
153    function postConnect()
154    {
155
156        $this->Lexer->addExitPattern('</' . self::TAG . '>', PluginUtility::getModeFromTag($this->getPluginComponent()));
157
158
159    }
160
161    /**
162     *
163     * The handle function goal is to parse the matched syntax through the pattern function
164     * and to return the result for use in the renderer
165     * This result is always cached until the page is modified.
166     * @param string $match
167     * @param int $state
168     * @param int $pos - byte position in the original source file
169     * @param Doku_Handler $handler
170     * @return array
171     * @see DokuWiki_Syntax_Plugin::handle()
172     *
173     */
174    function handle($match, $state, $pos, Doku_Handler $handler): array
175    {
176
177        switch ($state) {
178
179            case DOKU_LEXER_ENTER :
180                $tagAttributes = TagAttributes::createFromTagMatch($match);
181                $callStack = CallStack::createFromHandler($handler);
182                $parent = $callStack->moveToParent();
183                $context = null;
184                if ($parent !== false) {
185                    $context = $parent->getTagName();
186                }
187                return array(
188                    PluginUtility::STATE => $state,
189                    PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(),
190                    PluginUtility::CONTEXT => $context
191                );
192
193            case DOKU_LEXER_UNMATCHED :
194
195                return PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler);
196
197
198            case DOKU_LEXER_EXIT :
199
200                $callStack = CallStack::createFromHandler($handler);
201                $openingCall = $callStack->moveToPreviousCorrespondingOpeningCall();
202                $actualCall = $callStack->moveToFirstChildTag();
203                $childrenCount = null;
204                if ($actualCall !== false) {
205                    if ($actualCall->getTagName() === syntax_plugin_combo_template::TAG) {
206                        $templateEndCall = $callStack->moveToNextCorrespondingExitTag();
207                        $templateCallStackInstructions = $templateEndCall->getPluginData(syntax_plugin_combo_template::CALLSTACK);
208                        if ($templateCallStackInstructions !== null) {
209                            $templateCallStack = CallStack::createFromInstructions($templateCallStackInstructions);
210                            // The glide class
211                            $templateCallStack->moveToStart();
212                            $firstTemplateEnterTag = $templateCallStack->moveToFirstEnterTag();
213                            if ($firstTemplateEnterTag !== false) {
214                                self::madeChildElementCarrouselAware($firstTemplateEnterTag);
215                            }
216                            // Lazy load
217                            $templateCallStack->moveToStart();
218                            self::setLazyLoadToHtmlOnImageTagUntilTheEndOfTheStack($templateCallStack);
219                            $templateEndCall->setPluginData(syntax_plugin_combo_template::CALLSTACK, $templateCallStack->getStack());
220                        }
221                    } else {
222                        self::madeChildElementCarrouselAware($actualCall);
223                        $childrenCount = 1;
224                        while ($actualCall = $callStack->moveToNextSiblingTag()) {
225                            self::madeChildElementCarrouselAware($actualCall);
226                            $childrenCount++;
227                        }
228                        $openingCall->setPluginData(self::ELEMENT_COUNT, $childrenCount);
229                        // Lazy load
230                        $callStack->moveToEnd();
231                        $callStack->moveToPreviousCorrespondingOpeningCall();
232                        self::setLazyLoadToHtmlOnImageTagUntilTheEndOfTheStack($callStack);
233                    }
234                }
235                return array(
236                    PluginUtility::STATE => $state,
237                    PluginUtility::ATTRIBUTES => $openingCall->getAttributes(),
238                    self::ELEMENT_COUNT => $childrenCount,
239                );
240
241
242        }
243        return array();
244
245    }
246
247    /**
248     * Render the output
249     * @param string $format
250     * @param Doku_Renderer $renderer
251     * @param array $data - what the function handle() return'ed
252     * @return boolean - rendered correctly? (however, returned value is not used at the moment)
253     * @see DokuWiki_Syntax_Plugin::render()
254     *
255     *
256     */
257    function render($format, Doku_Renderer $renderer, $data): bool
258    {
259
260
261        if ($format == 'xhtml') {
262
263            /** @var Doku_Renderer_xhtml $renderer */
264            $state = $data [PluginUtility::STATE];
265            switch ($state) {
266                case DOKU_LEXER_ENTER :
267
268                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES], self::TAG);
269
270                    $slideMinimalWidth = $tagAttributes->getValue(self::ELEMENT_WIDTH_ATTRIBUTE);
271                    $slideMinimalWidthData = "";
272                    if ($slideMinimalWidth !== null) {
273                        try {
274                            $slideMinimalWidth = Dimension::toPixelValue($slideMinimalWidth);
275                            $slideMinimalWidthData = "data-" . self::ELEMENT_WIDTH_ATTRIBUTE . "=\"$slideMinimalWidth\"";
276                        } catch (ExceptionCombo $e) {
277                            $slideMinimalWidth = 250;
278                            LogUtility::msg("The minimal width value ($slideMinimalWidth) is not a valid value. Error: {$e->getMessage()}");
279                        }
280                    }
281                    $snippetManager = PluginUtility::getSnippetManager();
282                    $snippetId = self::TAG;
283                    $carrouselClass = "carrousel-combo";
284                    $isCarrousel = self::isCarrousel($data, $tagAttributes);
285                    if ($isCarrousel) {
286
287                        $renderer->doc .= <<<EOF
288<div class="$carrouselClass glide" $slideMinimalWidthData>
289  <div class="glide__track" data-glide-el="track">
290    <div class="glide__slides">
291EOF;
292
293                        $snippetManager->attachCssInternalStyleSheetForSlot($snippetId);
294                        // https://www.jsdelivr.com/package/npm/@glidejs/glide
295                        $snippetManager->attachCssExternalStyleSheetForSlot($snippetId,
296                            "https://cdn.jsdelivr.net/npm/@glidejs/glide@3.5.2/dist/css/glide.core.min.css",
297                            "sha256-bmdlmBAVo1Q6XV2cHiyaBuBfe9KgYQhCrfQmoRq8+Sg="
298                        );
299                        if (PluginUtility::isDev()) {
300
301                            $javascriptSnippet = $snippetManager->attachJavascriptLibraryForSlot($snippetId,
302                                "https://cdn.jsdelivr.net/npm/@glidejs/glide@3.5.2/dist/glide.js",
303                                "sha256-zkYoJ1XwwGA4FbdmSdTz28y5PtHT8O/ZKzUAuQsmhKg="
304                            );
305
306                        } else {
307                            $javascriptSnippet = $snippetManager->attachJavascriptLibraryForSlot($snippetId,
308                                "https://cdn.jsdelivr.net/npm/@glidejs/glide@3.5.2/dist/glide.min.js",
309                                "sha256-cXguqBvlUaDoW4nGjs4YamNC2mlLGJUOl64bhts/ztU="
310                            );
311                        }
312                        $javascriptSnippet->setDoesManipulateTheDomOnRun(false);
313
314                        // Theme customized from the below official theme
315                        // https://cdn.jsdelivr.net/npm/@glidejs/glide@3.5.2/dist/css/glide.theme.css
316                        $snippetManager->attachCssInternalStyleSheetForSlot($snippetId)
317                            ->setCritical(false);
318                    } else {
319                        // gutter is done with the margin because we don't wrap the child in a cell container.
320                        $renderer->doc .= <<<EOF
321<div class="$carrouselClass row justify-content-center" $slideMinimalWidthData>
322EOF;
323                    }
324                    $snippetManager->attachInternalJavascriptForSlot($snippetId);
325                    break;
326
327                case DOKU_LEXER_UNMATCHED :
328
329                    $renderer->doc .= PluginUtility::renderUnmatched($data);
330                    break;
331
332                case DOKU_LEXER_EXIT :
333
334                    $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
335                    $isCarrousel = self::isCarrousel($data, $tagAttributes);
336
337                    switch ($isCarrousel) {
338                        case false:
339                            // grid
340                            $renderer->doc .= "</div>";
341                            break;
342                        default:
343                        case true:
344                            $renderer->doc .= <<<EOF
345</div>
346  </div>
347EOF;
348
349                            $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]);
350                            $control = $tagAttributes->getValue(self::CONTROL_ATTRIBUTE);
351                            if ($control !== "none") {
352                                // move per view
353                                // https://github.com/glidejs/glide/issues/346#issuecomment-1046137773
354                                $escapedLessThan = PluginUtility::htmlEncode("|<");
355                                $escapedGreaterThan = PluginUtility::htmlEncode("|>");
356
357                                $minimumWidth = $tagAttributes->getValue(self::ELEMENT_WIDTH_ATTRIBUTE);
358                                $classDontShowOnSmallDevice = "";
359                                if ($minimumWidth !== null) {
360                                    // not a one by one (not a gallery)
361                                    $classDontShowOnSmallDevice = "class=\"d-none d-sm-block\"";
362                                }
363                                $renderer->doc .= <<<EOF
364<div>
365  <div $classDontShowOnSmallDevice data-glide-el="controls">
366    <button class="glide__arrow glide__arrow--left" data-glide-dir="$escapedLessThan">
367      <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
368        <path d="M0 12l10.975 11 2.848-2.828-6.176-6.176H24v-3.992H7.646l6.176-6.176L10.975 1 0 12z"></path>
369      </svg>
370    </button>
371    <button class="glide__arrow glide__arrow--right" data-glide-dir="$escapedGreaterThan">
372      <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24">
373        <path d="M13.025 1l-2.847 2.828 6.176 6.176h-16.354v3.992h16.354l-6.176 6.176 2.847 2.828 10.975-11z"></path>
374      </svg>
375    </button>
376  </div>
377  <div class="glide__bullets d-none d-sm-block" data-glide-el="controls[nav]">
378EOF;
379                                $elementCount = $data[self::ELEMENT_COUNT];
380                                for ($i = 0; $i < $elementCount; $i++) {
381                                    $activeClass = "";
382                                    if ($i === 0) {
383                                        $activeClass = " glide__bullet--activeClass";
384                                    }
385                                    $renderer->doc .= <<<EOF
386    <button class="glide__bullet{$activeClass}" data-glide-dir="={$i}"></button>
387EOF;
388                                }
389                                $renderer->doc .= <<<EOF
390  </div>
391</div>
392EOF;
393                            }
394                            $renderer->doc .= "</div>";
395                            break;
396
397                    }
398
399
400                    break;
401
402            }
403            return true;
404        }
405
406
407        // unsupported $mode
408        return false;
409
410    }
411
412
413}
414
415