1<?php 2 3use dokuwiki\Extension\SyntaxPlugin; 4 5/** 6 * PageQuery Plugin: search for and list pages, sorted/grouped by name, date, creator, etc 7 * 8 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 9 * @author Symon Bent <hendrybadao@gmail.com> 10 * 11 * @phpcs :disable Squiz.Classes.ValidClassName.NotCamelCaps 12 */ 13class syntax_plugin_pagequery extends SyntaxPlugin 14{ 15 public const MAX_COLS = 12; 16 17 public function getType(): string 18 { 19 return 'substition'; 20 } 21 22 public function getPType(): string 23 { 24 return 'block'; 25 } 26 27 public function getSort(): int 28 { 29 return 98; 30 } 31 32 public function connectTo($mode): void 33 { 34 // this regex allows multi-line syntax for easier composition/reading 35 $this->Lexer->addSpecialPattern('\{\{pagequery>(?m).*?(?-m)\}\}', $mode, 'plugin_pagequery'); 36 } 37 38 /** 39 * Parses all the pagequery options: 40 * Insert the pagequery markup wherever you want your list to appear. E.g: 41 * 42 * {{pagequery>}} 43 * 44 * {{pagequery>[query];fulltext;sort=key:direction,key2:direction;group;limit=??;cols=?;spelldate;proper}} 45 * 46 * @link https://www.dokuwiki.org/plugin:pagequery See PageQuery page for full details 47 */ 48 public function handle($match, $state, $pos, Doku_Handler $handler): array 49 { 50 $opt = []; 51 $match = substr($match, 12, -2); // strip markup "{{pagequery>...}}" 52 $params = explode(';', $match); 53 // remove any pre/trailing spaces due to multi-line syntax 54 $params = array_map('trim', $params); 55 56 $opt['query'] = $params[0]; 57 58 // establish some basic option defaults 59 $opt['border'] = 'none'; // show borders around entire list and/or between columns 60 $opt['bullet'] = 'none'; // bullet style for list items 61 $opt['casesort'] = false; // allow case sorting 62 $opt['cols'] = 1; // number of displayed columns (fixed for table layout, max for column layout 63 $opt['dformat'] = "%d %b %Y"; // general display date format 64 $opt['display'] = 'name'; // how page links should be displayed 65 $opt['filter'] = []; // filtering by metadata prior to sorting 66 $opt['fontsize'] = ''; // base fontsize of pagequery; best to use % 67 $opt['fullregex'] = false; // power-user regex search option--file name only 68 $opt['fulltext'] = false; // search full-text; including file contents 69 $opt['group'] = false; // group the results based on sort headings 70 $opt['hidejump'] = false; // hide the jump to top link 71 $opt['hidemsg'] = false; // hide any error messages 72 $opt['hidestart'] = false; // hide start pages 73 $opt['label'] = ''; // label to put at top of the list 74 $opt['layout'] = 'table'; // html layout type: table (1 col = div only) or columns (html 5 only) 75 $opt['limit'] = 0; // limit results to certain number 76 $opt['maxns'] = 0; // max number of namespaces to display (i.e. ns depth) 77 $opt['natsort'] = false; // allow natural case sorting 78 $opt['proper'] = 'none'; // display file names in Proper Case 79 $opt['showcount'] = false; // show the count of links found 80 $opt['snippet'] = ['type' => 'none', 'count' => 0, 'extent' => '']; // show content snippets/abstracts 81 $opt['sort'] = []; // sort by various headings 82 $opt['spelldate'] = false; // spell out date headings in words where possible 83 $opt['underline'] = false; // faint underline below each link for clarity 84 $opt['nstitle'] = false; // internal use currently... 85 86 foreach ($params as $param) { 87 [$option, $value] = $this->keyvalue($param, '='); 88 switch ($option) { 89 case 'casesort': 90 case 'fullregex': 91 case 'fulltext': 92 case 'group': 93 case 'hidejump': 94 case 'hidemsg': 95 case 'hidestart': 96 case 'natsort': 97 case 'showcount': 98 case 'spelldate': 99 case 'underline': 100 $opt[$option] = true; 101 break; 102 case 'limit': 103 case 'maxns': 104 $opt[$option] = abs($value); 105 break; 106 case 'sort': 107 case 'filter': 108 $fields = explode(',', $value); 109 foreach ($fields as $field) { 110 [$key, $expr] = $this->keyvalue($field); 111 // allow for a few common naming differences 112 switch ($key) { 113 case 'pagename': 114 $key = 'name'; 115 break; 116 case 'heading': 117 case 'firstheading': 118 $key = 'title'; 119 break; 120 case 'pageid': 121 case 'id': 122 $key = 'id'; 123 break; 124 case 'contrib': 125 $key = 'contributor'; 126 break; 127 } 128 $opt[$option][$key] = $expr; 129 } 130 break; 131 case 'proper': 132 switch ($value) { 133 case 'hdr': 134 case 'header': 135 case 'group': 136 $opt['proper'] = 'header'; 137 break; 138 case 'name': 139 case 'page': 140 $opt['proper'] = 'name'; 141 break; 142 default: 143 $opt['proper'] = 'both'; 144 } 145 break; 146 case 'cols': 147 $opt['cols'] = ($value > self::MAX_COLS) ? self::MAX_COLS : $value; 148 break; 149 case 'border': 150 switch ($value) { 151 case 'both': 152 case 'inside': 153 case 'outside': 154 case 'none': 155 $opt['border'] = $value; 156 break; 157 default: 158 $opt['border'] = 'both'; 159 } 160 break; 161 case 'snippet': 162 $default = 'tooltip'; 163 if (empty($value)) { 164 $opt['snippet']['type'] = $default; 165 break; 166 } else { 167 $options = explode(',', $value); 168 $type = (empty($options[0])) ? $opt['snippet']['type'] : $options[0]; 169 $count = (empty($options[1])) ? $opt['snippet']['count'] : $options[1]; 170 $extent = (empty($options[2])) ? $opt['snippet']['extent'] : $options[2]; 171 172 $valid = ['none', 'tooltip', 'inline', 'plain', 'quoted']; 173 if (!in_array($type, $valid)) { 174 $type = $default; // empty snippet type => tooltip 175 } 176 $opt['snippet'] = ['type' => $type, 'count' => $count, 'extent' => $extent]; 177 break; 178 } 179 case 'label': 180 case 'bullet': 181 $opt[$option] = $value; 182 break; 183 case 'display': 184 switch ($value) { 185 case 'name': 186 $opt['display'] = 'name'; 187 break; 188 case 'title': 189 case 'heading': 190 case 'firstheading': 191 $opt['display'] = 'title'; 192 $opt['nstitle'] = true; 193 break; 194 case 'pageid': 195 case 'id': 196 $opt['display'] = 'id'; 197 break; 198 default: 199 $opt['display'] = $value; 200 } 201 if (preg_match('/{(title|heading|firstheading)}/', $value)) { 202 $opt['nstitle'] = true; 203 } 204 break; 205 case 'layout': 206 if (!in_array($value, ['table', 'column'])) { 207 $value = 'table'; 208 } 209 $opt['layout'] = $value; 210 break; 211 case 'fontsize': 212 if (!empty($value)) { 213 $opt['fontsize'] = $value; 214 } 215 break; 216 } 217 } 218 return $opt; 219 } 220 221 /** 222 * Split a string into key => value parts. 223 * 224 * @param string $str 225 * @param string $delim 226 */ 227 private function keyvalue(string $str, string $delim = ':'): array 228 { 229 $parts = explode($delim, $str); 230 $key = $parts[0] ?? ''; 231 $value = $parts[1] ?? ''; 232 return [$key, $value]; 233 } 234 235 public function render($format, Doku_Renderer $renderer, $data): bool 236 { 237 $incl_ns = []; 238 $excl_ns = []; 239 $sort_opts = []; 240 $group_opts = []; 241 $message = ''; 242 243 $lang = ['jump_section' => $this->getLang('jump_section'), 'link_to_top' => $this->getLang('link_to_top'), 244 'no_results' => $this->getLang('no_results')]; 245 require_once DOKU_PLUGIN . 'pagequery/PageQuery.php'; 246 $pq = new PageQuery($lang); 247 248 $query = $data['query']; 249 250 if ($format === 'xhtml') { 251 // first get a raw list of matching results 252 if ($data['fulltext']) { 253 // full text searching (Dokuwiki style) 254 $results = $pq->pageSearch($query); 255 } else { 256 // page id searching (i.e. namespace and name, faster) 257 258 // fullregex option considers entire query to be a regex 259 // over the whole page id, incl. namespace 260 if (!$data['fullregex']) { 261 [$query, $incl_ns, $excl_ns] = $pq->parseNamespaceQuery($query); 262 } 263 264 // Allow for a lazy man's option! 265 if ($query === '*') { 266 $query = '.*'; 267 } 268 // search by page name or path only 269 $results = $pq->pageLookup($query, $data['fullregex'], $incl_ns, $excl_ns); 270 } 271 $no_result = false; 272 $sort_array = []; 273 if ($results == false) { 274 $no_result = true; 275 $message = $this->getLang('regex_error'); 276 } elseif (!empty($results)) { 277 $results = $pq->validatePages($results, $data['hidestart'], $data['maxns']); 278 // prepare the necessary sorting arrays, as per users options 279 [$sort_array, $sort_opts, $group_opts] = $pq->buildSortingArray($results, $data); 280 281 // meta data filtering of the list is next 282 $sort_array = $pq->filterMetadata($sort_array, $data['filter']); 283 if ($sort_array === []) { 284 $no_result = true; 285 $message = $this->getLang("empty_filter"); 286 } 287 } else { 288 $no_result = true; 289 } 290 // successful search... 291 if (!$no_result) { 292 // now do the sorting 293 $pq->msort($sort_array, $sort_opts); 294 // limit the result list length if required; this can only be done after sorting! 295 if ($data['limit'] > 0) { 296 $sort_array = array_slice($sort_array, 0, $data['limit']); 297 } 298 // do a link count BEFORE grouping (don't want to count headers...) 299 $count = count($sort_array); 300 // and finally the grouping 301 $keys = ['name', 'id', 'title', 'abstract', 'display']; 302 if (!$data['group']) { 303 $group_opts = []; 304 } 305 $sorted_results = $pq->mgroup($sort_array, $keys, $group_opts); 306 // and out to the page 307 $renderer->doc .= $pq->renderAsHtml($data['layout'], $sorted_results, $data, $count); 308 // no results... 309 } elseif (!$data['hidemsg']) { 310 $renderer->doc .= $pq->renderAsEmpty($query, $message); 311 } 312 return true; 313 } elseif ($format === 'metadata') { 314 // this is a pagequery page needing PARSER_CACHE_USE event trigger; 315 $renderer->meta['pagequery'] = true; 316 unset($renderer->persistent['pagequery']); 317 return true; 318 } else { 319 return false; 320 } 321 } 322} 323