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