1*04fd306cSNickeau<?php 2*04fd306cSNickeau/** 3*04fd306cSNickeau * DokuWiki Syntax Plugin Combostrap. 4*04fd306cSNickeau * Implementation of https://getbootstrap.com/docs/5.0/content/typography/#blockquotes 5*04fd306cSNickeau * 6*04fd306cSNickeau */ 7*04fd306cSNickeau 8*04fd306cSNickeaunamespace ComboStrap; 9*04fd306cSNickeau 10*04fd306cSNickeau 11*04fd306cSNickeauuse ComboStrap\Tag\BoxTag; 12*04fd306cSNickeauuse Doku_Handler; 13*04fd306cSNickeauuse Doku_Renderer_xhtml; 14*04fd306cSNickeauuse syntax_plugin_combo_cite; 15*04fd306cSNickeauuse syntax_plugin_combo_header; 16*04fd306cSNickeauuse syntax_plugin_combo_link; 17*04fd306cSNickeau 18*04fd306cSNickeau 19*04fd306cSNickeau/** 20*04fd306cSNickeau * All DokuWiki plugins to extend the parser/rendering mechanism 21*04fd306cSNickeau * need to inherit from this class 22*04fd306cSNickeau * 23*04fd306cSNickeau * The name of the class must follow a pattern (don't change it) 24*04fd306cSNickeau * ie: 25*04fd306cSNickeau * syntax_plugin_PluginName_ComponentName 26*04fd306cSNickeau */ 27*04fd306cSNickeauclass BlockquoteTag 28*04fd306cSNickeau{ 29*04fd306cSNickeau 30*04fd306cSNickeau const TAG = "blockquote"; 31*04fd306cSNickeau 32*04fd306cSNickeau /** 33*04fd306cSNickeau * When the blockquote is a tweet 34*04fd306cSNickeau */ 35*04fd306cSNickeau const TWEET = "tweet"; 36*04fd306cSNickeau const TWEET_SUPPORTED_LANG = array("en", "ar", "bn", "cs", "da", "de", "el", "es", "fa", "fi", "fil", "fr", "he", "hi", "hu", "id", "it", "ja", "ko", "msa", "nl", "no", "pl", "pt", "ro", "ru", "sv", "th", "tr", "uk", "ur", "vi", "zh-cn", "zh-tw"); 37*04fd306cSNickeau const CONF_TWEET_WIDGETS_THEME = "twitter:widgets:theme"; 38*04fd306cSNickeau const CONF_TWEET_WIDGETS_THEME_DEFAULT = "light"; 39*04fd306cSNickeau const CONF_TWEET_WIDGETS_BORDER = "twitter:widgets:border-color"; 40*04fd306cSNickeau const TYPO_TYPE = "typo"; 41*04fd306cSNickeau const CARD_TYPE = "card"; 42*04fd306cSNickeau const CONF_TWEET_WIDGETS_BORDER_DEFAULT = "#55acee"; 43*04fd306cSNickeau 44*04fd306cSNickeau 45*04fd306cSNickeau /** 46*04fd306cSNickeau * @var mixed|string 47*04fd306cSNickeau */ 48*04fd306cSNickeau static public $type = self::CARD_TYPE; 49*04fd306cSNickeau 50*04fd306cSNickeau 51*04fd306cSNickeau static function handleEnter($handler): array 52*04fd306cSNickeau { 53*04fd306cSNickeau /** 54*04fd306cSNickeau * Parent 55*04fd306cSNickeau */ 56*04fd306cSNickeau $callStack = CallStack::createFromHandler($handler); 57*04fd306cSNickeau $context = null; 58*04fd306cSNickeau $parent = $callStack->moveToParent(); 59*04fd306cSNickeau if ($parent !== false) { 60*04fd306cSNickeau $context = $parent->getTagName(); 61*04fd306cSNickeau if ($context === FragmentTag::FRAGMENT_TAG) { 62*04fd306cSNickeau $parent = $callStack->moveToParent(); 63*04fd306cSNickeau if ($parent !== false) { 64*04fd306cSNickeau $context = $parent->getTagName(); 65*04fd306cSNickeau } 66*04fd306cSNickeau } 67*04fd306cSNickeau } 68*04fd306cSNickeau 69*04fd306cSNickeau return array(PluginUtility::CONTEXT => $context); 70*04fd306cSNickeau 71*04fd306cSNickeau } 72*04fd306cSNickeau 73*04fd306cSNickeau 74*04fd306cSNickeau static public function handleExit(Doku_Handler $handler): array 75*04fd306cSNickeau { 76*04fd306cSNickeau $callStack = CallStack::createFromHandler($handler); 77*04fd306cSNickeau 78*04fd306cSNickeau /** 79*04fd306cSNickeau * Check and add a scroll toggle if the 80*04fd306cSNickeau * blockquote is constrained by height 81*04fd306cSNickeau */ 82*04fd306cSNickeau Dimension::addScrollToggleOnClickIfNoControl($callStack); 83*04fd306cSNickeau 84*04fd306cSNickeau 85*04fd306cSNickeau /** 86*04fd306cSNickeau * Pre-parsing: 87*04fd306cSNickeau * Cite: A cite should be wrapped into a footer 88*04fd306cSNickeau * This should happens before the p processing because we 89*04fd306cSNickeau * are adding a {@link BoxTag} which is a stack 90*04fd306cSNickeau * Tweet blockquote: If a link has tweet link status, this is a tweet blockquote 91*04fd306cSNickeau */ 92*04fd306cSNickeau $callStack->moveToEnd(); 93*04fd306cSNickeau $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 94*04fd306cSNickeau $tweetUrlFound = false; 95*04fd306cSNickeau while ($actualCall = $callStack->next()) { 96*04fd306cSNickeau if ($actualCall->getTagName() == syntax_plugin_combo_cite::TAG) { 97*04fd306cSNickeau switch ($actualCall->getState()) { 98*04fd306cSNickeau case DOKU_LEXER_ENTER: 99*04fd306cSNickeau // insert before 100*04fd306cSNickeau $callStack->insertBefore(Call::createComboCall( 101*04fd306cSNickeau BoxTag::TAG, 102*04fd306cSNickeau DOKU_LEXER_ENTER, 103*04fd306cSNickeau array( 104*04fd306cSNickeau "class" => "blockquote-footer", 105*04fd306cSNickeau BoxTag::HTML_TAG_ATTRIBUTE => "footer" 106*04fd306cSNickeau ), 107*04fd306cSNickeau null, 108*04fd306cSNickeau null, 109*04fd306cSNickeau null, 110*04fd306cSNickeau null, 111*04fd306cSNickeau \syntax_plugin_combo_xmlblocktag::TAG 112*04fd306cSNickeau )); 113*04fd306cSNickeau break; 114*04fd306cSNickeau case DOKU_LEXER_EXIT: 115*04fd306cSNickeau // insert after 116*04fd306cSNickeau $callStack->insertAfter(Call::createComboCall( 117*04fd306cSNickeau BoxTag::TAG, 118*04fd306cSNickeau DOKU_LEXER_EXIT, 119*04fd306cSNickeau array( 120*04fd306cSNickeau BoxTag::HTML_TAG_ATTRIBUTE => "footer" 121*04fd306cSNickeau ), 122*04fd306cSNickeau null, 123*04fd306cSNickeau null, 124*04fd306cSNickeau null, 125*04fd306cSNickeau null, 126*04fd306cSNickeau \syntax_plugin_combo_xmlblocktag::TAG 127*04fd306cSNickeau )); 128*04fd306cSNickeau break; 129*04fd306cSNickeau } 130*04fd306cSNickeau } 131*04fd306cSNickeau if ( 132*04fd306cSNickeau $actualCall->getTagName() == syntax_plugin_combo_link::TAG 133*04fd306cSNickeau && $actualCall->getState() == DOKU_LEXER_ENTER 134*04fd306cSNickeau ) { 135*04fd306cSNickeau $ref = $actualCall->getAttribute(syntax_plugin_combo_link::MARKUP_REF_ATTRIBUTE); 136*04fd306cSNickeau if (StringUtility::match($ref, "https:\/\/twitter.com\/[^\/]*\/status\/.*")) { 137*04fd306cSNickeau $tweetUrlFound = true; 138*04fd306cSNickeau } 139*04fd306cSNickeau } 140*04fd306cSNickeau } 141*04fd306cSNickeau if ($tweetUrlFound) { 142*04fd306cSNickeau $context = BlockquoteTag::TWEET; 143*04fd306cSNickeau $type = $context; 144*04fd306cSNickeau $openingTag->setType($context); 145*04fd306cSNickeau $openingTag->setContext($context); 146*04fd306cSNickeau } 147*04fd306cSNickeau 148*04fd306cSNickeau /** 149*04fd306cSNickeau * Because we can change the type above to tweet 150*04fd306cSNickeau * we set them after 151*04fd306cSNickeau */ 152*04fd306cSNickeau $type = $openingTag->getType(); 153*04fd306cSNickeau $context = $openingTag->getContext(); 154*04fd306cSNickeau if ($context === null) { 155*04fd306cSNickeau $context = $type; 156*04fd306cSNickeau } 157*04fd306cSNickeau $attributes = $openingTag->getAttributes(); 158*04fd306cSNickeau 159*04fd306cSNickeau /** 160*04fd306cSNickeau * Create the paragraph 161*04fd306cSNickeau */ 162*04fd306cSNickeau $callStack->moveToPreviousCorrespondingOpeningCall(); 163*04fd306cSNickeau $callStack->insertEolIfNextCallIsNotEolOrBlock(); // eol is mandatory to have a paragraph if there is only content 164*04fd306cSNickeau $paragraphAttributes["class"] = "blockquote-text"; 165*04fd306cSNickeau if ($type == self::TYPO_TYPE) { 166*04fd306cSNickeau $bootstrapVersion = Bootstrap::getBootStrapMajorVersion(); 167*04fd306cSNickeau if ($bootstrapVersion == Bootstrap::BootStrapFourMajorVersion) { 168*04fd306cSNickeau // As seen here https://getbootstrap.com/docs/4.0/content/typography/#blockquotes 169*04fd306cSNickeau $paragraphAttributes["class"] .= " mb-0"; 170*04fd306cSNickeau // not on 5 https://getbootstrap.com/docs/5.0/content/typography/#blockquotes 171*04fd306cSNickeau } 172*04fd306cSNickeau } 173*04fd306cSNickeau $callStack->processEolToEndStack($paragraphAttributes); 174*04fd306cSNickeau 175*04fd306cSNickeau /** 176*04fd306cSNickeau * Wrap the blockquote into a card 177*04fd306cSNickeau * 178*04fd306cSNickeau * In a blockquote card, a blockquote typo is wrapped around a card 179*04fd306cSNickeau * 180*04fd306cSNickeau * We add then: 181*04fd306cSNickeau * * at the body location: a card body start and a blockquote typo start 182*04fd306cSNickeau * * at the end location: a card end body and a blockquote end typo 183*04fd306cSNickeau */ 184*04fd306cSNickeau if ($type == self::CARD_TYPE) { 185*04fd306cSNickeau 186*04fd306cSNickeau $callStack->moveToPreviousCorrespondingOpeningCall(); 187*04fd306cSNickeau $callEnterTypeCall = Call::createComboCall( 188*04fd306cSNickeau self::TAG, 189*04fd306cSNickeau DOKU_LEXER_ENTER, 190*04fd306cSNickeau array(TagAttributes::TYPE_KEY => self::TYPO_TYPE), 191*04fd306cSNickeau $context, 192*04fd306cSNickeau null, 193*04fd306cSNickeau null, 194*04fd306cSNickeau null, 195*04fd306cSNickeau \syntax_plugin_combo_xmlblocktag::TAG 196*04fd306cSNickeau ); 197*04fd306cSNickeau $cardBodyEnterCall = CardTag::createCardBodyEnterCall($context); 198*04fd306cSNickeau $firstChild = $callStack->moveToFirstChildTag(); 199*04fd306cSNickeau 200*04fd306cSNickeau if ($firstChild !== false) { 201*04fd306cSNickeau if ($firstChild->getTagName() == syntax_plugin_combo_header::TAG) { 202*04fd306cSNickeau $callStack->moveToNextSiblingTag(); 203*04fd306cSNickeau } 204*04fd306cSNickeau // Head: Insert card body 205*04fd306cSNickeau $callStack->insertBefore($cardBodyEnterCall); 206*04fd306cSNickeau // Head: Insert Blockquote typo 207*04fd306cSNickeau $callStack->insertBefore($callEnterTypeCall); 208*04fd306cSNickeau 209*04fd306cSNickeau } else { 210*04fd306cSNickeau // No child 211*04fd306cSNickeau // Move back 212*04fd306cSNickeau $callStack->moveToEnd();; 213*04fd306cSNickeau $callStack->moveToPreviousCorrespondingOpeningCall(); 214*04fd306cSNickeau // Head: Insert Blockquote typo 215*04fd306cSNickeau $callStack->insertAfter($callEnterTypeCall); 216*04fd306cSNickeau // Head: Insert card body 217*04fd306cSNickeau $callStack->insertAfter($cardBodyEnterCall); 218*04fd306cSNickeau } 219*04fd306cSNickeau 220*04fd306cSNickeau 221*04fd306cSNickeau /** 222*04fd306cSNickeau * End 223*04fd306cSNickeau */ 224*04fd306cSNickeau // Insert the card body exit 225*04fd306cSNickeau $callStack->moveToEnd(); 226*04fd306cSNickeau $callStack->insertBefore( 227*04fd306cSNickeau Call::createComboCall( 228*04fd306cSNickeau self::TAG, 229*04fd306cSNickeau DOKU_LEXER_EXIT, 230*04fd306cSNickeau array("type" => self::TYPO_TYPE), 231*04fd306cSNickeau $context, 232*04fd306cSNickeau null, 233*04fd306cSNickeau null, 234*04fd306cSNickeau null, 235*04fd306cSNickeau \syntax_plugin_combo_xmlblocktag::TAG 236*04fd306cSNickeau ) 237*04fd306cSNickeau ); 238*04fd306cSNickeau $callStack->insertBefore(CardTag::createCardBodyExitCall()); 239*04fd306cSNickeau } 240*04fd306cSNickeau 241*04fd306cSNickeau return array( 242*04fd306cSNickeau PluginUtility::CONTEXT => $context, 243*04fd306cSNickeau PluginUtility::ATTRIBUTES => $attributes 244*04fd306cSNickeau ); 245*04fd306cSNickeau } 246*04fd306cSNickeau 247*04fd306cSNickeau public static function renderEnterXhtml(TagAttributes $tagAttributes, $data, $renderer): string 248*04fd306cSNickeau { 249*04fd306cSNickeau /** 250*04fd306cSNickeau * Add the CSS 251*04fd306cSNickeau */ 252*04fd306cSNickeau $snippetManager = PluginUtility::getSnippetManager(); 253*04fd306cSNickeau $snippetManager->attachCssInternalStyleSheet(self::TAG); 254*04fd306cSNickeau 255*04fd306cSNickeau /** 256*04fd306cSNickeau * Create the HTML 257*04fd306cSNickeau */ 258*04fd306cSNickeau $type = $tagAttributes->getType(); 259*04fd306cSNickeau switch ($type) { 260*04fd306cSNickeau case self::TYPO_TYPE: 261*04fd306cSNickeau 262*04fd306cSNickeau $tagAttributes->addClassName("blockquote"); 263*04fd306cSNickeau $cardTags = [CardTag::CARD_TAG, MasonryTag::MASONRY_TAG]; 264*04fd306cSNickeau if (in_array($data[PluginUtility::CONTEXT], $cardTags)) { 265*04fd306cSNickeau // As seen here: https://getbootstrap.com/docs/5.0/components/card/#header-and-footer 266*04fd306cSNickeau // A blockquote in a card 267*04fd306cSNickeau // This context is added dynamically when the blockquote is a card type 268*04fd306cSNickeau $tagAttributes->addClassName("mb-0"); 269*04fd306cSNickeau } 270*04fd306cSNickeau return $tagAttributes->toHtmlEnterTag("blockquote"); 271*04fd306cSNickeau 272*04fd306cSNickeau case self::TWEET: 273*04fd306cSNickeau 274*04fd306cSNickeau try { 275*04fd306cSNickeau PluginUtility::getSnippetManager() 276*04fd306cSNickeau ->attachRemoteJavascriptLibrary(self::TWEET, "https://platform.twitter.com/widgets.js") 277*04fd306cSNickeau ->addHtmlAttribute("id", "twitter-wjs"); 278*04fd306cSNickeau } catch (ExceptionBadArgument|ExceptionBadSyntax $e) { 279*04fd306cSNickeau LogUtility::internalError("It should not happen as the url is written by ons (ie is a literal)", self::TAG, $e); 280*04fd306cSNickeau } 281*04fd306cSNickeau 282*04fd306cSNickeau $tagAttributes->addClassName("twitter-tweet"); 283*04fd306cSNickeau 284*04fd306cSNickeau $tweetAttributesNames = ["cards", "dnt", "conversation", "align", "width", "theme", "lang"]; 285*04fd306cSNickeau foreach ($tweetAttributesNames as $tweetAttributesName) { 286*04fd306cSNickeau if ($tagAttributes->hasComponentAttribute($tweetAttributesName)) { 287*04fd306cSNickeau $value = $tagAttributes->getValueAndRemove($tweetAttributesName); 288*04fd306cSNickeau $tagAttributes->addOutputAttributeValue("data-" . $tweetAttributesName, $value); 289*04fd306cSNickeau } 290*04fd306cSNickeau } 291*04fd306cSNickeau 292*04fd306cSNickeau return $tagAttributes->toHtmlEnterTag("blockquote"); 293*04fd306cSNickeau 294*04fd306cSNickeau case self::CARD_TYPE: 295*04fd306cSNickeau default: 296*04fd306cSNickeau 297*04fd306cSNickeau /** 298*04fd306cSNickeau * Wrap with column 299*04fd306cSNickeau */ 300*04fd306cSNickeau $context = $data[PluginUtility::CONTEXT]; 301*04fd306cSNickeau if ($context === MasonryTag::MASONRY_TAG) { 302*04fd306cSNickeau MasonryTag::addColIfBootstrap5AndCardColumns($renderer, $context); 303*04fd306cSNickeau } 304*04fd306cSNickeau 305*04fd306cSNickeau /** 306*04fd306cSNickeau * Starting the card 307*04fd306cSNickeau */ 308*04fd306cSNickeau $tagAttributes->addClassName(self::CARD_TYPE); 309*04fd306cSNickeau return $tagAttributes->toHtmlEnterTag("div") . DOKU_LF; 310*04fd306cSNickeau /** 311*04fd306cSNickeau * The card body and blockquote body 312*04fd306cSNickeau * of the example (https://getbootstrap.com/docs/4.0/components/card/#header-and-footer) 313*04fd306cSNickeau * are added via call at 314*04fd306cSNickeau * the {@link DOKU_LEXER_EXIT} state of {@link BlockquoteTag::handle()} 315*04fd306cSNickeau */ 316*04fd306cSNickeau } 317*04fd306cSNickeau } 318*04fd306cSNickeau 319*04fd306cSNickeau 320*04fd306cSNickeau static function renderExitXhtml(TagAttributes $tagAttributes, Doku_Renderer_xhtml $renderer, array $data) 321*04fd306cSNickeau { 322*04fd306cSNickeau // Because we can have several unmatched on a line we don't know if 323*04fd306cSNickeau // there is a eol 324*04fd306cSNickeau StringUtility::addEolCharacterIfNotPresent($renderer->doc); 325*04fd306cSNickeau $type = $tagAttributes->getValue(TagAttributes::TYPE_KEY); 326*04fd306cSNickeau switch ($type) { 327*04fd306cSNickeau case self::CARD_TYPE: 328*04fd306cSNickeau $renderer->doc .= "</div>"; 329*04fd306cSNickeau break; 330*04fd306cSNickeau case self::TWEET: 331*04fd306cSNickeau case self::TYPO_TYPE: 332*04fd306cSNickeau default: 333*04fd306cSNickeau $renderer->doc .= "</blockquote>"; 334*04fd306cSNickeau break; 335*04fd306cSNickeau } 336*04fd306cSNickeau 337*04fd306cSNickeau /** 338*04fd306cSNickeau * Closing the masonry column 339*04fd306cSNickeau * (Only if this is a card blockquote) 340*04fd306cSNickeau */ 341*04fd306cSNickeau if ($type == CardTag::CARD_TAG) { 342*04fd306cSNickeau $context = $data[PluginUtility::CONTEXT]; 343*04fd306cSNickeau if ($context === MasonryTag::MASONRY_TAG) { 344*04fd306cSNickeau MasonryTag::endColIfBootstrap5AnCardColumns($renderer, $context); 345*04fd306cSNickeau } 346*04fd306cSNickeau } 347*04fd306cSNickeau 348*04fd306cSNickeau } 349*04fd306cSNickeau 350*04fd306cSNickeau 351*04fd306cSNickeau} 352