1<?php 2/** 3 * DokuWiki Syntax Plugin Related. 4 * 5 */ 6 7use ComboStrap\ExceptionCombo; 8use ComboStrap\LogUtility; 9use ComboStrap\MarkupRef; 10use ComboStrap\Page; 11use ComboStrap\PluginUtility; 12use ComboStrap\TagAttributes; 13 14 15require_once(DOKU_INC . 'inc/parserutils.php'); 16 17/** 18 * All DokuWiki plugins to extend the parser/rendering mechanism 19 * need to inherit from this class 20 * 21 * The name of the class must follow a pattern (don't change it) 22 * 23 * The index and the metadata key for backlinks is called 'relation_references' 24 * It's the key value that you need to pass in the {@link lookupKey} of the {@link \dokuwiki\Search\Indexer} 25 * 26 * Type of conf[index]/index: 27 * * page.idx (id of the page is the element number) 28 * * title 29 * * relation_references_w.idx - _w for words 30 * * relation_references_w.idx - _i for lines (index by lines) 31 * 32 * The index is a associative map by key 33 * 34 * 35 */ 36class syntax_plugin_combo_related extends DokuWiki_Syntax_Plugin 37{ 38 39 40 // Conf property key 41 const MAX_LINKS_CONF = 'maxLinks'; 42 const MAX_LINKS_CONF_DEFAULT = 10; 43 // For when you come from another plugin (such as backlinks) and that you don't want to change the pattern on each page 44 const EXTRA_PATTERN_CONF = 'extra_pattern'; 45 46 // This is a fake page ID that is added 47 // to the related page array when the number of backlinks is bigger than the max 48 // Poisoning object strategy 49 const MORE_PAGE_ID = 'related_more'; 50 51 // The array key of an array of related page 52 const RELATED_PAGE_ID_PROP = 'id'; 53 const RELATED_BACKLINKS_COUNT_PROP = 'backlinks'; 54 const TAG = "related"; 55 56 57 /** 58 * @param Page $page 59 * @param int|null $max 60 * @return string 61 */ 62 public static function getHtmlRelated(Page $page, ?int $max = null): string 63 { 64 global $lang; 65 66 $tagAttributes = TagAttributes::createEmpty(self::getTag()); 67 $tagAttributes->addClassName("d-print-none"); 68 $html = $tagAttributes->toHtmlEnterTag("div"); 69 70 $relatedPages = self::getRelatedPagesOrderedByBacklinkCount($page, $max); 71 if (empty($relatedPages)) { 72 73 $html .= "<strong>Plugin " . PluginUtility::PLUGIN_BASE_NAME . " - Component " . self::getTag() . ": " . $lang['nothingfound'] . "</strong>" . DOKU_LF; 74 75 } else { 76 77 // Dokuwiki debug 78 79 $html .= '<ul>' . DOKU_LF; 80 81 foreach ($relatedPages as $backlink) { 82 $backlinkId = $backlink[self::RELATED_PAGE_ID_PROP]; 83 $html .= '<li>'; 84 if ($backlinkId != self::MORE_PAGE_ID) { 85 $linkUtility = MarkupRef::createFromPageId($backlinkId); 86 try { 87 $html .= $linkUtility->toAttributes(self::TAG)->toHtmlEnterTag("a"); 88 $html .= $linkUtility->getLabel(); 89 $html .= "</a>"; 90 } catch (ExceptionCombo $e) { 91 $html = "Error while trying to create the link for the page ($backlinkId). Error: {$e->getMessage()}"; 92 LogUtility::msg($html); 93 } 94 95 } else { 96 $html .= 97 tpl_link( 98 wl($page->getDokuwikiId()) . '?do=backlink', 99 "More ...", 100 'class="" rel="nofollow" title="More..."', 101 true 102 ); 103 } 104 $html .= '</li>' . DOKU_LF; 105 } 106 107 $html .= '</ul>' . DOKU_LF; 108 109 } 110 111 return $html . '</div>' . DOKU_LF; 112 } 113 114 115 /** 116 * Syntax Type. 117 * 118 * Needs to return one of the mode types defined in $PARSER_MODES in parser.php 119 * @see DokuWiki_Syntax_Plugin::getType() 120 */ 121 function getType() 122 { 123 return 'substition'; 124 } 125 126 /** 127 * @see DokuWiki_Syntax_Plugin::getPType() 128 */ 129 function getPType() 130 { 131 return 'block'; 132 } 133 134 /** 135 * @see Doku_Parser_Mode::getSort() 136 */ 137 function getSort() 138 { 139 return 100; 140 } 141 142 /** 143 * Create a pattern that will called this plugin 144 * 145 * @param string $mode 146 * @see Doku_Parser_Mode::connectTo() 147 */ 148 function connectTo($mode) 149 { 150 // The basic 151 $this->Lexer->addSpecialPattern(PluginUtility::getVoidElementTagPattern(self::getTag()), $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent()); 152 153 // To replace backlinks, you may add it in the configuration 154 $extraPattern = $this->getConf(self::EXTRA_PATTERN_CONF); 155 if ($extraPattern != "") { 156 $this->Lexer->addSpecialPattern($extraPattern, $mode, 'plugin_' . PluginUtility::PLUGIN_BASE_NAME . '_' . $this->getPluginComponent()); 157 } 158 159 } 160 161 /** 162 * 163 * The handle function goal is to parse the matched syntax through the pattern function 164 * and to return the result for use in the renderer 165 * This result is always cached until the page is modified. 166 * @param string $match 167 * @param int $state 168 * @param int $pos 169 * @param Doku_Handler $handler 170 * @return array|bool 171 * @see DokuWiki_Syntax_Plugin::handle() 172 * 173 */ 174 function handle($match, $state, $pos, Doku_Handler $handler) 175 { 176 177 switch ($state) { 178 179 // As there is only one call to connect to in order to a add a pattern, 180 // there is only one state entering the function 181 // but I leave it for better understanding of the process flow 182 case DOKU_LEXER_SPECIAL : 183 184 $qualifiedMach = trim($match); 185 $attributes = []; 186 if ($qualifiedMach[0] === "<") { 187 // not an extra pattern 188 $tagAttributes = TagAttributes::createFromTagMatch($match); 189 $attributes = $tagAttributes->toCallStackArray(); 190 } 191 return array( 192 PluginUtility::STATE => $state, 193 PluginUtility::ATTRIBUTES => $attributes 194 ); 195 196 } 197 198 // Cache the values 199 return array($state); 200 } 201 202 /** 203 * Render the output 204 * @param string $format 205 * @param Doku_Renderer $renderer 206 * @param array $data - what the function handle() return'ed 207 * @return boolean - rendered correctly? (however, returned value is not used at the moment) 208 * @see DokuWiki_Syntax_Plugin::render() 209 * 210 * 211 */ 212 function render($format, Doku_Renderer $renderer, $data) 213 { 214 215 216 if ($format == 'xhtml') { 217 218 $page = Page::createPageFromRequestedPage(); 219 $tagAttributes = TagAttributes::createFromCallStackArray($data[PluginUtility::ATTRIBUTES]); 220 $max = $tagAttributes->getValue(self::MAX_LINKS_CONF); 221 if ($max === NULL) { 222 $max = PluginUtility::getConfValue(self::MAX_LINKS_CONF, self::MAX_LINKS_CONF_DEFAULT); 223 } 224 $renderer->doc .= self::getHtmlRelated($page, $max); 225 return true; 226 } 227 return false; 228 } 229 230 /** 231 * @param Page $page 232 * @param int|null $max 233 * @return array 234 */ 235 public static function getRelatedPagesOrderedByBacklinkCount(Page $page, ?int $max = null): array 236 { 237 238 // Call the dokuwiki backlinks function 239 // @require_once(DOKU_INC . 'inc/fulltext.php'); 240 // Backlinks called the indexer, for more info 241 // See: https://www.dokuwiki.org/devel:metadata#metadata_index 242 $backlinks = ft_backlinks($page->getDokuwikiId(), $ignore_perms = false); 243 244 $related = array(); 245 foreach ($backlinks as $backlink) { 246 $page = array(); 247 $page[self::RELATED_PAGE_ID_PROP] = $backlink; 248 $page[self::RELATED_BACKLINKS_COUNT_PROP] = sizeof(ft_backlinks($backlink, $ignore_perms = false)); 249 $related[] = $page; 250 } 251 252 usort($related, function ($a, $b) { 253 return $b[self::RELATED_BACKLINKS_COUNT_PROP] - $a[self::RELATED_BACKLINKS_COUNT_PROP]; 254 }); 255 256 if ($max !== null) { 257 if (sizeof($related) > $max) { 258 $related = array_slice($related, 0, $max); 259 $page = array(); 260 $page[self::RELATED_PAGE_ID_PROP] = self::MORE_PAGE_ID; 261 $related[] = $page; 262 } 263 } 264 265 return $related; 266 267 } 268 269 public static function getTag(): string 270 { 271 return self::TAG; 272 } 273 274 275} 276