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