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