1<?php 2 3 4require_once(__DIR__ . "/../class/Analytics.php"); 5require_once(__DIR__ . "/../class/PluginUtility.php"); 6require_once(__DIR__ . "/../class/LinkUtility.php"); 7require_once(__DIR__ . "/../class/XhtmlUtility.php"); 8 9use ComboStrap\CallStack; 10use ComboStrap\LinkUtility; 11use ComboStrap\PluginUtility; 12use ComboStrap\Tag; 13use ComboStrap\TagAttributes; 14 15if (!defined('DOKU_INC')) die(); 16 17/** 18 * 19 * A link pattern to take over the link of Dokuwiki 20 * and transform it as a bootstrap link 21 * 22 * The handle of the move of link is to be found in the 23 * admin action {@link action_plugin_combo_linkmove} 24 * 25 */ 26class syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin 27{ 28 const TAG = 'link'; 29 const COMPONENT = 'combo_link'; 30 31 /** 32 * Disable the link 33 */ 34 const CONF_DISABLE_LINK = "disableLink"; 35 36 /** 37 * The link Tag 38 * a or p 39 */ 40 const LINK_TAG = "linkTag"; 41 42 /** 43 * Do the link component allows to be spawn on multilines 44 */ 45 const CLICKABLE_ATTRIBUTE = "clickable"; 46 47 48 /** 49 * Syntax Type. 50 * 51 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 52 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 53 */ 54 function getType() 55 { 56 return 'substition'; 57 } 58 59 /** 60 * How Dokuwiki will add P element 61 * 62 * * 'normal' - The plugin can be used inside paragraphs 63 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 64 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 65 * 66 * @see DokuWiki_Syntax_Plugin::getPType() 67 */ 68 function getPType() 69 { 70 return 'normal'; 71 } 72 73 /** 74 * @return array 75 * Allow which kind of plugin inside 76 * 77 * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 78 * because we manage self the content and we call self the parser 79 */ 80 function getAllowedTypes() 81 { 82 return array('substition', 'formatting', 'disabled'); 83 } 84 85 public function accepts($mode) 86 { 87 /** 88 * To avoid that the description if it contains a link 89 * will be taken by the links mode 90 * 91 * For instance, [[https://hallo|https://hallo]] will send https://hallo 92 * to the external link mode 93 */ 94 $linkModes = [ 95 "externallink", 96 "locallink", 97 "internallink", 98 "interwikilink", 99 "emaillink", 100 "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description 101 //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet 102 //"emphasis_close", 103 //"acrnonym" 104 ]; 105 if (in_array($mode, $linkModes)) { 106 return false; 107 } else { 108 return true; 109 } 110 } 111 112 113 /** 114 * @see Doku_Parser_Mode::getSort() 115 * The mode with the lowest sort number will win out 116 */ 117 function getSort() 118 { 119 return 100; 120 } 121 122 123 function connectTo($mode) 124 { 125 126 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 127 $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE; 128 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 129 } 130 131 } 132 133 public function postConnect() 134 { 135 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 136 $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent())); 137 } 138 } 139 140 141 /** 142 * The handler for an internal link 143 * based on `internallink` in {@link Doku_Handler} 144 * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 145 * the parameters (ie for instance internallink) 146 * @param string $match 147 * @param int $state 148 * @param int $pos 149 * @param Doku_Handler $handler 150 * @return array|bool 151 */ 152 function handle($match, $state, $pos, Doku_Handler $handler) 153 { 154 155 156 switch ($state) { 157 case DOKU_LEXER_ENTER: 158 $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match)); 159 $callStack = CallStack::createFromHandler($handler); 160 161 162 $parent = $callStack->moveToParent(); 163 $parentName = ""; 164 if ($parent != false) { 165 166 /** 167 * Button Link 168 * Getting the attributes 169 */ 170 $parentName = $parent->getTagName(); 171 if ($parentName == syntax_plugin_combo_button::TAG) { 172 $tagAttributes->mergeWithCallStackArray($parent->getAttributes()); 173 } 174 175 /** 176 * Searching Clickable parent 177 */ 178 $maxLevel = 3; 179 $level = 0; 180 while ( 181 $parent != false && 182 !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 183 $level < $maxLevel 184 ) { 185 $parent = $callStack->moveToParent(); 186 $level++; 187 } 188 if ($parent != false) { 189 if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 190 $tagAttributes->addClassName("stretched-link"); 191 $parent->addClassName("position-relative"); 192 $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 193 } 194 } 195 196 } 197 198 $link = new LinkUtility($tagAttributes->getValue(LinkUtility::ATTRIBUTE_REF)); 199 $linkTag = $link->getHtmlTag(); 200 return array( 201 PluginUtility::STATE => $state, 202 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 203 PluginUtility::CONTEXT => $parentName, 204 self::LINK_TAG => $linkTag 205 ); 206 case DOKU_LEXER_UNMATCHED: 207 208 $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 209 /** 210 * Delete the separator `|` between the ref and the description if any 211 */ 212 $tag = new Tag(self::TAG, array(), $state, $handler); 213 $parent = $tag->getParent(); 214 if ($parent->getName() == self::TAG) { 215 if (strpos($match, '|') === 0) { 216 $data[PluginUtility::PAYLOAD] = substr($match, 1); 217 } 218 } 219 return $data; 220 221 case DOKU_LEXER_EXIT: 222 $callStack = CallStack::createFromHandler($handler); 223 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 224 $openingAttributes = $openingTag->getAttributes(); 225 $linkTag = $openingTag->getPluginData()[self::LINK_TAG]; 226 $openingPosition = $openingTag->getKey(); 227 228 $callStack->moveToEnd(); 229 $previousCall = $callStack->previous(); 230 $previousCallPosition = $previousCall->getKey(); 231 $previousCallContent = $previousCall->getCapturedContent(); 232 233 if ( 234 $openingPosition == $previousCallPosition // ie [[id]] 235 || 236 ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 237 ) { 238 // There is no name 239 $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]); 240 $linkName = $link->getName(); 241 } else { 242 $linkName = ""; 243 } 244 return array( 245 PluginUtility::STATE => $state, 246 PluginUtility::ATTRIBUTES => $openingAttributes, 247 PluginUtility::PAYLOAD => $linkName, 248 PluginUtility::CONTEXT => $openingTag->getContext(), 249 self::LINK_TAG => $linkTag 250 ); 251 } 252 return true; 253 254 255 } 256 257 /** 258 * Render the output 259 * @param string $format 260 * @param Doku_Renderer $renderer 261 * @param array $data - what the function handle() return'ed 262 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 263 * @see DokuWiki_Syntax_Plugin::render() 264 * 265 * 266 */ 267 function render($format, Doku_Renderer $renderer, $data) 268 { 269 // The data 270 switch ($format) { 271 case 'xhtml': 272 273 /** @var Doku_Renderer_xhtml $renderer */ 274 /** 275 * Cache problem may occurs while releasing 276 */ 277 if (isset($data[PluginUtility::ATTRIBUTES])) { 278 $callStackAttributes = $data[PluginUtility::ATTRIBUTES]; 279 } else { 280 $callStackAttributes = $data; 281 } 282 283 PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 284 285 $state = $data[PluginUtility::STATE]; 286 switch ($state) { 287 case DOKU_LEXER_ENTER: 288 $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG); 289 $ref = $tagAttributes->getValueAndRemove(LinkUtility::ATTRIBUTE_REF); 290 $link = new LinkUtility($ref, $tagAttributes); 291 292 /** 293 * Extra styling 294 */ 295 $parentTag = $data[PluginUtility::CONTEXT]; 296 switch ($parentTag) { 297 /** 298 * Button link 299 */ 300 case syntax_plugin_combo_button::TAG: 301 $tagAttributes->addHtmlAttributeValue("role", "button"); 302 syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes); 303 $htmlLink = $link->renderOpenTag($renderer); 304 break; 305 case syntax_plugin_combo_badge::TAG: 306 case syntax_plugin_combo_cite::TAG: 307 case syntax_plugin_combo_contentlistitem::DOKU_TAG: 308 case syntax_plugin_combo_preformatted::TAG: 309 $htmlLink = $link->renderOpenTag($renderer); 310 break; 311 case syntax_plugin_combo_dropdown::TAG: 312 $tagAttributes->addClassName("dropdown-item"); 313 $htmlLink = $link->renderOpenTag($renderer); 314 break; 315 case syntax_plugin_combo_navbarcollapse::COMPONENT: 316 $tagAttributes->addClassName("navbar-link"); 317 $htmlLink = '<div class="navbar-nav">' . $link->renderOpenTag($renderer); 318 break; 319 case syntax_plugin_combo_navbargroup::COMPONENT: 320 $tagAttributes->addClassName("nav-link"); 321 $htmlLink = '<li class="nav-item">' . $link->renderOpenTag($renderer); 322 break; 323 default: 324 $htmlLink = $link->renderOpenTag($renderer); 325 326 } 327 328 329 /** 330 * Add it to the rendering 331 */ 332 $renderer->doc .= $htmlLink; 333 break; 334 case DOKU_LEXER_UNMATCHED: 335 $renderer->doc .= PluginUtility::renderUnmatched($data); 336 break; 337 case DOKU_LEXER_EXIT: 338 339 // if there is no link name defined, we get the name as ref in the payload 340 // otherwise null string 341 $renderer->doc .= $data[PluginUtility::PAYLOAD];; 342 343 // Close the link 344 $linkTag = $data[self::LINK_TAG]; 345 $renderer->doc .= "</$linkTag>"; 346 347 // Close the html wrapper element 348 $context = $data[PluginUtility::CONTEXT]; 349 switch ($context) { 350 case syntax_plugin_combo_navbarcollapse::COMPONENT: 351 $renderer->doc .= '</div>'; 352 break; 353 case syntax_plugin_combo_navbargroup::COMPONENT: 354 $renderer->doc .= '</li>'; 355 break; 356 } 357 358 359 } 360 361 362 return true; 363 364 case 'metadata': 365 366 $state = $data[PluginUtility::STATE]; 367 if ($state == DOKU_LEXER_ENTER) { 368 /** 369 * Keep track of the backlinks ie meta['relation']['references'] 370 * @var Doku_Renderer_metadata $renderer 371 */ 372 if (isset($data[PluginUtility::ATTRIBUTES])) { 373 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 374 } else { 375 $tagAttributes = $data; 376 } 377 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 378 379 $link = new LinkUtility($ref); 380 $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME]; 381 if ($name != null) { 382 $link->setName($name); 383 } 384 $link->handleMetadata($renderer); 385 386 return true; 387 } 388 break; 389 390 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 391 392 $state = $data[PluginUtility::STATE]; 393 if ($state == DOKU_LEXER_ENTER) { 394 /** 395 * 396 * @var renderer_plugin_combo_analytics $renderer 397 */ 398 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 399 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 400 $link = new LinkUtility($ref); 401 $link->processLinkStats($renderer->stats); 402 break; 403 } 404 405 } 406 // unsupported $mode 407 return false; 408 } 409 410 411} 412 413