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