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/HtmlUtility.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 CONF_ENABLE_MULTI_LINES_LINK = "enableMultiLinesLink"; 46 const CLICKABLE_ATTRIBUTE = "clickable"; 47 48 49 /** 50 * Syntax Type. 51 * 52 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 53 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 54 */ 55 function getType() 56 { 57 return 'substition'; 58 } 59 60 /** 61 * How Dokuwiki will add P element 62 * 63 * * 'normal' - The plugin can be used inside paragraphs 64 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 65 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 66 * 67 * @see DokuWiki_Syntax_Plugin::getPType() 68 */ 69 function getPType() 70 { 71 return 'normal'; 72 } 73 74 /** 75 * @return array 76 * Allow which kind of plugin inside 77 * 78 * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 79 * because we manage self the content and we call self the parser 80 */ 81 function getAllowedTypes() 82 { 83 return array('substition', 'formatting', 'disabled'); 84 } 85 86 public function accepts($mode) 87 { 88 /** 89 * To avoid that the description if it contains a link 90 * will be taken by the links mode 91 * 92 * For instance, [[https://hallo|https://hallo]] will send https://hallo 93 * to the external link mode 94 */ 95 $linkModes = [ 96 "externallink", 97 "locallink", 98 "internallink", 99 "interwikilink", 100 "emaillink", 101 "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description 102 //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet 103 //"emphasis_close", 104 //"acrnonym" 105 ]; 106 if (in_array($mode, $linkModes)) { 107 return false; 108 } else { 109 return true; 110 } 111 } 112 113 114 /** 115 * @see Doku_Parser_Mode::getSort() 116 * The mode with the lowest sort number will win out 117 */ 118 function getSort() 119 { 120 return 100; 121 } 122 123 124 function connectTo($mode) 125 { 126 127 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 128 $pattern = LinkUtility::ENTRY_PATTERN_SINGLE_LINE; 129 if ($this->getConf(self::CONF_ENABLE_MULTI_LINES_LINK, false)) { 130 $pattern = LinkUtility::ENTRY_PATTERN_MULTI_LINE; 131 } 132 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeForComponent($this->getPluginComponent())); 133 } 134 135 } 136 137 public function postConnect() 138 { 139 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 140 $this->Lexer->addExitPattern(LinkUtility::EXIT_PATTERN, PluginUtility::getModeForComponent($this->getPluginComponent())); 141 } 142 } 143 144 145 /** 146 * The handler for an internal link 147 * based on `internallink` in {@link Doku_Handler} 148 * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 149 * the parameters (ie for instance internallink) 150 * @param string $match 151 * @param int $state 152 * @param int $pos 153 * @param Doku_Handler $handler 154 * @return array|bool 155 */ 156 function handle($match, $state, $pos, Doku_Handler $handler) 157 { 158 159 160 switch ($state) { 161 case DOKU_LEXER_ENTER: 162 $tagAttributes = TagAttributes::createFromCallStackArray(LinkUtility::parse($match)); 163 $callStack = CallStack::createFromHandler($handler); 164 165 166 $parent = $callStack->moveToParent(); 167 $parentName = ""; 168 if ($parent != false) { 169 170 /** 171 * Button Link 172 * Getting the attributes 173 */ 174 $parentName = $parent->getTagName(); 175 if ($parentName == syntax_plugin_combo_button::TAG) { 176 $tagAttributes->mergeWithCallStackArray($parent->getAttributes()); 177 } 178 179 /** 180 * Searching Clickable parent 181 */ 182 $maxLevel = 3; 183 $level = 0; 184 while ( 185 $parent != false && 186 !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 187 $level < $maxLevel 188 ) { 189 $parent = $callStack->moveToParent(); 190 $level++; 191 } 192 if ($parent != false) { 193 if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 194 $tagAttributes->addClassName("stretched-link"); 195 $parent->addClassName("position-relative"); 196 $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 197 } 198 } 199 200 } 201 202 $link = new LinkUtility($tagAttributes->getValue(LinkUtility::ATTRIBUTE_REF)); 203 $linkTag = $link->getHtmlTag(); 204 return array( 205 PluginUtility::STATE => $state, 206 PluginUtility::ATTRIBUTES => $tagAttributes->toCallStackArray(), 207 PluginUtility::CONTEXT => $parentName, 208 self::LINK_TAG => $linkTag 209 ); 210 case DOKU_LEXER_UNMATCHED: 211 212 $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 213 /** 214 * Delete the separator `|` between the ref and the description if any 215 */ 216 $tag = new Tag(self::TAG, array(), $state, $handler); 217 $parent = $tag->getParent(); 218 if ($parent->getName() == self::TAG) { 219 if (strpos($match, '|') === 0) { 220 $data[PluginUtility::PAYLOAD] = substr($match, 1); 221 } 222 } 223 return $data; 224 225 case DOKU_LEXER_EXIT: 226 $callStack = CallStack::createFromHandler($handler); 227 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 228 $openingAttributes = $openingTag->getAttributes(); 229 $linkTag = $openingTag->getPluginData()[self::LINK_TAG]; 230 $openingPosition = $openingTag->getKey(); 231 232 $callStack->moveToEnd(); 233 $previousCall = $callStack->previous(); 234 $previousCallPosition = $previousCall->getKey(); 235 $previousCallContent = $previousCall->getCapturedContent(); 236 237 if ( 238 $openingPosition == $previousCallPosition // ie [[id]] 239 || 240 ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 241 ) { 242 // There is no name 243 $link = new LinkUtility($openingAttributes[LinkUtility::ATTRIBUTE_REF]); 244 $linkName = $link->getName(); 245 } else { 246 $linkName = ""; 247 } 248 return array( 249 PluginUtility::STATE => $state, 250 PluginUtility::ATTRIBUTES => $openingAttributes, 251 PluginUtility::PAYLOAD => $linkName, 252 PluginUtility::CONTEXT => $openingTag->getContext(), 253 self::LINK_TAG => $linkTag 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_listitem::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 $linkTag = $data[self::LINK_TAG]; 349 $renderer->doc .= "</$linkTag>"; 350 351 // Close the html wrapper element 352 $context = $data[PluginUtility::CONTEXT]; 353 switch ($context) { 354 case syntax_plugin_combo_navbarcollapse::COMPONENT: 355 $renderer->doc .= '</div>'; 356 break; 357 case syntax_plugin_combo_navbargroup::COMPONENT: 358 $renderer->doc .= '</li>'; 359 break; 360 } 361 362 363 } 364 365 366 return true; 367 break; 368 369 case 'metadata': 370 371 $state = $data[PluginUtility::STATE]; 372 if ($state == DOKU_LEXER_ENTER) { 373 /** 374 * Keep track of the backlinks ie meta['relation']['references'] 375 * @var Doku_Renderer_metadata $renderer 376 */ 377 if (isset($data[PluginUtility::ATTRIBUTES])) { 378 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 379 } else { 380 $tagAttributes = $data; 381 } 382 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 383 384 $link = new LinkUtility($ref); 385 $name = $tagAttributes[LinkUtility::ATTRIBUTE_NAME]; 386 if ($name != null) { 387 $link->setName($name); 388 } 389 $link->handleMetadata($renderer); 390 391 return true; 392 } 393 break; 394 395 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 396 397 $state = $data[PluginUtility::STATE]; 398 if ($state == DOKU_LEXER_ENTER) { 399 /** 400 * 401 * @var renderer_plugin_combo_analytics $renderer 402 */ 403 $tagAttributes = $data[PluginUtility::ATTRIBUTES]; 404 $ref = $tagAttributes[LinkUtility::ATTRIBUTE_REF]; 405 $link = new LinkUtility($ref); 406 $link->processLinkStats($renderer->stats); 407 break; 408 } 409 410 } 411 // unsupported $mode 412 return false; 413 } 414 415 416} 417 418