aboutsummaryrefslogblamecommitdiffstats
path: root/Zotlabs/Lib/NativeWikiPage.php
blob: 941ade90c8a958fb29b0408e4ba29cfdf45d511a (plain) (tree)





















                                                                                                       
                                                                                                                                      
                                                 









                                                                                                               

                                                                                              













                                                                                                                             



                                                                                                               


                                           

                                                       
                                                    




                                                                
 

                                                                                                                       


                                                       
                                                                                                   
 
                                                            
 
                                   


                                                                 

                                                                
                          
 
                                                                                                                                                              

                                                                                          


                                                  
 






                                                                                                         
                                  


                                                                                            
 




                                                                                               
 

                                                                                                         

                 
 
                          
 




                                                                                                           
 



                                                                                                
 






                                                                                  
                 
 
                                                                                                            









                                                                                                                
 












































                                                                                                                  
                                                                                                                      














                                                                                               
 






                                                                                                             
 




                                                                                                               
 



                                                                                                                                                              
 







                                                          
                                                                              
 
                                     


                                                                                   

                          

 




                                                                                                                                                                
 




                                                                  
 






























                                                                                                                                                              
                                                                              
 



                                                         
                                                                                                                                                                             










































                                                                                                                                 


                                                                                                         

                                                                                                         
                                                                                                        





                                                                                               







                                                                                               
        

                                                           



                                                                                           


                                                                                  


                                       

                                                 
                                                       

                                                                                                                            










                                                                                               
                                                                                                                                











                                                                                                         

                                                                                            
                 







                                                                                               
 


                                                           
                         


                          
                                         



                                                                                        



































                                                                                                                       

                                                                                                           


                                                                                                         

                                                                                          
                                  

                                                                                               

















                                                                                                 
                 

                                                                                             


                                             
 












                                                                                                                        



                                                                                               
 
                                              
 

                                                                                                        
                                                                                                















































                                                                                                                            
                                                                                                                          
                        

























                                                                                                                                             
        
                          









































































                                                                                                      
 
<?php

namespace Zotlabs\Lib;

use \Zotlabs\Lib as Zlib;

class NativeWikiPage {

	static public function page_list($channel_id,$observer_hash, $resource_id) {

		// TODO: Create item table records for pages so that metadata like title can be applied
		$w = Zlib\NativeWiki::get_wiki($channel_id,$observer_hash,$resource_id);

		$pages[] = [
			'resource_id' => '',
			'title'       => 'Home',
			'url'         => 'Home',
			'link_id'     => 'id_wiki_home_0'
		];

		$sql_extra = item_permissions_sql($channel_id,$observer_hash);

		$r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and item_deleted = 0 
			$sql_extra group by mid",
			dbesc($resource_id),
			intval($channel_id)
		);
		if($r) {
			$items = fetch_post_tags($r,true);
			foreach($items as $page_item) {
				$title = get_iconfig($page_item['id'],'nwikipage','pagetitle',t('(No Title)'));
				if(urldecode($title) !== 'Home') {
					$pages[] = [
						'resource_id' => $resource_id,
						'title'       => escape_tags($title),
						'url'         => urlencode(urlencode($title)),
						'link_id'     => 'id_' . substr($resource_id, 0, 10) . '_' . $page_item['id']
					];
				}
			}
		}

