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 component 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 /** 120 * It should be less than the number 121 * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort} 122 * and the like 123 * 124 * For whatever reason, the number below should be less than 100, 125 * otherwise on windows with DokuWiki Stick, the link syntax may be not taken 126 * into account 127 */ 128 return 99; 129 } 130 131 132 function connectTo($mode) 133 { 134 135 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 136 $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE; 137 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 138 } 139 140 } 141 142 public function postConnect() 143 { 144 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 145 $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent())); 146 } 147 } 148 149 150 /** 151 * The handler for an internal link 152 * based on `internallink` in {@link Doku_Handler} 153 * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 154 * the parameters (ie for instance internallink) 155 * @param string $match 156 * @param int $state 157 * @param int $pos 158 * @param Doku_Handler $handler 159 * @return array|bool 160 */ 161 function handle($match, $state, $pos, Doku_Handler $handler) 162 { 163 164 165 switch ($state) { 166 case DOKU_LEXER_ENTER: 167 $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match)); 168 $callStack = CallStack::createFromHandler($handler); 169 170 171 $parent = $callStack->moveToParent(); 172 $parentName = ""; 173 if ($parent != false) { 174 175 /** 176 * Button Link 177 * Getting the attributes 178 */ 179 $parentName = $parent->getTagName(); 180 if ($parentName == syntax_plugin_combo_button::TAG) { 181 $tagAttributes->mergeWithCallStackArray($parent->getAttributes()); 182 } 183 184 /** 185 * Searching Clickable parent 186 */ 187 $maxLevel = 3; 188 $level = 0; 189 while ( 190 $parent != false && 191 !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 192 $level < $maxLevel 193 ) { 194 $parent = $callStack->moveToParent(); 195 $level++; 196 } 197 if ($parent != false) { 198 if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 199 $tagAttributes->addClassName("stretched-link"); 200 $parent->addClassName("position-relative"); 201 $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 202 } 203 } 204 205 } 206 $callStackAttributes = $tagAttributes->toCallStackArray(); 207 return array( 208 PluginUtility::STATE => $state, 209 PluginUtility::ATTRIBUTES => $callStackAttributes, 210 PluginUtility::CONTEXT => $parentName 211 ); 212 case DOKU_LEXER_UNMATCHED: 213 214 $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 215 /** 216 * Delete the separator `|` between the ref and the description if any 217 */ 218 $tag = new Tag(self::TAG, array(), $state, $handler); 219 $parent = $tag->getParent(); 220 if ($parent->getName() == self::TAG) { 221 if (strpos($match, '|') === 0) { 222 $data[PluginUtility::PAYLOAD] = substr($match, 1); 223 } 224 } 225 return $data; 226 227 case DOKU_LEXER_EXIT: 228 $callStack = CallStack::createFromHandler($handler); 229 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 230 $openingAttributes = $openingTag->getAttributes(); 231 $openingPosition = $openingTag->getKey(); 232 233 $callStack->moveToEnd(); 234 $previousCall = $callStack->previous(); 235 $previousCallPosition = $previousCall->getKey(); 236 $previousCallContent = $previousCall->getCapturedContent(); 237 238 if ( 239 $openingPosition == $previousCallPosition // ie [[id]] 240 || 241 ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 242 ) { 243 // There is no name 244 $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]); 245 $linkName = $link->getName(); 246 } else { 247 $linkName = ""; 248 } 249 return array( 250 PluginUtility::STATE => $state, 251 PluginUtility::ATTRIBUTES => $openingAttributes, 252 PluginUtility::PAYLOAD => $linkName, 253 PluginUtility::CONTEXT => $openingTag->getContext() 254 ); 255 } 256 return true; 257 258 259 } 260 261 /** 262 * Render the output 263 * @param string $format 264 * @param Doku_Renderer $renderer 265 * @param array $data - what the function handle() return'ed 266 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 267 * @see DokuWiki_Syntax_Plugin::render() 268 * 269 * 270 */ 271 function render($format, Doku_Renderer $renderer, $data) 272 { 273 // The data 274 switch ($format) { 275 case 'xhtml': 276 277 /** @var Doku_Renderer_xhtml $renderer */ 278 /** 279 * Cache problem may occurs while releasing 280 */ 281 if (isset($data[PluginUtility::ATTRIBUTES])) { 282 $callStackAttributes = $data[PluginUtility::ATTRIBUTES]; 283 } else { 284 $callStackAttributes = $data; 285 } 286 287 PluginUtility::getSnippetManager()->attachCssSnippetForBar(self::TAG); 288 289 $state = $data[PluginUtility::STATE]; 290 switch ($state) { 291 case DOKU_LEXER_ENTER: 292 $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG); 293 $ref = $tagAttributes->getValueAndRemove(LinkUtility::ATTRIBUTE_REF); 294 $link = new LinkUtility($ref, $tagAttributes); 295 296 /** 297 * Extra styling 298 */ 299 $parentTag = $data[PluginUtility::CONTEXT]; 300 switch ($parentTag) { 301 /** 302 * Button link 303 */ 304 case syntax_plugin_combo_button::TAG: 305 $tagAttributes->addHtmlAttributeValue("role", "button"); 306 syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes); 307 $htmlLink = $link->renderOpenTag($renderer); 308 break; 309 case syntax_plugin_combo_badge::TAG: 310 case syntax_plugin_combo_cite::TAG: 311 case syntax_plugin_combo_contentlistitem::DOKU_TAG: 312 case syntax_plugin_combo_preformatted::TAG: 313 $htmlLink = $link->renderOpenTag($renderer); 314 break; 315 case syntax_plugin_combo_dropdown::TAG: 316 $tagAttributes->addClassName("dropdown-item"); 317 $htmlLink = $link->renderOpenTag($renderer); 318 break; 319 case syntax_plugin_combo_navbarcollapse::COMPONENT: 320 $tagAttributes->addClassName("navbar-link"); 321 $htmlLink = '<div class="navbar-nav">' . $link->renderOpenTag($renderer); 322 break; 323 case syntax_plugin_combo_navbargroup::COMPONENT: 324 $tagAttributes->addClassName("nav-link"); 325 $htmlLink = '<li class="nav-item">' . $link->renderOpenTag($renderer); 326 break; 327 default: 328 $htmlLink = $link->renderOpenTag($renderer); 329 330 } 331 332 333 /** 334 * Add it to the rendering 335 */ 336 $renderer->doc .= $htmlLink; 337 break; 338 case DOKU_LEXER_UNMATCHED: 339 $renderer->doc .= PluginUtility::renderUnmatched($data); 340 break; 341 case DOKU_LEXER_EXIT: 342 343 // if there is no link name defined, we get the name as ref in the payload 344 // otherwise null string 345 $renderer->doc .= $data[PluginUtility::PAYLOAD]; 346 347 // Close the link 348 $renderer->doc .= "</a>"; 349 350 // Close the html wrapper element 351 $context = $data[PluginUtility::CONTEXT]; 352 switch ($context) { 353 case syntax_plugin_combo_navbarcollapse::COMPONENT: 354 $renderer->doc .= '</div>'; 355 break; 356 case syntax_plugin_combo_navbargroup::COMPONENT: 357 $renderer->doc .= '</li>'; 358 break; 359 } 360 361 362 } 363 364 365 return true; 366 367 case 'metadata': 368 369 $state = $data[PluginUtility::STATE]; 370 if ($state == DOKU_LEXER_ENTER) { 371 /** 372 * Keep track of the backlinks ie meta['relation']['references'] 373 * @var Doku_Renderer_metadata $renderer 374 */ 375 if (isset($data[PluginUtility::ATTRIBUTES])) { 376 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 377 } else { 378 $tagAttributes = $data; 379 } 380 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 381 382 $link = new LinkUtility($ref); 383 $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME]; 384 if ($name != null) { 385 $link->setName($name); 386 } 387 $link->handleMetadata($renderer); 388 389 return true; 390 } 391 break; 392 393 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 394 395 $state = $data[PluginUtility::STATE]; 396 if ($state == DOKU_LEXER_ENTER) { 397 /** 398 * 399 * @var renderer_plugin_combo_analytics $renderer 400 */ 401 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 402 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 403 $link = new LinkUtility($ref); 404 $link->processLinkStats($renderer->stats); 405 break; 406 } 407 408 } 409 // unsupported $mode 410 return false; 411 } 412 413 414} 415 416