1007225e5Sgerardnico<?php 2007225e5Sgerardnico 3007225e5Sgerardnico 437748cd8SNickeaurequire_once(__DIR__ . "/../ComboStrap/PluginUtility.php"); 5007225e5Sgerardnico 64cadd4f8SNickeauuse ComboStrap\ArrayUtility; 704fd306cSNickeauuse ComboStrap\ButtonTag; 84cadd4f8SNickeauuse ComboStrap\Call; 9531e725cSNickeauuse ComboStrap\CallStack; 1004fd306cSNickeauuse ComboStrap\DropDownTag; 1104fd306cSNickeauuse ComboStrap\ExceptionBadArgument; 1204fd306cSNickeauuse ComboStrap\ExceptionBadSyntax; 1304fd306cSNickeauuse ComboStrap\ExceptionCompile; 1404fd306cSNickeauuse ComboStrap\ExceptionNotFound; 1504fd306cSNickeauuse ComboStrap\FileSystems; 1604fd306cSNickeauuse ComboStrap\LinkMarkup; 174cadd4f8SNickeauuse ComboStrap\LogUtility; 1804fd306cSNickeauuse ComboStrap\MarkupRef; 1904fd306cSNickeauuse ComboStrap\MarkupPath; 20007225e5Sgerardnicouse ComboStrap\PluginUtility; 21531e725cSNickeauuse ComboStrap\TagAttributes; 22c3437056SNickeauuse ComboStrap\ThirdPartyPlugins; 2304fd306cSNickeauuse ComboStrap\Web\UrlEndpoint; 24007225e5Sgerardnico 25007225e5Sgerardnicoif (!defined('DOKU_INC')) die(); 26007225e5Sgerardnico 27007225e5Sgerardnico/** 28007225e5Sgerardnico * 29007225e5Sgerardnico * A link pattern to take over the link of Dokuwiki 30007225e5Sgerardnico * and transform it as a bootstrap link 31007225e5Sgerardnico * 32007225e5Sgerardnico * The handle of the move of link is to be found in the 33007225e5Sgerardnico * admin action {@link action_plugin_combo_linkmove} 34007225e5Sgerardnico * 3504fd306cSNickeau * popular [[ wiki ]] syntax for linking notes 3604fd306cSNickeau * and makes it easy to build personal wikis, 3704fd306cSNickeau * team knowledge bases, 3804fd306cSNickeau * or something like a Second Brain or a Zettelkasten. 3904fd306cSNickeau * 40007225e5Sgerardnico */ 41007225e5Sgerardnicoclass syntax_plugin_combo_link extends DokuWiki_Syntax_Plugin 42007225e5Sgerardnico{ 43007225e5Sgerardnico const TAG = 'link'; 44ef295d81Sgerardnico const COMPONENT = 'combo_link'; 45007225e5Sgerardnico 465f891b7eSNickeau /** 4785e82846SNickeau * Disable the link component 4821913ab3SNickeau */ 4921913ab3SNickeau const CONF_DISABLE_LINK = "disableLink"; 5021913ab3SNickeau 5121913ab3SNickeau /** 525f891b7eSNickeau * The link Tag 53531e725cSNickeau * a or p 545f891b7eSNickeau */ 555f891b7eSNickeau const LINK_TAG = "linkTag"; 565f891b7eSNickeau 5721913ab3SNickeau /** 5821913ab3SNickeau * Do the link component allows to be spawn on multilines 5921913ab3SNickeau */ 60531e725cSNickeau const CLICKABLE_ATTRIBUTE = "clickable"; 614cadd4f8SNickeau public const ATTRIBUTE_LABEL = 'label'; 624cadd4f8SNickeau /** 634cadd4f8SNickeau * The key of the array for the handle cache 644cadd4f8SNickeau */ 6504fd306cSNickeau public const MARKUP_REF_ATTRIBUTE = 'ref'; 6604fd306cSNickeau 674cadd4f8SNickeau public const ATTRIBUTE_IMAGE_IN_LABEL = 'image-in-label'; 684cadd4f8SNickeau 694cadd4f8SNickeau /** 704cadd4f8SNickeau * A link may have a title or not 714cadd4f8SNickeau * ie 724cadd4f8SNickeau * [[path:page]] 734cadd4f8SNickeau * [[path:page|title]] 744cadd4f8SNickeau * are valid 754cadd4f8SNickeau * 764cadd4f8SNickeau * Get the content until one of this character is found: 774cadd4f8SNickeau * * | 784cadd4f8SNickeau * * or ]] 794cadd4f8SNickeau * * or \n (No line break allowed, too much difficult to debug) 804cadd4f8SNickeau * * and not [ (for two links on the same line) 814cadd4f8SNickeau */ 824cadd4f8SNickeau public const ENTRY_PATTERN_SINGLE_LINE = "\[\[[^\|\]]*(?=[^\n\[]*\]\])"; 834cadd4f8SNickeau public const EXIT_PATTERN = "\]\]"; 844cadd4f8SNickeau 854cadd4f8SNickeau 864cadd4f8SNickeau /** 874cadd4f8SNickeau * Dokuwiki Link pattern ter info 884cadd4f8SNickeau * Found in {@link \dokuwiki\Parsing\ParserMode\Internallink} 894cadd4f8SNickeau */ 904cadd4f8SNickeau const SPECIAL_PATTERN = "\[\[.*?\]\](?!\])"; 914cadd4f8SNickeau 924cadd4f8SNickeau /** 934cadd4f8SNickeau * The link title attribute (ie popup) 944cadd4f8SNickeau */ 954cadd4f8SNickeau const TITLE_ATTRIBUTE = "title"; 9604fd306cSNickeau const STRETCHED_LINK = "stretched-link"; 9704fd306cSNickeau const CANONICAL = "link"; 984cadd4f8SNickeau 994cadd4f8SNickeau 1004cadd4f8SNickeau /** 1014cadd4f8SNickeau * Parse the match of a syntax {@link DokuWiki_Syntax_Plugin} handle function 1024cadd4f8SNickeau * @param $match 1034cadd4f8SNickeau * @return string[] - an array with the attributes constant `ATTRIBUTE_xxxx` as key 1044cadd4f8SNickeau * 1054cadd4f8SNickeau * Code adapted from {@link Doku_Handler::internallink()} 1064cadd4f8SNickeau */ 1074cadd4f8SNickeau public static function parse($match): array 1084cadd4f8SNickeau { 1094cadd4f8SNickeau 1104cadd4f8SNickeau // Strip the opening and closing markup 11104fd306cSNickeau $linkString = preg_replace(array('/^\[\[/', '/]]$/u'), '', $match); 1124cadd4f8SNickeau 1134cadd4f8SNickeau // Split title from URL 1144cadd4f8SNickeau $linkArray = explode('|', $linkString, 2); 1154cadd4f8SNickeau 1164cadd4f8SNickeau // Id 11704fd306cSNickeau $attributes[self::MARKUP_REF_ATTRIBUTE] = trim($linkArray[0]); 1184cadd4f8SNickeau 1194cadd4f8SNickeau 1204cadd4f8SNickeau // Text or image 1214cadd4f8SNickeau if (!isset($linkArray[1])) { 1224cadd4f8SNickeau $attributes[self::ATTRIBUTE_LABEL] = null; 1234cadd4f8SNickeau } else { 1244cadd4f8SNickeau // An image in the title 12504fd306cSNickeau if (preg_match('/^{{[^}]+}}$/', $linkArray[1])) { 1264cadd4f8SNickeau // If the title is an image, convert it to an array containing the image details 1274cadd4f8SNickeau $attributes[self::ATTRIBUTE_IMAGE_IN_LABEL] = Doku_Handler_Parse_Media($linkArray[1]); 1284cadd4f8SNickeau } else { 1294cadd4f8SNickeau $attributes[self::ATTRIBUTE_LABEL] = $linkArray[1]; 1304cadd4f8SNickeau } 1314cadd4f8SNickeau } 1324cadd4f8SNickeau 1334cadd4f8SNickeau return $attributes; 1344cadd4f8SNickeau 1354cadd4f8SNickeau } 13621913ab3SNickeau 1375f891b7eSNickeau 138007225e5Sgerardnico /** 139007225e5Sgerardnico * Syntax Type. 140007225e5Sgerardnico * 141007225e5Sgerardnico * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 142007225e5Sgerardnico * @see https://www.dokuwiki.org/devel:syntax_plugins#syntax_types 143007225e5Sgerardnico */ 144007225e5Sgerardnico function getType() 145007225e5Sgerardnico { 146007225e5Sgerardnico return 'substition'; 147007225e5Sgerardnico } 148007225e5Sgerardnico 149007225e5Sgerardnico /** 150007225e5Sgerardnico * How Dokuwiki will add P element 151007225e5Sgerardnico * 152007225e5Sgerardnico * * 'normal' - The plugin can be used inside paragraphs 153007225e5Sgerardnico * * 'block' - Open paragraphs need to be closed before plugin output - block should not be inside paragraphs 154007225e5Sgerardnico * * 'stack' - Special case. Plugin wraps other paragraphs. - Stacks can contain paragraphs 155007225e5Sgerardnico * 156007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::getPType() 157007225e5Sgerardnico */ 158007225e5Sgerardnico function getPType() 159007225e5Sgerardnico { 160007225e5Sgerardnico return 'normal'; 161007225e5Sgerardnico } 162007225e5Sgerardnico 163007225e5Sgerardnico /** 164007225e5Sgerardnico * @return array 165007225e5Sgerardnico * Allow which kind of plugin inside 166007225e5Sgerardnico * 167007225e5Sgerardnico * No one of array('container', 'baseonly', 'formatting', 'substition', 'protected', 'disabled', 'paragraphs') 168007225e5Sgerardnico * because we manage self the content and we call self the parser 169007225e5Sgerardnico */ 1704cadd4f8SNickeau function getAllowedTypes(): array 171007225e5Sgerardnico { 172007225e5Sgerardnico return array('substition', 'formatting', 'disabled'); 173007225e5Sgerardnico } 174007225e5Sgerardnico 17537748cd8SNickeau /** 17637748cd8SNickeau * @param string $mode 17737748cd8SNickeau * @return bool 17837748cd8SNickeau * Accepts inside 17937748cd8SNickeau */ 1804cadd4f8SNickeau public function accepts($mode): bool 1815f891b7eSNickeau { 1825f891b7eSNickeau /** 1835f891b7eSNickeau * To avoid that the description if it contains a link 1845f891b7eSNickeau * will be taken by the links mode 1855f891b7eSNickeau * 1865f891b7eSNickeau * For instance, [[https://hallo|https://hallo]] will send https://hallo 1875f891b7eSNickeau * to the external link mode 1885f891b7eSNickeau */ 1895f891b7eSNickeau $linkModes = [ 1905f891b7eSNickeau "externallink", 1915f891b7eSNickeau "locallink", 1925f891b7eSNickeau "internallink", 1935f891b7eSNickeau "interwikilink", 1945f891b7eSNickeau "emaillink", 195fc45fbf7Sgerardnico "emphasis", // double slash can not be used inside to preserve the possibility to write an URL in the description 1965f891b7eSNickeau //"emphasis_open", // italic use // and therefore take over a link as description which is not handy when copying a tweet 1975f891b7eSNickeau //"emphasis_close", 1985f891b7eSNickeau //"acrnonym" 1995f891b7eSNickeau ]; 2005f891b7eSNickeau if (in_array($mode, $linkModes)) { 2015f891b7eSNickeau return false; 2025f891b7eSNickeau } else { 2035f891b7eSNickeau return true; 2045f891b7eSNickeau } 2055f891b7eSNickeau } 2065f891b7eSNickeau 2075f891b7eSNickeau 208007225e5Sgerardnico /** 209007225e5Sgerardnico * @see Doku_Parser_Mode::getSort() 210007225e5Sgerardnico * The mode with the lowest sort number will win out 211007225e5Sgerardnico */ 212007225e5Sgerardnico function getSort() 213007225e5Sgerardnico { 214e8b2ff59SNickeau /** 215e8b2ff59SNickeau * It should be less than the number 216e8b2ff59SNickeau * at {@link \dokuwiki\Parsing\ParserMode\Internallink::getSort} 217e8b2ff59SNickeau * and the like 218e8b2ff59SNickeau * 219e8b2ff59SNickeau * For whatever reason, the number below should be less than 100, 220e8b2ff59SNickeau * otherwise on windows with DokuWiki Stick, the link syntax may be not taken 221e8b2ff59SNickeau * into account 222e8b2ff59SNickeau */ 223e8b2ff59SNickeau return 99; 224007225e5Sgerardnico } 225007225e5Sgerardnico 226007225e5Sgerardnico 227007225e5Sgerardnico function connectTo($mode) 228007225e5Sgerardnico { 229d262537cSgerardnico 23037748cd8SNickeau if (!$this->getConf(self::CONF_DISABLE_LINK, false) 23137748cd8SNickeau && 23237748cd8SNickeau $mode !== PluginUtility::getModeFromPluginName(ThirdPartyPlugins::IMAGE_MAPPING_NAME) 23337748cd8SNickeau ) { 23437748cd8SNickeau 2354cadd4f8SNickeau $pattern = self::ENTRY_PATTERN_SINGLE_LINE; 2369337a630SNickeau $this->Lexer->addEntryPattern($pattern, $mode, PluginUtility::getModeFromTag($this->getPluginComponent())); 23737748cd8SNickeau 23821913ab3SNickeau } 239d262537cSgerardnico 240007225e5Sgerardnico } 241007225e5Sgerardnico 2425f891b7eSNickeau public function postConnect() 2435f891b7eSNickeau { 24421913ab3SNickeau if (!$this->getConf(self::CONF_DISABLE_LINK, false)) { 2454cadd4f8SNickeau $this->Lexer->addExitPattern(self::EXIT_PATTERN, PluginUtility::getModeFromTag($this->getPluginComponent())); 2465f891b7eSNickeau } 24721913ab3SNickeau } 2485f891b7eSNickeau 249007225e5Sgerardnico 250007225e5Sgerardnico /** 251007225e5Sgerardnico * The handler for an internal link 252007225e5Sgerardnico * based on `internallink` in {@link Doku_Handler} 253007225e5Sgerardnico * The handler call the good renderer in {@link Doku_Renderer_xhtml} with 254007225e5Sgerardnico * the parameters (ie for instance internallink) 255007225e5Sgerardnico * @param string $match 256007225e5Sgerardnico * @param int $state 257007225e5Sgerardnico * @param int $pos 258007225e5Sgerardnico * @param Doku_Handler $handler 259007225e5Sgerardnico * @return array|bool 260007225e5Sgerardnico */ 261007225e5Sgerardnico function handle($match, $state, $pos, Doku_Handler $handler) 262007225e5Sgerardnico { 263007225e5Sgerardnico 2645f891b7eSNickeau switch ($state) { 2655f891b7eSNickeau case DOKU_LEXER_ENTER: 2664cadd4f8SNickeau $parsedArray = self::parse($match); 2674cadd4f8SNickeau $htmlAttributes = TagAttributes::createEmpty(self::TAG); 26804fd306cSNickeau 2694cadd4f8SNickeau /** 27004fd306cSNickeau * The markup ref needs to be passed to the 27104fd306cSNickeau * instructions stack (because we support link with a variable as markup ref 27204fd306cSNickeau * via a {@link syntax_plugin_combo_fragment} in a {@link syntax_plugin_combo_iterator} 27304fd306cSNickeau * 27404fd306cSNickeau * The variable is replaced in the {@link syntax_plugin_combo_link::render()} 27504fd306cSNickeau * at runtime while rendering 2764cadd4f8SNickeau */ 27704fd306cSNickeau $markupRef = $parsedArray[self::MARKUP_REF_ATTRIBUTE]; 27804fd306cSNickeau if ($markupRef !== null) { 2794fbd4ae2SNico /** 2804fbd4ae2SNico * If the Rel is a wiki link, we make the path absolute and not relative 2814fbd4ae2SNico * (this is for the {@link \ComboStrap\FetcherPageBundler)}} 2824fbd4ae2SNico * otherwise the links are not good and are seen as non-existent 2834fbd4ae2SNico */ 2844fbd4ae2SNico try { 2854fbd4ae2SNico $markupRefObject = MarkupRef::createLinkFromRef($markupRef); 2864fbd4ae2SNico $scheme = $markupRefObject->getSchemeType(); 2874fbd4ae2SNico if ($scheme === MarkupRef::WIKI_URI) { 288738ed353SNico 289738ed353SNico /** 290738ed353SNico * Properties? 291738ed353SNico */ 292738ed353SNico $newProperties = []; 293738ed353SNico $url = $markupRefObject->getUrl(); 294738ed353SNico foreach (array_keys($url->getQueryProperties()) as $propertyName) { 295738ed353SNico if ($propertyName === "id") { 296738ed353SNico continue; 297738ed353SNico } 298738ed353SNico $queryPropertyValue = $url->getQueryPropertyValue($propertyName); 299738ed353SNico if (is_array($queryPropertyValue)) { 300738ed353SNico foreach ($queryPropertyValue as $arrayValue) { 301738ed353SNico $newProperties[] = $propertyName . "=" . $arrayValue; 302738ed353SNico } 303738ed353SNico } else { 304738ed353SNico $newProperties[] = $propertyName . "=" . $queryPropertyValue; 305738ed353SNico } 306738ed353SNico } 3074fbd4ae2SNico $markupRef = $markupRefObject->getPath()->toAbsoluteId(); 308738ed353SNico if (count($newProperties) > 0) { 309738ed353SNico $queryString = implode("&", $newProperties); 310738ed353SNico $markupRef .= "?" . $queryString; 311738ed353SNico } 312738ed353SNico try { 313738ed353SNico $markupRef .= "#" . $url->getFragment(); 314738ed353SNico } catch (ExceptionNotFound $e) { 315738ed353SNico // no fragment 316738ed353SNico } 3174fbd4ae2SNico } 3184fbd4ae2SNico } catch (ExceptionBadArgument|ExceptionBadSyntax|ExceptionNotFound $e) { 319738ed353SNico // not a valid ref 3204fbd4ae2SNico } 32104fd306cSNickeau $htmlAttributes->addComponentAttributeValue(self::MARKUP_REF_ATTRIBUTE, $markupRef); 3224cadd4f8SNickeau } 3234cadd4f8SNickeau 3244cadd4f8SNickeau 3254cadd4f8SNickeau /** 3264cadd4f8SNickeau * Extra HTML attribute 3274cadd4f8SNickeau */ 328531e725cSNickeau $callStack = CallStack::createFromHandler($handler); 329531e725cSNickeau $parent = $callStack->moveToParent(); 330007225e5Sgerardnico $parentName = ""; 3314cadd4f8SNickeau if ($parent !== false) { 332531e725cSNickeau 333531e725cSNickeau /** 334531e725cSNickeau * Button Link 335531e725cSNickeau * Getting the attributes 336531e725cSNickeau */ 337531e725cSNickeau $parentName = $parent->getTagName(); 33804fd306cSNickeau if ($parentName == ButtonTag::MARKUP_LONG) { 3394cadd4f8SNickeau $htmlAttributes->mergeWithCallStackArray($parent->getAttributes()); 34021913ab3SNickeau } 341531e725cSNickeau 342531e725cSNickeau /** 343531e725cSNickeau * Searching Clickable parent 344531e725cSNickeau */ 345531e725cSNickeau $maxLevel = 3; 346531e725cSNickeau $level = 0; 347531e725cSNickeau while ( 348531e725cSNickeau $parent != false && 349531e725cSNickeau !$parent->hasAttribute(self::CLICKABLE_ATTRIBUTE) && 350531e725cSNickeau $level < $maxLevel 351531e725cSNickeau ) { 352531e725cSNickeau $parent = $callStack->moveToParent(); 353531e725cSNickeau $level++; 3545f891b7eSNickeau } 35504fd306cSNickeau if ($parent !== false) { 356531e725cSNickeau if ($parent->getAttribute(self::CLICKABLE_ATTRIBUTE)) { 35704fd306cSNickeau $htmlAttributes->addClassName(self::STRETCHED_LINK); 358531e725cSNickeau $parent->addClassName("position-relative"); 359531e725cSNickeau $parent->removeAttribute(self::CLICKABLE_ATTRIBUTE); 3605f891b7eSNickeau } 36121913ab3SNickeau } 36221913ab3SNickeau 363531e725cSNickeau } 3644cadd4f8SNickeau $returnedArray[PluginUtility::STATE] = $state; 3654cadd4f8SNickeau $returnedArray[PluginUtility::ATTRIBUTES] = $htmlAttributes->toCallStackArray(); 3664cadd4f8SNickeau $returnedArray[PluginUtility::CONTEXT] = $parentName; 3674cadd4f8SNickeau return $returnedArray; 3684cadd4f8SNickeau 3695f891b7eSNickeau case DOKU_LEXER_UNMATCHED: 3705f891b7eSNickeau 37132b85071SNickeau $data = PluginUtility::handleAndReturnUnmatchedData(self::TAG, $match, $handler); 3725f891b7eSNickeau /** 37332b85071SNickeau * Delete the separator `|` between the ref and the description if any 3745f891b7eSNickeau */ 3751fa8c418SNickeau $tag = CallStack::createFromHandler($handler); 3761fa8c418SNickeau $parent = $tag->moveToParent(); 3771fa8c418SNickeau if ($parent->getTagName() == self::TAG) { 3785f891b7eSNickeau if (strpos($match, '|') === 0) { 37932b85071SNickeau $data[PluginUtility::PAYLOAD] = substr($match, 1); 3805f891b7eSNickeau } 381007225e5Sgerardnico } 38232b85071SNickeau return $data; 383007225e5Sgerardnico 3845f891b7eSNickeau case DOKU_LEXER_EXIT: 38504fd306cSNickeau 386531e725cSNickeau $callStack = CallStack::createFromHandler($handler); 387531e725cSNickeau $openingTag = $callStack->moveToPreviousCorrespondingOpeningCall(); 3884cadd4f8SNickeau 3895f891b7eSNickeau $openingAttributes = $openingTag->getAttributes(); 390531e725cSNickeau $openingPosition = $openingTag->getKey(); 3915f891b7eSNickeau 392531e725cSNickeau $callStack->moveToEnd(); 393531e725cSNickeau $previousCall = $callStack->previous(); 394531e725cSNickeau $previousCallPosition = $previousCall->getKey(); 395531e725cSNickeau $previousCallContent = $previousCall->getCapturedContent(); 396531e725cSNickeau 3974cadd4f8SNickeau /** 3984cadd4f8SNickeau * Link label 3994cadd4f8SNickeau * is set if there is no content 4004cadd4f8SNickeau * between enter and exit node 4014cadd4f8SNickeau */ 4024cadd4f8SNickeau $linkLabel = ""; 403531e725cSNickeau if ( 404531e725cSNickeau $openingPosition == $previousCallPosition // ie [[id]] 405531e725cSNickeau || 406531e725cSNickeau ($openingPosition == $previousCallPosition - 1 && $previousCallContent == "|") // ie [[id|]] 407531e725cSNickeau ) { 4085f891b7eSNickeau // There is no name 40904fd306cSNickeau $markupRef = $openingTag->getAttribute(self::MARKUP_REF_ATTRIBUTE); 41004fd306cSNickeau if ($markupRef !== null) { 41104fd306cSNickeau try { 41204fd306cSNickeau $linkLabel = LinkMarkup::createFromRef($markupRef) 41304fd306cSNickeau ->getDefaultLabel(); 41404fd306cSNickeau } catch (ExceptionCompile $e) { 41504fd306cSNickeau LogUtility::error("No default Label can be defined. Error while parsing the markup ref ($markupRef). Error: {$e->getMessage()}", self::CANONICAL, $e); 41604fd306cSNickeau } 41704fd306cSNickeau 4184cadd4f8SNickeau } 4195f891b7eSNickeau } 4205f891b7eSNickeau return array( 4215f891b7eSNickeau PluginUtility::STATE => $state, 4225f891b7eSNickeau PluginUtility::ATTRIBUTES => $openingAttributes, 4234cadd4f8SNickeau PluginUtility::PAYLOAD => $linkLabel, 42485e82846SNickeau PluginUtility::CONTEXT => $openingTag->getContext() 4255f891b7eSNickeau ); 4265f891b7eSNickeau } 4275f891b7eSNickeau return true; 4285f891b7eSNickeau 429007225e5Sgerardnico 430007225e5Sgerardnico } 431007225e5Sgerardnico 432007225e5Sgerardnico /** 433007225e5Sgerardnico * Render the output 434007225e5Sgerardnico * @param string $format 435007225e5Sgerardnico * @param Doku_Renderer $renderer 436007225e5Sgerardnico * @param array $data - what the function handle() return'ed 437007225e5Sgerardnico * @return boolean - rendered correctly? (however, returned value is not used at the moment) 438007225e5Sgerardnico * @see DokuWiki_Syntax_Plugin::render() 439007225e5Sgerardnico * 440007225e5Sgerardnico * 441007225e5Sgerardnico */ 442c3437056SNickeau function render($format, Doku_Renderer $renderer, $data): bool 443007225e5Sgerardnico { 444007225e5Sgerardnico // The data 44504fd306cSNickeau $state = $data[PluginUtility::STATE]; 446007225e5Sgerardnico switch ($format) { 447007225e5Sgerardnico case 'xhtml': 448007225e5Sgerardnico 449007225e5Sgerardnico /** @var Doku_Renderer_xhtml $renderer */ 450007225e5Sgerardnico /** 45119b0880dSgerardnico * Cache problem may occurs while releasing 452007225e5Sgerardnico */ 453007225e5Sgerardnico if (isset($data[PluginUtility::ATTRIBUTES])) { 454531e725cSNickeau $callStackAttributes = $data[PluginUtility::ATTRIBUTES]; 455007225e5Sgerardnico } else { 456531e725cSNickeau $callStackAttributes = $data; 457007225e5Sgerardnico } 4585f891b7eSNickeau 45904fd306cSNickeau PluginUtility::getSnippetManager()->attachCssInternalStyleSheet(self::TAG); 4605f891b7eSNickeau 4615f891b7eSNickeau switch ($state) { 4625f891b7eSNickeau case DOKU_LEXER_ENTER: 463531e725cSNickeau $tagAttributes = TagAttributes::createFromCallStackArray($callStackAttributes, self::TAG); 4644cadd4f8SNickeau 46504fd306cSNickeau $markupRef = $tagAttributes->getValueAndRemove(self::MARKUP_REF_ATTRIBUTE); 46604fd306cSNickeau if ($markupRef === null) { 46704fd306cSNickeau $message = "Internal Error: A link reference was not found"; 46804fd306cSNickeau LogUtility::internalError($message); 46904fd306cSNickeau $renderer->doc .= LogUtility::wrapInRedForHtml($message); 4704cadd4f8SNickeau return false; 4714cadd4f8SNickeau } 47204fd306cSNickeau try { 47304fd306cSNickeau $markupRef = syntax_plugin_combo_variable::replaceVariablesWithValuesFromContext($markupRef); 47404fd306cSNickeau $markupLink = LinkMarkup::createFromRef($markupRef); 47504fd306cSNickeau $markupAttributes = $markupLink->toAttributes(); 47604fd306cSNickeau } catch (ExceptionCompile $e) { 47704fd306cSNickeau // uncomment to get the original error stack trace in dev 47804fd306cSNickeau // and see where the exception comes from 47904fd306cSNickeau // Don't forget to comment back 48004fd306cSNickeau// if (PluginUtility::isDevOrTest()) { 48104fd306cSNickeau// throw new ExceptionRuntime("Error on markup ref", self::TAG, 0, $e); 48204fd306cSNickeau// } 48304fd306cSNickeau 48404fd306cSNickeau /** 48504fd306cSNickeau * Error. Example: unknown inter-wiki ... 48604fd306cSNickeau * We still create the a to be xhtml compliante 48704fd306cSNickeau */ 48804fd306cSNickeau $url = UrlEndpoint::createSupportUrl(); 48904fd306cSNickeau $markupAttributes = TagAttributes::createEmpty() 49004fd306cSNickeau ->addOutputAttributeValue("href", $url->toString()) 49104fd306cSNickeau ->addClassName(LinkMarkup::getHtmlClassNotExist()); 49204fd306cSNickeau $renderer->doc .= $markupAttributes->toHtmlEnterTag("a") . $e->getMessage(); 49304fd306cSNickeau 49404fd306cSNickeau LogUtility::warning($e->getMessage(), "link", $e); 49504fd306cSNickeau return false; 4964cadd4f8SNickeau 4974cadd4f8SNickeau } 49804fd306cSNickeau // markup attributes is leading because it has already output attribute such as href 49904fd306cSNickeau $markupAttributes->mergeWithCallStackArray($tagAttributes->toCallStackArray()); 50004fd306cSNickeau $tagAttributes = $markupAttributes; 50104fd306cSNickeau 502d262537cSgerardnico 50319b0880dSgerardnico /** 5045f891b7eSNickeau * Extra styling 50519b0880dSgerardnico */ 5065f891b7eSNickeau $parentTag = $data[PluginUtility::CONTEXT]; 5074cadd4f8SNickeau $htmlPrefix = ""; 5089f4383e9Sgerardnico switch ($parentTag) { 50921913ab3SNickeau /** 51021913ab3SNickeau * Button link 51121913ab3SNickeau */ 51204fd306cSNickeau case ButtonTag::MARKUP_LONG: 5134cadd4f8SNickeau $tagAttributes->addOutputAttributeValue("role", "button"); 51404fd306cSNickeau ButtonTag::processButtonAttributesToHtmlAttributes($tagAttributes); 5159f4383e9Sgerardnico break; 51604fd306cSNickeau case DropDownTag::TAG: 5174cadd4f8SNickeau $tagAttributes->addClassName("dropdown-item"); 5184cadd4f8SNickeau break; 5194cadd4f8SNickeau case syntax_plugin_combo_navbarcollapse::COMPONENT: 5204cadd4f8SNickeau $tagAttributes->addClassName("navbar-link"); 5214cadd4f8SNickeau $htmlPrefix = '<div class="navbar-nav">'; 5224cadd4f8SNickeau break; 5234cadd4f8SNickeau case syntax_plugin_combo_navbargroup::COMPONENT: 5244cadd4f8SNickeau $tagAttributes->addClassName("nav-link"); 5254cadd4f8SNickeau $htmlPrefix = '<li class="nav-item">'; 5264cadd4f8SNickeau break; 5274cadd4f8SNickeau default: 5285f891b7eSNickeau case syntax_plugin_combo_badge::TAG: 5299f4383e9Sgerardnico case syntax_plugin_combo_cite::TAG: 5309337a630SNickeau case syntax_plugin_combo_contentlistitem::DOKU_TAG: 5319f4383e9Sgerardnico case syntax_plugin_combo_preformatted::TAG: 5329f4383e9Sgerardnico break; 5330a517624Sgerardnico 5349f4383e9Sgerardnico } 5359f4383e9Sgerardnico 53619b0880dSgerardnico /** 53719b0880dSgerardnico * Add it to the rendering 53819b0880dSgerardnico */ 5394cadd4f8SNickeau $renderer->doc .= $htmlPrefix . $tagAttributes->toHtmlEnterTag("a"); 5405f891b7eSNickeau break; 5415f891b7eSNickeau case DOKU_LEXER_UNMATCHED: 54232b85071SNickeau $renderer->doc .= PluginUtility::renderUnmatched($data); 5435f891b7eSNickeau break; 5445f891b7eSNickeau case DOKU_LEXER_EXIT: 5455f891b7eSNickeau 5465f891b7eSNickeau // if there is no link name defined, we get the name as ref in the payload 5475f891b7eSNickeau // otherwise null string 54870bbd7f1Sgerardnico $renderer->doc .= $data[PluginUtility::PAYLOAD] ?? ''; 5495f891b7eSNickeau 550e3d0019cSgerardnico // Close the link 55185e82846SNickeau $renderer->doc .= "</a>"; 552e3d0019cSgerardnico 553e3d0019cSgerardnico // Close the html wrapper element 5545f891b7eSNickeau $context = $data[PluginUtility::CONTEXT]; 5555f891b7eSNickeau switch ($context) { 5565f891b7eSNickeau case syntax_plugin_combo_navbarcollapse::COMPONENT: 5575f891b7eSNickeau $renderer->doc .= '</div>'; 5585f891b7eSNickeau break; 5595f891b7eSNickeau case syntax_plugin_combo_navbargroup::COMPONENT: 5605f891b7eSNickeau $renderer->doc .= '</li>'; 5615f891b7eSNickeau break; 5625f891b7eSNickeau } 5635f891b7eSNickeau 5645f891b7eSNickeau } 565007225e5Sgerardnico return true; 5665f891b7eSNickeau case 'metadata': 567007225e5Sgerardnico 5684cadd4f8SNickeau /** 5694cadd4f8SNickeau * @var Doku_Renderer_metadata $renderer 5704cadd4f8SNickeau */ 5714cadd4f8SNickeau switch ($state) { 5724cadd4f8SNickeau case DOKU_LEXER_ENTER: 573007225e5Sgerardnico /** 574007225e5Sgerardnico * Keep track of the backlinks ie meta['relation']['references'] 575007225e5Sgerardnico * @var Doku_Renderer_metadata $renderer 576007225e5Sgerardnico */ 5774cadd4f8SNickeau $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 57804fd306cSNickeau 57904fd306cSNickeau $markupRef = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE); 58004fd306cSNickeau if ($markupRef === null) { 58104fd306cSNickeau LogUtility::internalError("The markup ref was not found for a link."); 5824cadd4f8SNickeau return false; 583007225e5Sgerardnico } 58404fd306cSNickeau try { 58504fd306cSNickeau $type = MarkupRef::createLinkFromRef($markupRef) 58604fd306cSNickeau ->getSchemeType(); 58704fd306cSNickeau } catch (ExceptionCompile $e) { 58804fd306cSNickeau LogUtility::error("The markup ref could not be parsed. Error:{$e->getMessage()}"); 58904fd306cSNickeau return false; 59004fd306cSNickeau } 5914cadd4f8SNickeau $name = $tagAttributes->getValue(self::ATTRIBUTE_LABEL); 5929f4383e9Sgerardnico 5934cadd4f8SNickeau switch ($type) { 5944cadd4f8SNickeau case MarkupRef::WIKI_URI: 5954cadd4f8SNickeau /** 5964cadd4f8SNickeau * The relative link should be passed (ie the original) 5974cadd4f8SNickeau * Dokuwiki has a default description 5984cadd4f8SNickeau * We can't pass empty or the array(title), it does not work 5994cadd4f8SNickeau */ 6004cadd4f8SNickeau $descriptionToDelete = "b"; 60104fd306cSNickeau $renderer->internallink($markupRef, $descriptionToDelete); 6024cadd4f8SNickeau $renderer->doc = substr($renderer->doc, 0, -strlen($descriptionToDelete)); 6034cadd4f8SNickeau break; 6044cadd4f8SNickeau case MarkupRef::WEB_URI: 60504fd306cSNickeau $renderer->externallink($markupRef, $name); 6064cadd4f8SNickeau break; 6074cadd4f8SNickeau case MarkupRef::LOCAL_URI: 60804fd306cSNickeau $renderer->locallink($markupRef, $name); 6094cadd4f8SNickeau break; 6104cadd4f8SNickeau case MarkupRef::EMAIL_URI: 61104fd306cSNickeau $renderer->emaillink($markupRef, $name); 6124cadd4f8SNickeau break; 6134cadd4f8SNickeau case MarkupRef::INTERWIKI_URI: 61404fd306cSNickeau $interWikiSplit = preg_split("/>/", $markupRef); 61504fd306cSNickeau $renderer->interwikilink($markupRef, $name, $interWikiSplit[0], $interWikiSplit[1]); 6164cadd4f8SNickeau break; 6174cadd4f8SNickeau case MarkupRef::WINDOWS_SHARE_URI: 61804fd306cSNickeau $renderer->windowssharelink($markupRef, $name); 6194cadd4f8SNickeau break; 6204cadd4f8SNickeau case MarkupRef::VARIABLE_URI: 6214cadd4f8SNickeau // No backlinks for link template 6224cadd4f8SNickeau break; 6234cadd4f8SNickeau default: 62404fd306cSNickeau LogUtility::msg("The markup reference ({$markupRef}) with the type $type was not processed into the metadata"); 6259f4383e9Sgerardnico } 626007225e5Sgerardnico 627007225e5Sgerardnico return true; 6284cadd4f8SNickeau case DOKU_LEXER_UNMATCHED: 6294cadd4f8SNickeau $renderer->doc .= PluginUtility::renderUnmatched($data); 6304cadd4f8SNickeau break; 6315f891b7eSNickeau } 632007225e5Sgerardnico break; 633007225e5Sgerardnico 634531e725cSNickeau case renderer_plugin_combo_analytics::RENDERER_FORMAT: 6355f891b7eSNickeau 63604fd306cSNickeau if ($state === DOKU_LEXER_ENTER) { 637007225e5Sgerardnico /** 638007225e5Sgerardnico * 639007225e5Sgerardnico * @var renderer_plugin_combo_analytics $renderer 640007225e5Sgerardnico */ 6414cadd4f8SNickeau $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 64204fd306cSNickeau $ref = $tagAttributes->getValue(self::MARKUP_REF_ATTRIBUTE); 64304fd306cSNickeau try { 64404fd306cSNickeau $markupRef = LinkMarkup::createFromRef($ref); 64504fd306cSNickeau } catch (ExceptionCompile $e) { 64604fd306cSNickeau LogUtility::error("Error while parsing the ref ($ref). Error: {$e->getMessage()}. No further analytics."); 6474cadd4f8SNickeau return false; 6484cadd4f8SNickeau } 64904fd306cSNickeau $refType = $markupRef->getMarkupRef()->getSchemeType(); 6504cadd4f8SNickeau 6514cadd4f8SNickeau 6524cadd4f8SNickeau /** 6534cadd4f8SNickeau * @param array $stats 6544cadd4f8SNickeau * Calculate internal link statistics 6554cadd4f8SNickeau */ 6564cadd4f8SNickeau $stats = &$renderer->stats; 6574cadd4f8SNickeau switch ($refType) { 6584cadd4f8SNickeau 6594cadd4f8SNickeau case MarkupRef::WIKI_URI: 6604cadd4f8SNickeau 6614cadd4f8SNickeau /** 6624cadd4f8SNickeau * Internal link count 6634cadd4f8SNickeau */ 66404fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT, $stats)) { 66504fd306cSNickeau $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT] = 0; 6664cadd4f8SNickeau } 66704fd306cSNickeau $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_COUNT]++; 6684cadd4f8SNickeau 6694cadd4f8SNickeau 6704cadd4f8SNickeau /** 6714cadd4f8SNickeau * Broken link ? 6724cadd4f8SNickeau */ 67304fd306cSNickeau try { 67404fd306cSNickeau $path = $markupRef->getMarkupRef()->getPath(); 67504fd306cSNickeau $linkedPage = MarkupPath::createPageFromPathObject($path); 67604fd306cSNickeau if (!FileSystems::exists($path)) { 67770bbd7f1Sgerardnico $internalLinkBroken = $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] ?? 0; 67870bbd7f1Sgerardnico $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_BROKEN_COUNT] = $internalLinkBroken + 1; 67904fd306cSNickeau $stats[renderer_plugin_combo_analytics::INFO][] = "The internal linked page `{$linkedPage}` does not exist"; 68004fd306cSNickeau } 68104fd306cSNickeau } catch (ExceptionNotFound $e) { 68204fd306cSNickeau // no local path 6834cadd4f8SNickeau } 6844cadd4f8SNickeau 6854cadd4f8SNickeau /** 6864cadd4f8SNickeau * Calculate link distance 6874cadd4f8SNickeau */ 6884cadd4f8SNickeau global $ID; 68904fd306cSNickeau $id = $linkedPage->getWikiId(); 6904cadd4f8SNickeau $a = explode(':', getNS($ID)); 6914cadd4f8SNickeau $b = explode(':', getNS($id)); 692*ac4b853aSNico while (isset($a[0]) && isset($b[0]) && $a[0] == $b[0]) { 6934cadd4f8SNickeau array_shift($a); 6944cadd4f8SNickeau array_shift($b); 6954cadd4f8SNickeau } 6964cadd4f8SNickeau $length = count($a) + count($b); 69704fd306cSNickeau $stats[renderer_plugin_combo_analytics::INTERNAL_LINK_DISTANCE][] = $length; 6984cadd4f8SNickeau break; 6994cadd4f8SNickeau 7004cadd4f8SNickeau case MarkupRef::WEB_URI: 7014cadd4f8SNickeau 70204fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT, $stats)) { 70304fd306cSNickeau $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT] = 0; 7044cadd4f8SNickeau } 70504fd306cSNickeau $stats[renderer_plugin_combo_analytics::EXTERNAL_LINK_COUNT]++; 7064cadd4f8SNickeau break; 7074cadd4f8SNickeau 7084cadd4f8SNickeau case MarkupRef::LOCAL_URI: 7094cadd4f8SNickeau 71004fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::LOCAL_LINK_COUNT, $stats)) { 71104fd306cSNickeau $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT] = 0; 7124cadd4f8SNickeau } 71304fd306cSNickeau $stats[renderer_plugin_combo_analytics::LOCAL_LINK_COUNT]++; 7144cadd4f8SNickeau break; 7154cadd4f8SNickeau 7164cadd4f8SNickeau case MarkupRef::INTERWIKI_URI: 7174cadd4f8SNickeau 71804fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT, $stats)) { 71904fd306cSNickeau $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT] = 0; 7204cadd4f8SNickeau } 72104fd306cSNickeau $stats[renderer_plugin_combo_analytics::INTERWIKI_LINK_COUNT]++; 7224cadd4f8SNickeau break; 7234cadd4f8SNickeau 7244cadd4f8SNickeau case MarkupRef::EMAIL_URI: 7254cadd4f8SNickeau 72604fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::EMAIL_COUNT, $stats)) { 72704fd306cSNickeau $stats[renderer_plugin_combo_analytics::EMAIL_COUNT] = 0; 7284cadd4f8SNickeau } 72904fd306cSNickeau $stats[renderer_plugin_combo_analytics::EMAIL_COUNT]++; 7304cadd4f8SNickeau break; 7314cadd4f8SNickeau 7324cadd4f8SNickeau case MarkupRef::WINDOWS_SHARE_URI: 7334cadd4f8SNickeau 73404fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT, $stats)) { 73504fd306cSNickeau $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT] = 0; 7364cadd4f8SNickeau } 73704fd306cSNickeau $stats[renderer_plugin_combo_analytics::WINDOWS_SHARE_COUNT]++; 7384cadd4f8SNickeau break; 7394cadd4f8SNickeau 7404cadd4f8SNickeau case MarkupRef::VARIABLE_URI: 7414cadd4f8SNickeau 74204fd306cSNickeau if (!array_key_exists(renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT, $stats)) { 74304fd306cSNickeau $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT] = 0; 7444cadd4f8SNickeau } 74504fd306cSNickeau $stats[renderer_plugin_combo_analytics::TEMPLATE_LINK_COUNT]++; 7464cadd4f8SNickeau break; 7474cadd4f8SNickeau 7484cadd4f8SNickeau default: 7494cadd4f8SNickeau 7504cadd4f8SNickeau LogUtility::msg("The link `{$ref}` with the type ($refType) is not taken into account into the statistics"); 7514cadd4f8SNickeau 7524cadd4f8SNickeau } 7534cadd4f8SNickeau 7544cadd4f8SNickeau 755007225e5Sgerardnico break; 7565f891b7eSNickeau } 757007225e5Sgerardnico 758007225e5Sgerardnico } 75904fd306cSNickeau 760007225e5Sgerardnico return false; 761007225e5Sgerardnico } 762007225e5Sgerardnico 763007225e5Sgerardnico 7644cadd4f8SNickeau /** 7654cadd4f8SNickeau * Utility function to add a link into the callstack 7664cadd4f8SNickeau * @param CallStack $callStack 7674cadd4f8SNickeau * @param TagAttributes $tagAttributes 7684cadd4f8SNickeau */ 76904fd306cSNickeau public 77004fd306cSNickeau static function addOpenLinkTagInCallStack(CallStack $callStack, TagAttributes $tagAttributes) 7714cadd4f8SNickeau { 7724cadd4f8SNickeau $parent = $callStack->moveToParent(); 7734cadd4f8SNickeau $context = ""; 7744cadd4f8SNickeau $attributes = $tagAttributes->toCallStackArray(); 7754cadd4f8SNickeau if ($parent !== false) { 7764cadd4f8SNickeau $context = $parent->getTagName(); 77704fd306cSNickeau if ($context === ButtonTag::MARKUP_LONG) { 7784cadd4f8SNickeau // the link takes by default the data from the button 7794cadd4f8SNickeau $parentAttributes = $parent->getAttributes(); 7804cadd4f8SNickeau if ($parentAttributes !== null) { 7814cadd4f8SNickeau $attributes = ArrayUtility::mergeByValue($parentAttributes, $attributes); 7824cadd4f8SNickeau } 7834cadd4f8SNickeau } 7844cadd4f8SNickeau } 7854cadd4f8SNickeau $callStack->appendCallAtTheEnd( 7864cadd4f8SNickeau Call::createComboCall( 7874cadd4f8SNickeau syntax_plugin_combo_link::TAG, 7884cadd4f8SNickeau DOKU_LEXER_ENTER, 7894cadd4f8SNickeau $attributes, 7904cadd4f8SNickeau $context 7914cadd4f8SNickeau )); 7924cadd4f8SNickeau } 7934cadd4f8SNickeau 79404fd306cSNickeau public 79504fd306cSNickeau static function addExitLinkTagInCallStack(CallStack $callStack) 7964cadd4f8SNickeau { 7974cadd4f8SNickeau $callStack->appendCallAtTheEnd( 7984cadd4f8SNickeau Call::createComboCall( 7994cadd4f8SNickeau syntax_plugin_combo_link::TAG, 8004cadd4f8SNickeau DOKU_LEXER_EXIT 8014cadd4f8SNickeau )); 8024cadd4f8SNickeau } 803007225e5Sgerardnico} 804007225e5Sgerardnico 805