1<?php 2 3// lightmenu utilities class 4 5class lightmenu 6{ 7 protected static $syntax = [ 8 '|^-head$|' => 'head', 9 '|^-min=(\d+)$|' => 'min', 10 '|^-sort=([\w,]+)$|' => 'sort' 11 ]; 12 protected static $options; 13 protected static $collator; 14 15 protected static function _log(string $format, string ...$params) 16 { 17 file_put_contents(__DIR__.'/lightmenu.log',date('Y/m/d H:i:s').':'.vsprintf($format,$params),FILE_APPEND); 18 } 19 20 protected static function _options(string $match) : array 21 { 22 self::$options = []; 23 24 $list = preg_split('|\s+|',$match); 25 foreach ($list as $option) 26 { 27 foreach (self::$syntax as $regex => $name) 28 { 29 if (preg_match($regex,$option,$matches)) 30 { 31 self::$options[$name] = (count($matches) === 2)?$matches[1]:true; 32 break; 33 } 34 } 35 } 36 37 return self::$options; 38 } 39 40 // function give lightmenu meta data file path from page id and environment 41 // $id : dokuwiki page id 42 // return : lightmenu meta data file path 43 protected static function _meta_path(string $id) : string 44 { 45 global $conf; 46 47 $_id = ':'.$id; 48 $pos = strrpos($_id,':'); 49 $path = strtr(substr($_id,0,$pos),':','/'); 50 $name = substr($_id,$pos + 1); 51 if (($name === $conf['start'] && ($path !== '')) || (basename($path) === $name)) 52 return $path; 53 if (is_dir(sprintf('%s%s/%s',$conf['datadir'],$path,$name))) 54 return sprintf('%s/%s',$path,$name); 55 return sprintf('%s/%s.txt',$path,$name); 56 } 57 58 // function give lightmenu meta data file path from page id and environment 59 // $id : dokuwiki page id 60 // return : lightmenu meta data file path 61 protected static function _subpath(string $id) : string 62 { 63 global $conf; 64 65 $parts = explode(':',$id); 66 $path = implode('/',array_map(function ($e) { 67 return rawurlencode($e); 68 }, array_slice($parts, 0, -1))); 69 $name = rawurlencode($parts[count($parts) - 1]); 70 if (($name === $conf['start'] && ($path !== '')) || (basename($path) === $name)) 71 return $path; 72 if (is_dir(sprintf('%s/%s/%s',$conf['datadir'],$path,$name))) 73 return sprintf('/%s/%s',$path,$name); 74 return sprintf('/%s/%s.txt',$path,$name); 75 } 76 77 78 // return data about wiki path identified by file subpath and name 79 // subpath : the sub path to the file from data page root directory 80 // name : the file name of the page or directory (ex : start.txt) 81 // returned values : array 82 // (boolean) true if the $name is a page, false if a directory 83 // (string) the dokuwiki id of the page 84 // (array) the lightmenu meta data 85 protected static function _get_page_data(string $subpath, string $name) : array 86 { 87 global $conf; 88 89 $path = sprintf('%s%s/%s.lightmenu.json',$conf['metadir'],$subpath,$name); 90 $data = []; 91 if (is_file($path) && is_readable($path)) 92 $data = json_decode(file_get_contents($path),true,2,JSON_THROW_ON_ERROR); 93 return [ 94 $is_page = substr($name,-4) === '.txt', 95 rawurldecode(($is_page)?substr($name,0,-4):$name), 96 $data 97 ]; 98 } 99 100 protected static function _set_page_data(string $subpath, array &$data) 101 { 102 global $conf; 103 104 $path = sprintf('%s%s.lightmenu.json',$conf['metadir'],'/'.ltrim($subpath,'/')); 105 if (is_file($path) && (! is_writable($path))) 106 throw new Exception(sprintf('Lightmenu : meta data file "%s" not writable.',$path)); 107 if (is_file($path) && (count($data) === 0)) 108 unlink($path); 109 else 110 { 111 if (! is_dir(dirname($path))) 112 mkdir(dirname($path),0755,true); 113 if (file_put_contents($path,json_encode($data,JSON_THROW_ON_ERROR)) === false) 114 throw new Exception('Lightmenu unable to write meta data.'); 115 } 116 } 117 118 protected static function _touch_sidebar() 119 { 120 global $conf; 121 122 if (is_writeable($path = $conf['datadir'].'/'.$conf['sidebar'].'.txt')) 123 touch($path); 124 } 125 126 public static function update(string $id, string &$contents) 127 { 128 $data = []; 129 if (preg_match('|<lm:([^>]+)>|',$contents,$matches)) 130 $data = json_decode(trim($matches[1]),true,2,JSON_THROW_ON_ERROR); 131 if (preg_match('/(?:^|[^=])======([^=\n]+)======(?:$|[^=])/',$contents,$matches)) 132 $data['head'] = trim($matches[1]); 133 self::_set_page_data(self::_subpath($id),$data); 134 self::_touch_sidebar(); 135 } 136 137 protected static function _str_compare(string $a, string $b, bool $use_locale = false) : int 138 { 139 if (self::$collator === null) 140 return strcmp($a,$b) * (($a === '')?-1:1) * (($b === '')?-1:1); 141 else 142 return self::$collator->compare($a,$b); 143 } 144 145 protected static function _browse(string $subpath = '', string $sort_criteria = '') : array 146 { 147 global $conf; 148 149 $tree = []; 150 $times = []; 151 $path = $conf['datadir'].$subpath; 152 $list = scandir($path); 153 154 foreach ($list as $name) 155 { 156 if (($name === '.') || ($name === '..')) 157 continue; 158 $filepath = $path.'/'.$name; 159 [$is_page,$id,$data] = self::_get_page_data($subpath,$name); 160 if (is_dir($filepath)) 161 $tree[] = [$id,$data,self::_browse($subpath.'/'.$name,$data['_sort'] ?? $sort_criteria)]; 162 elseif ($is_page) 163 { 164 $short = substr($name,0,strrpos($name,'.')); 165 if (($id === $conf['start']) || is_dir($path.'/'.$short) || ($short === basename($path))) 166 continue; 167 $tree[] = [$id,$data]; 168 } 169 $times[$id] = filemtime($filepath); 170 } 171 if ($sort_criteria === '') 172 $sort_criteria = self::$options['sort'] ?? 'type_asc,id_asc'; 173 $sort = function ($a,$b) use (&$times,$sort_criteria) { 174 $orders = explode(',',$sort_criteria); 175 foreach ($orders as $order) 176 { 177 if (! preg_match('/^([^_]+)_(asc|desc)$/',$order,$matches)) 178 continue; 179 [,$type,$dir] = $matches; 180 if ($type === 'type') 181 $diff = count($b) - count($a); // Count: 2 file, 3 folder. Folder come before file 182 elseif ($type === 'id') 183 $diff = self::_str_compare($a[0],$b[0]); 184 elseif ($type === 'date') 185 $diff = $times[$a[0]] - $times[$b[0]]; 186 elseif ($type === 'label') 187 $diff = self::_str_compare($a[1]['label'] ?? ($a[1]['head'] ?? ''),$b[1]['label'] ?? ($b[1]['head'] ?? '')); 188 elseif (is_int($a[1][$key = '_oc_'.$type] ?? '') || is_int($b[1][$key] ?? '')) 189 $diff = ($a[1][$key] ?? ($b[1][$key] + 1)) - ($b[1][$key] ?? ($a[1][$key] + 1)); 190 else 191 $diff = self::_str_compare($a[1]['_oc_'.$type] ?? '',$b[1]['_oc_'.$type] ?? ''); 192 if ($diff !== 0) 193 return ($dir === 'asc')?$diff:-$diff; 194 } 195 return self::_str_compare($a[0],$b[0]); 196 }; 197 usort($tree,$sort); 198 return $tree; 199 } 200 201 /* browse the wiki hierarchy to get data needed for Lightmenu tree. 202 $options : string of options after "<lightmenu" tag. 203 return : array with start page data, wiki hierachy tree data and options 204 */ 205 public static function get_data(string $options) : array 206 { 207 global $conf; 208 209 if (extension_loaded('intl')) 210 self::$collator = new Collator($conf['lang']); 211 else 212 self::$collator = null; 213 lightmenu::_options($options); 214 [$is_page,$id,$data] = self::_get_page_data('',$conf['start'].'.txt'); 215 return [[$id,$data],self::_browse(),self::$options]; 216 } 217 218 protected static function _get_label(string $id,array &$data) : string 219 { 220 global $conf; 221 222 if (isset($data['label.'.$conf['lang']])) 223 return $data['label.'.$conf['lang']]; 224 if (isset($data['label'])) 225 return $data['label']; 226 if (self::$options['head'] && isset($data['head'])) 227 return $data['head']; 228 return $id; 229 } 230 231 protected static function _format_attributes(array &$metas) : string 232 { 233 $html = ''; 234 235 foreach ($metas as $name => $value) 236 { 237 if ((strncmp($name,'label',5) === 0) || ($name === 'head') || ($name === 'title') || ($name === 'href') || (strncmp($name,'_',1) === 0)) 238 continue; 239 $html .= sprintf(' %s="%s"',$name,$value); 240 } 241 242 return $html; 243 } 244 245 protected static function _get_page(string $prefix, string $id, array &$metas) : string 246 { 247 $label = self::_get_label($id,$metas); 248 $html = '<div class="child">'.PHP_EOL; 249 $html .= sprintf('<span class="label" id="lm-%s%s"><a%s title="%s" href="%s">%s</a></span>'.PHP_EOL, 250 $prefix,$id,self::_format_attributes($metas),isset($metas['title'])?$metas['title']:$label,wl($prefix.$id),trim($label)); 251 $html .= '</div>'.PHP_EOL; 252 return $html; 253 } 254 255 protected static function _get_html(array &$data, string $prefix = '', int $level = 0) : string 256 { 257 global $conf; 258 259 $html = ''; 260 261 foreach ($data as $values) 262 { 263 $n = count($values); 264 if ($n === 3) 265 { 266 [$id,$metas,$children] = $values; 267 $label = self::_get_label($id,$metas); 268 $html .= '<div class="child">'.PHP_EOL; 269 $html .= sprintf('<input type="checkbox" id="checkbox-%s%s" />',$prefix,$id); 270 $html .= sprintf('<label class="label" id="lm-%s%s" for="checkbox-%s%s"><a%s title="%s" href="%s">%s</a></label>'.PHP_EOL,$prefix,$id,$prefix,$id, 271 self::_format_attributes($metas),isset($metas['title'])?$metas['title']:$label,wl($prefix.$id.':'),trim($label)); 272 if (count($children) > 0) 273 $html .= '<div class="tree">'.PHP_EOL.self::_get_html($children,$prefix.$id.':').'</div>'.PHP_EOL; 274 $html .= '</div>'.PHP_EOL; 275 } 276 elseif ($n === 2) 277 { 278 [$id,$metas] = $values; 279 if (($prefix === '') && ($id === $conf['sidebar'])) 280 continue; 281 $html .= self::_get_page($prefix,$id,$metas); 282 } 283 else 284 throw new Exception('lightmenu error : wrong entry data count'); 285 } 286 287 return $html; 288 } 289 290 public static function render(array &$data) : string 291 { 292 self::$options = $data[2]; 293 $html = '<div class="lm">'; 294 $html .= self::_get_page('',$data[0][0],$data[0][1]); 295 $html .= self::_get_html($data[1]); 296 $html .= '</div>'; 297 298 return $html; 299 } 300 301 protected static function _rescan(string $subpath = '') 302 { 303 global $conf; 304 305 $path = $conf['datadir'].$subpath; 306 $list = scandir($path); 307 foreach ($list as $name) 308 { 309 if (($name === '.') || ($name === '..')) 310 continue; 311 if (($subpath === '') && ($name === $conf['sidebar'].'.txt')) 312 continue; 313 $filepath = $path.'/'.$name; 314 [$is_page,$id,$data] = self::_get_page_data($subpath,$name); 315 if (is_dir($filepath)) 316 self::_rescan($subpath.'/'.$name); 317 elseif ($is_page) 318 { 319 $contents = file_get_contents($filepath); 320 if (preg_match('/(?:^|[^=])======([^=\n]+)======(?:$|[^=])/',$contents,$matches)) 321 { 322 $data['head'] = trim($matches[1]); 323 if ((($id === $conf['start']) && ($subpath !== '')) || ($id === basename($path))) 324 self::_set_page_data($subpath,$data); 325 elseif (is_dir($path.'/'.$id)) 326 self::_set_page_data($subpath.'/'.$id,$data); 327 else 328 self::_set_page_data($subpath.'/'.$name,$data); 329 } 330 } 331 } 332 } 333 334 public static function rescan() 335 { 336 global $conf; 337 338 self::_rescan(); 339 self::_touch_sidebar(); 340 } 341}