		return array('pages' => $pages, 'wiki' => $w);
	}


	static public function create_page($channel_id, $observer_hash, $name, $resource_id) {

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);

		if (! $w['wiki']) {
			return array('content' => null, 'message' => 'Error reading wiki', 'success' => false);
		}

		// create an empty activity

		$arr = [];
		$arr['uid']           = $channel_id;
		$arr['author_xchan']  = $observer_hash;
		$arr['resource_type'] = 'nwikipage';
		$arr['resource_id']   = $resource_id;
		$arr['allow_cid']     = $w['wiki']['allow_cid'];
		$arr['allow_gid']     = $w['wiki']['allow_gid'];
		$arr['deny_cid']      = $w['wiki']['deny_cid'];
		$arr['deny_gid']      = $w['wiki']['deny_gid'];

		$arr['public_policy'] = map_scope(\Zotlabs\Access\PermissionLimits::Get($channel_id,'view_wiki'),true);

		// We may wish to change this some day.
		$arr['item_unpublished'] = 1;

		set_iconfig($arr,'nwikipage','pagetitle',(($name) ? $name : t('(No Title)')),true);

		$p = post_activity_item($arr, false, false);

		if($p['item_id']) {
			$page = [ 
				'rawName'  => $name,
				'htmlName' => escape_tags($name),
				'urlName'  => urlencode($name), 

			];

			return array('page' => $page, 'item_id' => $p['item_id'], 'item' => $p['activity'], 'wiki' => $w, 'message' => '', 'success' => true);
		}
		return [ 'success' => false, 'message' => t('Wiki page create failed.') ];
	}

	static public function rename_page($arr) {

		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']   : '');
		$pageNewName   = ((array_key_exists('pageNewName',$arr))   ? $arr['pageNewName']   : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']   : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if(! $w['wiki']) {
			return array('message' => t('Wiki not found.'), 'success' => false);
		}


		$ic = q("select * from iconfig left join item on iconfig.iid = item.id 
			where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'",
			intval($channel_id),
			dbesc($pageNewName)
		);

		if($ic) {
			return [ 'success' => false, 'message' => t('Destination name already exists') ];
		}


		$ids = [];

		$ic = q("select *, item.id as item_id from iconfig left join item on iconfig.iid = item.id 
			where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'",
			intval($channel_id),
			dbesc($pageUrlName)
		);

		if($ic) {
			foreach($ic as $c) {
				set_iconfig($c['item_id'],'nwikipage','pagetitle',$pageNewName);
			}

			$page = [ 
				'rawName'  => $pageNewName, 
				'htmlName' => escape_tags($pageNewName), 
				'urlName'  => urlencode(escape_tags($pageNewName))
			];

			return [ 'success' => true, 'page' => $page ];
		}

		return [ 'success' => false, 'item_id' => $c['item_id'], 'message' => t('Page not found') ];
	
	}

	static public function get_page_content($arr) {
		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']        : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']        : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash']      : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? intval($arr['channel_id']) : 0);
		$revision      = ((array_key_exists('revision',$arr))      ? intval($arr['revision'])   : (-1));


		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if (! $w['wiki']) {
			return array('content' => null, 'message' => 'Error reading wiki', 'success' => false);
		}

		$item = self::load_page($arr);

		if($item) {
			$content = $item['body'];

			return [ 
				'content' => json_encode($content), 
				'mimeType' => $w['mimeType'], 
				'message' => '', 
				'success' => true
			];
		}
	
		return array('content' => null, 'message' => t('Error reading page content'), 'success' => false);

	}

	static public function page_history($arr) {
		$pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : '');
		$resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if (!$w['wiki']) {
			return array('history' => null, 'message' => 'Error reading wiki', 'success' => false);
		}

		$items = self::load_page_history($arr);

		$history = [];

		if($items) {
			$processed = 0;
			foreach($items as $item) {
				if($processed > 1000)
					break;
				$processed ++;
				$history[] = [ 
					'revision' => $item['revision'],
					'date' => datetime_convert('UTC',date_default_timezone_get(),$item['edited']),
					'name' => $item['author']['xchan_name'],
					'title' => get_iconfig($item,'nwikipage','commit_msg') 
				];

			}

			return [ 'success' => true, 'history' => $history ];
		}

		return [ 'success' => false ];

	}
	

	static public function load_page($arr) {

		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']     : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']     : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash']   : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']      : 0);
		$revision      = ((array_key_exists('revision',$arr))      ? $arr['revision']        : (-1));

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);

		if (! $w['wiki']) {
			return array('content' => null, 'message' => 'Error reading wiki', 'success' => false);
		}

		$ids = '';

		$ic = q("select * from iconfig left join item on iconfig.iid = item.id where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'",
			intval($channel_id),
			dbesc($pageUrlName)
		);

		if($ic) {
			foreach($ic as $c) {
				if($ids)
					$ids .= ',';
				$ids .= intval($c['iid']);
			}
		}

		$sql_extra = item_permissions_sql($channel_id,$observer_hash);

		if($revision == (-1))
			$sql_extra .= " order by revision desc ";
		elseif($revision)
			$sql_extra .= " and revision = " . intval($revision) . " ";

		$r = null;


		if($ids) {
			$r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and id in ( $ids ) $sql_extra limit 1",
				dbesc($resource_id),
				intval($channel_id)
			);

			if($r) {
				$items = fetch_post_tags($r,true);
				return $items[0];
			}
		}

		return null;
	}

	static public function load_page_history($arr) {

		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']     : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']     : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash']   : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']      : 0);
		$revision      = ((array_key_exists('revision',$arr))      ? $arr['revision']        : (-1));

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if (! $w['wiki']) {
			return array('content' => null, 'message' => 'Error reading wiki', 'success' => false);
		}

		$ids = '';

		$ic = q("select * from iconfig left join item on iconfig.iid = item.id where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'",
			intval($channel_id),
			dbesc($pageUrlName)
		);
	
		if($ic) {
			foreach($ic as $c) {
				if($ids)
					$ids .= ',';
				$ids .= intval($c['iid']);
			}
		}

		$sql_extra = item_permissions_sql($channel_id,$observer_hash);

		$sql_extra .= " order by revision desc ";

		$r = null;
		if($ids) {
			$r = q("select * from item where resource_type = 'nwikipage' and resource_id = '%s' and uid = %d and id in ( $ids ) and item_deleted = 0 $sql_extra",
				dbesc($resource_id),
				intval($channel_id)
			);
			if($r) {
				xchan_query($r);
				$items = fetch_post_tags($r,true);
				return $items;
			}
		}

		return null;
	}



	static public function prepare_content($s) {
			
		$text = preg_replace_callback('{
					(?:\n\n|\A\n?)
					(	            # $1 = the code block -- one or more lines, starting with a space/tab
					  (?>
						[ ]{'.'4'.'}  # Lines must start with a tab or a tab-width of spaces
						.*\n+
					  )+
					)
					((?=^[ ]{0,'.'4'.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
				}xm',
				'self::nwiki_prepare_content_callback', $s);
	
		return $text;
	}
	
	static public function nwiki_prepare_content_callback($matches) {
		$codeblock = $matches[1];
	
		$codeblock = htmlspecialchars($codeblock, ENT_NOQUOTES, UTF8, false);
		return "\n\n" . $codeblock ;
	}
	
	
	
	static public function save_page($arr) {

		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']   : '');
		$content       = ((array_key_exists('content',$arr))       ? $arr['content']       : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']   : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);
		$revision      = ((array_key_exists('revision',$arr))      ? $arr['revision']      : 0);

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);

		if (!$w['wiki']) {
			return array('message' => t('Error reading wiki'), 'success' => false);
		}

		$mimetype = $w['mimeType'];
		if($mimetype === 'text/markdown') {
			$content = purify_html(Zlib\NativeWikiPage::prepare_content($content));
		}
		else {
			$content = escape_tags($content);
		}
	
		// fetch the most recently saved revision. 

		$item = self::load_page($arr);
		if(! $item) {
			return array('message' => t('Page not found'), 'success' => false);
		}

		// change just the fields we need to change to create a revision; 

		unset($item['id']);
		unset($item['author']);

		$item['parent']       = 0;
		$item['body']         = $content;
		$item['author_xchan'] = $observer_hash;
		$item['revision']     = (($arr['revision']) ? intval($arr['revision']) + 1 : intval($item['revision']) + 1);
		$item['edited']       = datetime_convert();

		if($item['iconfig'] && is_array($item['iconfig']) && count($item['iconfig'])) {
			for($x = 0; $x < count($item['iconfig']); $x ++) {
				unset($item['iconfig'][$x]['id']);
				unset($item['iconfig'][$x]['iid']);
			}
		}

		$ret = item_store($item, false, false);

		if($ret['item_id'])
			return array('message' => '', 'item_id' => $ret['item_id'], 'filename' => $filename, 'success' => true);
		else
			return array('message' => t('Page update failed.'), 'success' => false);
	}	

	static public function delete_page($arr) {
		$pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : '');
		$resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : '');
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);

		if(! $w['wiki']) {
			return [ 'success' => false, 'message' => t('Error reading wiki') ];
		}

		$ids = [];

		$ic = q("select * from iconfig left join item on iconfig.iid = item.id 
			where uid = %d and cat = 'nwikipage' and k = 'pagetitle' and v = '%s'",
			intval($channel_id),
			dbesc($pageUrlName)
		);

		if($ic) {
			foreach($ic as $c) {
				$ids[] = intval($c['iid']);
			}
		}

		if($ids) {
			drop_items($ids);
			return [ 'success' => true ];
		}

		return [ 'success' => false, 'message' => t('Nothing deleted') ];	
	}
	
	static public function revert_page($arr) {
		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']   : '');
		$resource_id   = ((array_key_exists('resource_id',$arr))   ? $arr['resource_id']   : '');
		$commitHash    = ((array_key_exists('commitHash',$arr))    ? $arr['commitHash']    : null);
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);

		if (! $commitHash) {
			return array('content' => $content, 'message' => 'No commit was provided', 'success' => false);
		}

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if (!$w['wiki']) {
			return array('content' => $content, 'message' => 'Error reading wiki', 'success' => false);
		}

		$x = $arr;

		if(intval($commitHash) > 0) {
			unset($x['commitHash']);
			$x['revision'] = intval($commitHash) - 1;
			$loaded = self::load_page($x);

			if($loaded) {
				$content = $loaded['body'];
				return [ 'content' => $content, 'success' => true ];
			}
			return [ 'content' => $content, 'success' => false ]; 
		}
	}
	
	static public function compare_page($arr) {
		$pageUrlName = ((array_key_exists('pageUrlName',$arr)) ? $arr['pageUrlName'] : '');
		$resource_id = ((array_key_exists('resource_id',$arr)) ? $arr['resource_id'] : '');
		$currentCommit = ((array_key_exists('currentCommit',$arr)) ? $arr['currentCommit'] : (-1));
		$compareCommit = ((array_key_exists('compareCommit',$arr)) ? $arr['compareCommit'] : 0);
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);

		if (!$w['wiki']) {
			return array('message' => t('Error reading wiki'), 'success' => false);
		}

		$x = $arr;
		$x['revision'] = (-1);

		$currpage = self::load_page($x);
		if($currpage)
			$currentContent = $currpage['body'];

		$x['revision'] = $compareCommit;
		$comppage = self::load_page($x);
		if($comppage)
			$compareContent = $comppage['body'];

		if($currpage && $comppage) {
			require_once('library/class.Diff.php');
			$diff = \Diff::toTable(\Diff::compare($currentContent, $compareContent));

			return [ 'success' => true, 'diff' => $diff ];
		}
		return [ 'success' => false, 'message' =>  t('Compare: object not found.') ];

	}
	
	static public function commit($arr) {

		$commit_msg    = ((array_key_exists('commit_msg', $arr))   ? $arr['commit_msg']    : t('Page updated'));
		$observer_hash = ((array_key_exists('observer_hash',$arr)) ? $arr['observer_hash'] : '');
		$channel_id    = ((array_key_exists('channel_id',$arr))    ? $arr['channel_id']    : 0);
		$pageUrlName   = ((array_key_exists('pageUrlName',$arr))   ? $arr['pageUrlName']   : t('Untitled'));

		if(array_key_exists('resource_id', $arr)) {
			$resource_id = $arr['resource_id'];
		}
		else {
			return array('message' => t('Wiki resource_id required for git commit'), 'success' => false);
		}

		$w = Zlib\NativeWiki::get_wiki($channel_id, $observer_hash, $resource_id);
		if (! $w['wiki']) {
			return array('message' => t('Error reading wiki'), 'success' => false);
		}


		$page = self::load_page($arr);

		if($page) {
			set_iconfig($page['id'],'nwikipage','commit_msg',escape_tags($commit_msg),true);
			return [ 'success' => true, 'item_id' => $page['id'], 'page' => $page ];
		}

		return [ 'success' => false, 'message' => t('Page not found.') ];

	}
	
	static public function convert_links($s, $wikiURL) {
		
		if (strpos($s,'[[') !== false) {
			preg_match_all("/\[\[(.*?)\]\]/", $s, $match);
			$pages = $pageURLs = array();
			foreach ($match[1] as $m) {
				// TODO: Why do we need to double urlencode for this to work?
				$pageURLs[] = urlencode(urlencode(escape_tags($m)));
				$pages[] = $m;
			}
			$idx = 0;
			while(strpos($s,'[[') !== false) {
			$replace = '<a href="'.$wikiURL.'/'.$pageURLs[$idx].'">'.$pages[$idx].'</a>';
				$s = preg_replace("/\[\[(.*?)\]\]/", $replace, $s, 1);
				$idx++;
			}
		}
		return $s;
	}
	
	/**
	 * Replace the instances of the string [toc] with a list element that will be populated by
	 * a table of contents by the JavaScript library
	 * @param string $s
	 * @return string
	 */
	static public function generate_toc($s) {
		if (strpos($s,'[toc]') !== false) {
			//$toc_md = wiki_toc($s);	// Generate Markdown-formatted list prior to HTML render
			$toc_md = '<ul id="wiki-toc"></ul>'; // use the available jQuery plugin http://ndabas.github.io/toc/
			$s = preg_replace("/\[toc\]/", $toc_md, $s, -1);
		}
		return $s;
	}
	
	/**
	 *  Converts a select set of bbcode tags. Much of the code is copied from include/bbcode.php
	 * @param string $s
	 * @return string
	 */
	static public function bbcode($s) {
			
		$s = str_replace(array('[baseurl]', '[sitename]'), array(z_root(), get_config('system', 'sitename')), $s);
			
		$s = preg_replace_callback("/\[observer\.language\=(.*?)\](.*?)\[\/observer\]/ism",'oblanguage_callback', $s);

		$s = preg_replace_callback("/\[observer\.language\!\=(.*?)\](.*?)\[\/observer\]/ism",'oblanguage_necallback', $s);


		$observer = \App::get_observer();
		if ($observer) {
			$s1 = '<span class="bb_observer" title="' . t('Different viewers will see this text differently') . '">';
			$s2 = '</span>';
			$obsBaseURL = $observer['xchan_connurl'];
			$obsBaseURL = preg_replace("/\/poco\/.*$/", '', $obsBaseURL);
			$s = str_replace('[observer.baseurl]', $obsBaseURL, $s);
			$s = str_replace('[observer.url]', $observer['xchan_url'], $s);
			$s = str_replace('[observer.name]', $s1 . $observer['xchan_name'] . $s2, $s);
			$s = str_replace('[observer.address]', $s1 . $observer['xchan_addr'] . $s2, $s);
			$s = str_replace('[observer.webname]', substr($observer['xchan_addr'], 0, strpos($observer['xchan_addr'], '@')), $s);
			$s = str_replace('[observer.photo]', '', $s);
		} 
		else {
			$s = str_replace('[observer.baseurl]', '', $s);
			$s = str_replace('[observer.url]', '', $s);
			$s = str_replace('[observer.name]', '', $s);
			$s = str_replace('[observer.address]', '', $s);
			$s = str_replace('[observer.webname]', '', $s);
			$s = str_replace('[observer.photo]', '', $s);
		}
	
		return $s;
	}
	
	static public function get_file_ext($arr) {
		if($arr['mimeType'] == 'text/bbcode')
			return '.bb';
		else
			return '.md';
	}
	
	// This function is derived from 
	// http://stackoverflow.com/questions/32068537/generate-table-of-contents-from-markdown-in-php
	static public function toc($content) {
	  // ensure using only "\n" as line-break
	  $source = str_replace(["\r\n", "\r"], "\n", $content);
	
	  // look for markdown TOC items
	  preg_match_all(
		'/^(?:=|-|#).*$/m',
		$source,
		$matches,
		PREG_PATTERN_ORDER | PREG_OFFSET_CAPTURE
	  );
	
	  // preprocess: iterate matched lines to create an array of items
	  // where each item is an array(level, text)
	  $file_size = strlen($source);
	  foreach ($matches[0] as $item) {
		$found_mark = substr($item[0], 0, 1);
		if ($found_mark == '#') {
		  // text is the found item
		  $item_text = $item[0];
		  $item_level = strrpos($item_text, '#') + 1;
		  $item_text = substr($item_text, $item_level);
		} else {
		  // text is the previous line (empty if <hr>)
		  $item_offset = $item[1];
		  $prev_line_offset = strrpos($source, "\n", -($file_size - $item_offset + 2));
		  $item_text =
			substr($source, $prev_line_offset, $item_offset - $prev_line_offset - 1);
		  $item_text = trim($item_text);
		  $item_level = $found_mark == '=' ? 1 : 2;
		}
		if (!trim($item_text) OR strpos($item_text, '|') !== FALSE) {
		  // item is an horizontal separator or a table header, don't mind
		  continue;
		}
		$raw_toc[] = ['level' => $item_level, 'text' => trim($item_text)];
	  }
		$o = '';
		foreach($raw_toc as $t) {
			$level = intval($t['level']);
			$text = $t['text'];
			switch ($level) {
				case 1:
					$li = '* ';
					break;
				case 2:
					$li = '  * ';
					break;
				case 3:
					$li = '    * ';
					break;
				case 4:
					$li = '      * ';
					break;
				default:
					$li = '* ';
					break;
			}
			$o .= $li . $text . "\n";
		}
	  return $o;
	}

}