1<?php 2 3 4require_once(__DIR__ . "/../ComboStrap/PluginUtility.php"); 5 6use ComboStrap\AnalyticsDocument; 7use ComboStrap\ArrayUtility; 8use ComboStrap\Call; 9use ComboStrap\CallStack; 10use ComboStrap\ExceptionCombo; 11use ComboStrap\MarkupRef; 12use ComboStrap\LogUtility; 13use ComboStrap\Page; 14use ComboStrap\PluginUtility; 15use ComboStrap\TagAttributes; 16use ComboStrap\ThirdPartyPlugins; 17 18if (!defined('DOKU_INC')) die(); 19 20/** 21 * 22 * A link pattern to take over the link of Dokuwiki 23 * and transform it as a bootstrap link 24 * 25 * The handle of the move of link is to be found in the 26 * admin action {@link action_plugin_combo_linkmove} 27 * 28 */ 29class syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin 30{ 31 const TAG = 'link'; 32 const COMPONENT = 'combo_link'; 33 34 /** 35 * Disable the link component 36 */ 37 const CONF_DISABLE_LINK = "disableLink"; 38 39 /** 40 * The link Tag 41 * a or p 42 */ 43 const LINK_TAG = "linkTag"; 44 45 /** 46 * Do the link component allows to be spawn on multilines 47 */ 48 const CLICKABLE_ATTRIBUTE = "clickable"; 49 public const ATTRIBUTE_LABEL = 'label'; 50 /** 51 * The key of the array for the handle cache 52 */ 53 public const ATTRIBUTE_HREF = 'href'; 54 /** 55 * Indicate if the href is a {@link MarkupRef} 56 * (ie the syntax from the markup document) 57 * or is a html href added by {@link syntax_plugin_combo_share} 58 * for instance 59 */ 60 const ATTRIBUTE_HREF_TYPE = "href-type"; 61 const HREF_MARKUP_TYPE_VALUE = "markup"; 62 public const ATTRIBUTE_IMAGE_IN_LABEL = 'image-in-label'; 63 64 /** 65 * A link may have a title or not 66 * ie 67 * [[path:page]] 68 * [[path:page|title]] 69 * are valid 70 * 71 * Get the content until one of this character is found: 72 * * | 73 * * or ]] 74 * * or \n (No line break allowed, too much difficult to debug) 75 * * and not [ (for two links on the same line) 76 */ 77 public const ENTRY_PATTERN_SINGLE_LINE = "\[\[[^\|\]]*(?=[^\n\[]*\]\])"; 78 public const EXIT_PATTERN = "\]\]"; 79 80 81 /** 82 * Dokuwiki Link pattern ter info 83 * Found in {@link \dokuwiki\Parsing\ParserMode\Internallink} 84 */ 85 const SPECIAL_PATTERN = "\[\[.*?\]\](?!\])"; 86 87 /** 88 * The link title attribute (ie popup) 89 */ 90 const TITLE_ATTRIBUTE = "title"; 91 92 93 /** 94 * Parse the match of a syntax {@link DokuWiki_Syntax_Plugin} handle function 95 * @param $match 96 * @return string[] - an array with the attributes constant `ATTRIBUTE_xxxx` as key 97 * 98 * Code adapted from {@link Doku_Handler::internallink()} 99 */ 100 public static function parse($match): array 101 { 102 103 // Strip the opening and closing markup 104 $linkString = preg_replace(array('/^\[\[/', '/\]\]$/u'), '', $match); 105 106 // Split title from URL 107 $linkArray = explode('|', $linkString, 2); 108 109 // Id 110 $attributes[self::ATTRIBUTE_HREF] = trim($linkArray[0]); 111 112 113 // Text or image 114 if (!isset($linkArray[1])) { 115 $attributes[self::ATTRIBUTE_LABEL] = null; 116 } else { 117 // An image in the title 118 if (preg_match('/^\{\{[^\}]+\}\}$/', $linkArray[1])) { 119 // If the title is an image, convert it to an array containing the image details 120 $attributes[self::ATTRIBUTE_IMAGE_IN_LABEL] = Doku_Handler_Parse_Media($linkArray[1]); 121 } else { 122 $attributes[self::ATTRIBUTE_LABEL] = $linkArray[1]; 123 } 124 } 125 126 return $attributes; 127 128 } 129 130 131 /** 132 * Syntax Type. 133 * 134 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 135 * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 136 */ 137 function getType() 138 { 139 return 'substition'; 140 } 141 142 /** 143 * How Dokuwiki will add P element 144 * 145 * * 'normal' - The plugin can be used inside paragraphs 146 * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 147 * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 148 * 149 * @see DokuWiki_Syntax_Plugin::getPType() 150 */ 151 function getPType() 152 { 153 return 'normal'; 154 } 155 156 /** 157 * @return array 158 * Allow which kind of plugin inside 159 * 160 * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 161 * because we manage self the content and we call self the parser 162 */ 163 function getAllowedTypes(): array 164 { 165 return array('substition', 'formatting', 'disabled'); 166 } 167 168 /** 169 * @param string $mode 170 * @return bool 171 * Accepts inside 172 */ 173 public function accepts($mode): bool 174 { 175 /** 176 * To avoid that the description if it contains a link 177 * will be taken by the links mode 178 * 179 * For instance, [[https://hallo|https://hallo]] will send https://hallo 180 * to the external link mode 181 */ 182 $linkModes = [ 183 "externallink", 184 "locallink", 185 "internallink", 186 "interwikilink", 187 "emaillink", 188 "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description 189 //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet 190 //"emphasis_close", 191 //"acrnonym" 192 ]; 193 if (in_array($mode, $linkModes)) { 194 return false; 195 } else { 196 return true; 197 } 198 } 199 200 201 /** 202 * @see Doku_Parser_Mode::getSort() 203 * The mode with the lowest sort number will win out 204 */ 205 function getSort() 206 { 207 /** 208 * It should be less than the number 209 * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort} 210 * and the like 211 * 212 * For whatever reason, the number below should be less than 100, 213 * otherwise on windows with DokuWiki Stick, the link syntax may be not taken 214 * into account 215 */ 216 return 99; 217 } 218 219 220 function connectTo($mode) 221 { 222 223 if (!$this->getConf(self::CONF_DISABLE_LINK, false) 224 && 225 $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME) 226 ) { 227 228 $pattern = self::ENTRY_PATTERN_SINGLE_LINE; 229 $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 230 231 } 232 233 } 234 235 public function postConnect() 236 { 237 if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 238 $this->Lexer->addExitPattern(self::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent())); 239 } 240 } 241 242 243 /** 244 * The handler for an internal link 245 * based on `internallink` in {@link Doku_Handler} 246 * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 247 * the parameters (ie for instance internallink) 248 * @param string $match 249 * @param int $state 250 * @param int $pos 251 * @param Doku_Handler $handler 252 * @return array|bool 253 */ 254 function handle($match, $state, $pos, Doku_Handler $handler) 255 { 256 257 switch ($state) { 258 case DOKU_LEXER_ENTER: 259 $parsedArray = self::parse($match); 260 $htmlAttributes = TagAttributes::createEmpty(self::TAG); 261 /** 262 * Href needs to be passed to the 263 * instructions stack (because we support) 264 * dynamic link call href with {@link syntax_plugin_combo_template} 265 */ 266 $href = $parsedArray[self::ATTRIBUTE_HREF]; 267 if ($href !== null) { 268 $htmlAttributes 269 ->addComponentAttributeValue(self::ATTRIBUTE_HREF, $href) 270 ->addComponentAttributeValue(self::ATTRIBUTE_HREF_TYPE, self::HREF_MARKUP_TYPE_VALUE); 271 } 272 273 274 /** 275 * Extra HTML attribute 276 */ 277 $callStack = CallStack::createFromHandler($handler); 278 $parent = $callStack->moveToParent(); 279 $parentName = ""; 280 if ($parent !== false) { 281 282 /** 283 * Button Link 284 * Getting the attributes 285 */ 286 $parentName = $parent->getTagName(); 287 if ($parentName == syntax_plugin_combo_button::TAG) { 288 $htmlAttributes->mergeWithCallStackArray($parent->getAttributes()); 289 } 290 291 /** 292 * Searching Clickable parent 293 */ 294 $maxLevel = 3; 295 $level = 0; 296 while ( 297 $parent != false && 298 !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 299 $level < $maxLevel 300 ) { 301 $parent = $callStack->moveToParent(); 302 $level++; 303 } 304 if ($parent != false) { 305 if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 306 $htmlAttributes->addClassName("stretched-link"); 307 $parent->addClassName("position-relative"); 308 $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 309 } 310 } 311 312 } 313 $returnedArray[PluginUtility::STATE] = $state; 314 $returnedArray[PluginUtility::ATTRIBUTES] = $htmlAttributes->toCallStackArray(); 315 $returnedArray[PluginUtility::CONTEXT] = $parentName; 316 return $returnedArray; 317 318 case DOKU_LEXER_UNMATCHED: 319 320 $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 321 /** 322 * Delete the separator `|` between the ref and the description if any 323 */ 324 $tag = CallStack::createFromHandler($handler); 325 $parent = $tag->moveToParent(); 326 if ($parent->getTagName() == self::TAG) { 327 if (strpos($match, '|') === 0) { 328 $data[PluginUtility::PAYLOAD] = substr($match, 1); 329 } 330 } 331 return $data; 332 333 case DOKU_LEXER_EXIT: 334 $callStack = CallStack::createFromHandler($handler); 335 $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 336 337 $openingAttributes = $openingTag->getAttributes(); 338 $openingPosition = $openingTag->getKey(); 339 340 $callStack->moveToEnd(); 341 $previousCall = $callStack->previous(); 342 $previousCallPosition = $previousCall->getKey(); 343 $previousCallContent = $previousCall->getCapturedContent(); 344 345 /** 346 * Link label 347 * is set if there is no content 348 * between enter and exit node 349 */ 350 $linkLabel = ""; 351 if ( 352 $openingPosition == $previousCallPosition // ie [[id]] 353 || 354 ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 355 ) { 356 // There is no name 357 $href = $openingTag->getAttribute(self::ATTRIBUTE_HREF); 358 if ($href !== null) { 359 $markup = MarkupRef::createFromRef($href); 360 $linkLabel = $markup->getLabel(); 361 } 362 } 363 return array( 364 PluginUtility::STATE => $state, 365 PluginUtility::ATTRIBUTES => $openingAttributes, 366 PluginUtility::PAYLOAD => $linkLabel, 367 PluginUtility::CONTEXT => $openingTag->getContext() 368 ); 369 } 370 return true; 371 372 373 } 374 375 /** 376 * Render the output 377 * @param string $format 378 * @param Doku_Renderer $renderer 379 * @param array $data - what the function handle() return'ed 380 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 381 * @see DokuWiki_Syntax_Plugin::render() 382 * 383 * 384 */ 385 function render($format, Doku_Renderer $renderer, $data): bool 386 { 387 // The data 388 switch ($format) { 389 case 'xhtml': 390 391 /** @var Doku_Renderer_xhtml $renderer */ 392 /** 393 * Cache problem may occurs while releasing 394 */ 395 if (isset($data[PluginUtility::ATTRIBUTES])) { 396 $callStackAttributes = $data[PluginUtility::ATTRIBUTES]; 397 } else { 398 $callStackAttributes = $data; 399 } 400 401 PluginUtility::getSnippetManager()->attachCssInternalStyleSheetForSlot(self::TAG); 402 403 $state = $data[PluginUtility::STATE]; 404 switch ($state) { 405 case DOKU_LEXER_ENTER: 406 $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG); 407 408 $href = $tagAttributes->getValue(self::ATTRIBUTE_HREF); 409 410 /** 411 * HrefMarkup ? 412 */ 413 $hrefSource = $tagAttributes->getValueAndRemoveIfPresent(self::ATTRIBUTE_HREF_TYPE); 414 if ($hrefSource !== null) { 415 try { 416 $markupRef = MarkupRef::createFromRef($href); 417 $url = $markupRef->getUrl(); 418 $markupRefAttributes = $markupRef->toAttributes(); 419 } catch (ExceptionCombo $e) { 420 $message = "Error while parsing the markup href ($href). Error: {$e->getMessage()}"; 421 $renderer->doc .= "<a>." . LogUtility::wrapInRedForHtml($message); 422 return false; 423 } 424 $tagAttributes->mergeWithCallStackArray($markupRefAttributes->toCallStackArray()); 425 // No href if the url could not be calculated 426 // such as a bad interwiki link 427 if (!empty($url)) { 428 $tagAttributes->setComponentAttributeValue(self::ATTRIBUTE_HREF, $url); 429 } else { 430 $tagAttributes->removeComponentAttributeIfPresent(self::ATTRIBUTE_HREF); 431 } 432 433 } 434 435 /** 436 * Extra styling 437 */ 438 $parentTag = $data[PluginUtility::CONTEXT]; 439 $htmlPrefix = ""; 440 switch ($parentTag) { 441 /** 442 * Button link 443 */ 444 case syntax_plugin_combo_button::TAG: 445 $tagAttributes->addOutputAttributeValue("role", "button"); 446 syntax_plugin_combo_button::processButtonAttributesToHtmlAttributes($tagAttributes); 447 break; 448 case syntax_plugin_combo_dropdown::TAG: 449 $tagAttributes->addClassName("dropdown-item"); 450 break; 451 case syntax_plugin_combo_navbarcollapse::COMPONENT: 452 $tagAttributes->addClassName("navbar-link"); 453 $htmlPrefix = '<div class="navbar-nav">'; 454 break; 455 case syntax_plugin_combo_navbargroup::COMPONENT: 456 $tagAttributes->addClassName("nav-link"); 457 $htmlPrefix = '<li class="nav-item">'; 458 break; 459 default: 460 case syntax_plugin_combo_badge::TAG: 461 case syntax_plugin_combo_cite::TAG: 462 case syntax_plugin_combo_contentlistitem::DOKU_TAG: 463 case syntax_plugin_combo_preformatted::TAG: 464 break; 465 466 } 467 468 /** 469 * Add it to the rendering 470 */ 471 $renderer->doc .= $htmlPrefix . $tagAttributes->toHtmlEnterTag("a"); 472 break; 473 case DOKU_LEXER_UNMATCHED: 474 $renderer->doc .= PluginUtility::renderUnmatched($data); 475 break; 476 case DOKU_LEXER_EXIT: 477 478 // if there is no link name defined, we get the name as ref in the payload 479 // otherwise null string 480 $renderer->doc .= $data[PluginUtility::PAYLOAD]; 481 482 // Close the link 483 $renderer->doc .= "</a>"; 484 485 // Close the html wrapper element 486 $context = $data[PluginUtility::CONTEXT]; 487 switch ($context) { 488 case syntax_plugin_combo_navbarcollapse::COMPONENT: 489 $renderer->doc .= '</div>'; 490 break; 491 case syntax_plugin_combo_navbargroup::COMPONENT: 492 $renderer->doc .= '</li>'; 493 break; 494 } 495 496 497 } 498 499 500 return true; 501 502 case 'metadata': 503 504 /** 505 * @var Doku_Renderer_metadata $renderer 506 */ 507 $state = $data[PluginUtility::STATE]; 508 switch ($state) { 509 case DOKU_LEXER_ENTER: 510 /** 511 * Keep track of the backlinks ie meta['relation']['references'] 512 * @var Doku_Renderer_metadata $renderer 513 */ 514 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 515 $hrefSource = $tagAttributes->getValue(self::ATTRIBUTE_HREF_TYPE); 516 if ($hrefSource === null || $hrefSource !== self::HREF_MARKUP_TYPE_VALUE) { 517 /** 518 * This is not a markup link 519 * (ie an external link created by a plugin {@link syntax_plugin_combo_share}) 520 */ 521 return false; 522 } 523 $href = $tagAttributes->getValue(self::ATTRIBUTE_HREF); 524 $type = MarkupRef::createFromRef($href) 525 ->getUriType(); 526 $name = $tagAttributes->getValue(self::ATTRIBUTE_LABEL); 527 528 switch ($type) { 529 case MarkupRef::WIKI_URI: 530 /** 531 * The relative link should be passed (ie the original) 532 * Dokuwiki has a default description 533 * We can't pass empty or the array(title), it does not work 534 */ 535 $descriptionToDelete = "b"; 536 $renderer->internallink($href, $descriptionToDelete); 537 $renderer->doc = substr($renderer->doc,0,-strlen($descriptionToDelete)); 538 break; 539 case MarkupRef::WEB_URI: 540 $renderer->externallink($href, $name); 541 break; 542 case MarkupRef::LOCAL_URI: 543 $renderer->locallink($href, $name); 544 break; 545 case MarkupRef::EMAIL_URI: 546 $renderer->emaillink($href, $name); 547 break; 548 case MarkupRef::INTERWIKI_URI: 549 $interWikiSplit = preg_split("/>/", $href); 550 $renderer->interwikilink($href, $name, $interWikiSplit[0], $interWikiSplit[1]); 551 break; 552 case MarkupRef::WINDOWS_SHARE_URI: 553 $renderer->windowssharelink($href, $name); 554 break; 555 case MarkupRef::VARIABLE_URI: 556 // No backlinks for link template 557 break; 558 default: 559 LogUtility::msg("The markup reference ({$href}) with the type $type was not processed into the metadata"); 560 } 561 562 return true; 563 case DOKU_LEXER_UNMATCHED: 564 $renderer->doc .= PluginUtility::renderUnmatched($data); 565 break; 566 } 567 break; 568 569 case renderer_plugin_combo_analytics::RENDERER_FORMAT: 570 571 $state = $data[PluginUtility::STATE]; 572 if ($state == DOKU_LEXER_ENTER) { 573 /** 574 * 575 * @var renderer_plugin_combo_analytics $renderer 576 */ 577 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 578 $refSource = $tagAttributes->getValue(self::ATTRIBUTE_HREF_TYPE); 579 if ($refSource === null || $refSource !== self::HREF_MARKUP_TYPE_VALUE) { 580 /** 581 * Link added programmatically 582 */ 583 return false; 584 } 585 $ref = $tagAttributes->getValue(self::ATTRIBUTE_HREF); 586 $href = MarkupRef::createFromRef($ref); 587 $refType = $href->getUriType(); 588 589 590 /** 591 * @param array $stats 592 * Calculate internal link statistics 593 */ 594 595 $stats = &$renderer->stats; 596 switch ($refType) { 597 598 case MarkupRef::WIKI_URI: 599 600 /** 601 * Internal link count 602 */ 603 if (!array_key_exists(AnalyticsDocument::INTERNAL_LINK_COUNT, $stats)) { 604 $stats[AnalyticsDocument::INTERNAL_LINK_COUNT] = 0; 605 } 606 $stats[AnalyticsDocument::INTERNAL_LINK_COUNT]++; 607 608 609 /** 610 * Broken link ? 611 */ 612 613 $linkedPage = $href->getInternalPage(); 614 if (!$linkedPage->exists()) { 615 $stats[AnalyticsDocument::INTERNAL_LINK_BROKEN_COUNT]++; 616 $stats[AnalyticsDocument::INFO][] = "The internal linked page `{$href->getInternalPage()}` does not exist"; 617 } 618 619 /** 620 * Calculate link distance 621 */ 622 global $ID; 623 $id = $href->getInternalPage()->getDokuwikiId(); 624 $a = explode(':', getNS($ID)); 625 $b = explode(':', getNS($id)); 626 while (isset($a[0]) && $a[0] == $b[0]) { 627 array_shift($a); 628 array_shift($b); 629 } 630 $length = count($a) + count($b); 631 $stats[AnalyticsDocument::INTERNAL_LINK_DISTANCE][] = $length; 632 break; 633 634 case MarkupRef::WEB_URI: 635 636 if (!array_key_exists(AnalyticsDocument::EXTERNAL_LINK_COUNT, $stats)) { 637 $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT] = 0; 638 } 639 $stats[AnalyticsDocument::EXTERNAL_LINK_COUNT]++; 640 break; 641 642 case MarkupRef::LOCAL_URI: 643 644 if (!array_key_exists(AnalyticsDocument::LOCAL_LINK_COUNT, $stats)) { 645 $stats[AnalyticsDocument::LOCAL_LINK_COUNT] = 0; 646 } 647 $stats[AnalyticsDocument::LOCAL_LINK_COUNT]++; 648 break; 649 650 case MarkupRef::INTERWIKI_URI: 651 652 if (!array_key_exists(AnalyticsDocument::INTERWIKI_LINK_COUNT, $stats)) { 653 $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT] = 0; 654 } 655 $stats[AnalyticsDocument::INTERWIKI_LINK_COUNT]++; 656 break; 657 658 case MarkupRef::EMAIL_URI: 659 660 if (!array_key_exists(AnalyticsDocument::EMAIL_COUNT, $stats)) { 661 $stats[AnalyticsDocument::EMAIL_COUNT] = 0; 662 } 663 $stats[AnalyticsDocument::EMAIL_COUNT]++; 664 break; 665 666 case MarkupRef::WINDOWS_SHARE_URI: 667 668 if (!array_key_exists(AnalyticsDocument::WINDOWS_SHARE_COUNT, $stats)) { 669 $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT] = 0; 670 } 671 $stats[AnalyticsDocument::WINDOWS_SHARE_COUNT]++; 672 break; 673 674 case MarkupRef::VARIABLE_URI: 675 676 if (!array_key_exists(AnalyticsDocument::TEMPLATE_LINK_COUNT, $stats)) { 677 $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT] = 0; 678 } 679 $stats[AnalyticsDocument::TEMPLATE_LINK_COUNT]++; 680 break; 681 682 default: 683 684 LogUtility::msg("The link `{$ref}` with the type ($refType) is not taken into account into the statistics"); 685 686 } 687 688 689 break; 690 } 691 692 } 693 // unsupported $mode 694 return false; 695 } 696 697 698 /** 699 * Utility function to add a link into the callstack 700 * @param CallStack $callStack 701 * @param TagAttributes $tagAttributes 702 */ 703 public static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes) 704 { 705 $parent = $callStack->moveToParent(); 706 $context = ""; 707 $attributes = $tagAttributes->toCallStackArray(); 708 if ($parent !== false) { 709 $context = $parent->getTagName(); 710 if ($context === syntax_plugin_combo_button::TAG) { 711 // the link takes by default the data from the button 712 $parentAttributes = $parent->getAttributes(); 713 if ($parentAttributes !== null) { 714 $attributes = ArrayUtility::mergeByValue($parentAttributes, $attributes); 715 } 716 } 717 } 718 $callStack->appendCallAtTheEnd( 719 Call::createComboCall( 720 syntax_plugin_combo_link::TAG, 721 DOKU_LEXER_ENTER, 722 $attributes, 723 $context 724 )); 725 } 726 727 public static function addExitLinkTagInCallStack(CallStack $callStack) 728 { 729 $callStack->appendCallAtTheEnd( 730 Call::createComboCall( 731 syntax_plugin_combo_link::TAG, 732 DOKU_LEXER_EXIT 733 )); 734 } 735} 736 737