<?php
// PukiWiki - Yet another WikiWikiWeb clone.
//
// $Id:$
//
// List plugin 2_1

/*
*説明 [#m17ed6bb]
[[ls2>PukiWiki/1.4/マニュアル/プラグイン/l#d2ce34ea]] 拡張((ls2 v1.23 の拡張です。))。[[自作プラグイン/ls3.inc.php]] とは違い、
ページ名による階層構造だけでリストする純粋な ls2 の拡張です。
MenuBar に #ls2_1(hogehoge/,depth=1,relative) のようにおいておくと便利です。
**標準プラグイン ls2 からの変更点(初版) [#k9cb86b6]
-階層指定可能。
-階層的リスト表示機能。
-相対パス的表示機能。
-pukiwiki.ini.php で設定する $non_list の利用。
-include の無限ループを修正。
-link 時に常に include, title オプションが付加されていたが、単純に同時指定したオプションを利用するように変更。
それに伴いリンク名として利用される引数を「オプションと判定されない引数以降のすべての引数」から link=リンク名 と指定するように変更
((ls2_1.inc.php v1.18 からです。それ以前は「オプションと判定されない最初の引数」でした))。
**その後の追加機能 [#u2116eb9]
-表示件数指定機能
-除外ページ指定機能
-更新日時表示機能
-更新日時によるソート機能
-正規表現によるページのフィルタ機能

*書式 [#jcb5f796]
 #ls2_1(パターン[,オプション])

**パラメータ [#r8a06bfd]
-パターン(最初に指定)
~リストするページ名のパターン。省略するときもカンマが必要。
省略時はカレントページ+"/"が指定されたことになる。
また / を指定した場合はすべてのページにマッチする。
また // を指定した場合は"カレントページ"が指定されたことになる(もろ後付け)。
-title=true|false
~ページ中の見出しもリストする。
title だけで title=true の意味になる。
-include=true|false
~インクルードしているページもリストする。
include だけでも include=true の意味になる。
-link=リンク名
~actionプラグインを呼び出すリンクを表示。
link だけの場合は「パターン」の部分を使用したリンク名が作られる。
-reverse=true|false
~ページの並び順を反転し、降順にする。
reverse だけでも reverse=true の意味にる。
Note: hierarchy,relative コンビとの併用はきっと納得のいかない表示になります(昇順用に設計されたオプションなので)。
-compact=true|false
~リストのレベルを調整する。
compact だけでも compact=true の意味になる。
ファイル中の PLUGIN_LS2_1_LIST_COMPACT で初期値を設定できます。デフォルトでは TRUE です。
現在デフォルトで TRUE です。
-title_compact=true|false
~title オプション用の compact 機能。
title_compact だけでも title_compact=true の意味になる。
ファイル中の PLUGIN_LS2_1_LIST_TITLE_COMPACT で初期値を設定できます。デフォルトでは TRUE です。
現在デフォルトで TRUE です。
-depth=\d*[-+]?\d*((\d*[-+]?\d* は正規表現による表記です。\d は数字のことです。))
~階層指定。1 なら 1 階層下のページのみを表示する。
2-4 のような指定も可能 (2,3,4 の意)。2- のように指定すると 2 階層下以下のページ。
2+1 のような指定も可能 (2 とそこから 1 階層下。つまり 2,3 の意)。
//0-2 = false-2 = -2. 1-0 = 1-false = 1-. 2+ = 2+false = 2+0. +2 = false+2 = 0+2.
//0 または - または + は指定しないときと同じ。
//0 becomes false. - = false-false. + = false+false.
-relative=true|false
~相対パス的表示。
relative だけでも relative=true の意味になる。
ファイル中の PLUGIN_LS2_1_RELATIVE で初期値を設定できます。デフォルトでは FALSE です。
-hierarchy=true|false
~階層的リスト表示。
hierarchy だけでも hierarchy=true の意味になる。
ファイル中の PLUGIN_LS2_1_HIERARCHY で初期値を設定できます。デフォルトでは FALSE です。
-non_list=true|false
~pukiwiki.ini.php で定義される $non_list によるリスト排除。
non_list だけでも non_list=true の意味になる。
ファイル中の PLUGIN_LS2_1_NON_LIST で初期値を設定できます。デフォルトでは TRUE です。
現在デフォルトで TRUE です。
-number=-?\d+ ((-?\d+ は正規表現による表記です。))
~リンク表示件数指定。Blog2プラグインを使用するときに便利らしいです。
number=10 で頭から10件表示します。number=-10 のように - をつけると後ろの10件になります。
それでも逆順にはならないので reverse を使用してください。
-title_number=\d+
~titleの表示件数指定。title_number=10で頭から10件表示します。
現在 - 機能はありません。title_number=1 でページの先頭見出しを表示することになります。
先頭見出しを必ず書く人は多いそうなのでそれを表示するのに便利かもしれません。
-except=正規表現
~リストしないページを正規表現にて指定。$non_list だけでは足りないときに使用。
relative の場合でもページ名全体で判定。
ヒント： non_list に合わせて [[preg_match>http://php.s3.to/man/function.preg-match.html]] での判定を行うので / に注意。
except=Test|sample → Test または sample を含むページを除く。
-new=true|false
~更新日時順（新しいほど上)に表示。
new だけでも new=true の意味になる。
Note: hierarchy,relative コンビとの併用はきっと納得のいかない表示になります
(hierarchy,relative はページ名の昇順ソート時用のオプションなので)。
-date=true|false
~ページの更新日時も表示。
date だけでも date=true の意味になる。
-filter=正規表現
~ページパターンをさらに正規表現で限定する。
パターンを / (全ての意味) にしてこちらだけを使うのもあり。
下位互換性と容易性のために「パターン」を正規表現にはできませんでした。
ヒント: マッチングには [[ereg>http://php.s3.to/man/function.ereg.html]] を使用します。
*/

//見出しアンカーの書式
define('PLUGIN_LS2_1_ANCHOR_PREFIX', '#content_1_');

//見出しアンカーの開始番号
define('PLUGIN_LS2_1_ANCHOR_ORIGIN',0);

//見出しレベルを調整する(デフォルト TRUE)
define('PLUGIN_LS2_1_LIST_COMPACT',TRUE);
define('PLUGIN_LS2_1_LIST_TITLE_COMPACT',TRUE);

//$non_list によるページ排除を使用する(デフォルト TRUE)
define('PLUGIN_LS2_1_NON_LIST',TRUE);

//相対パス的表示(デフォルト FALSE)
define('PLUGIN_LS2_1_RELATIVE',FALSE);

//階層的リスト表示(デフォルト FALSE)
define('PLUGIN_LS2_1_HIERARCHY',FALSE);

function plugin_ls2_1_action()
{
	global $vars, $_ls2_msg_title;

	$params = array();
	foreach (array(
		'title', 'include', 'reverse', 'compact', 'title_compact', 'depth',
		'relative', 'hierarchy', 'non_list', 'number', 'title_number', 'except',
		'date', 'new', 'filter') as $key)
	{
		$params[$key] = $vars[$key];
	}
	$prefix = isset($vars['prefix']) ? $vars['prefix'] : '';
	$body = plugin_ls2_1_show_lists($prefix, $params);

	return array('body'=>$body,
		'msg'=>str_replace('$1', htmlspecialchars($prefix), $_ls2_msg_title));
}

function plugin_ls2_1_convert()
{
	global $script, $vars, $_ls2_msg_title;

	// true or false のパラメーター
	$params = array(
		'title'   => FALSE,
		'include' => FALSE,
		'reverse' => FALSE,
		'compact' => PLUGIN_LS2_1_LIST_COMPACT,
		'title_compact' => PLUGIN_LS2_1_LIST_TITLE_COMPACT,
		'relative' => FALSE,
		'hierarchy' => FALSE,
		'non_list' => PLUGIN_LS2_1_NON_LIST,
		'date' => FALSE,
		'new' => FALSE,
	);
	// その他の引数を持つパラメーター
	$argparams = array(
		'link'    => FALSE,
		'depth'   => FALSE,
		'number' => FALSE,
		'title_number' => FALSE,
		'except' => FALSE,
		'filter' => FALSE,
	);
	$args = array();
	$prefix = '';
	if (func_num_args()) {
		$args   = func_get_args();
		$prefix = array_shift($args);
	}

	if ($prefix == '')
	{
		$prefix = strip_bracket($vars['page']) . '/';
	}
	else if($prefix === '/') {
		$prefix = '';
	}
	else if($prefix === '//') {
		$prefix = strip_bracket($vars['page']);
	}
		
	array_walk($args, 'plugin_ls2_1_check_params', & $params);
	array_walk($args, 'plugin_ls2_1_check_argparams', & $argparams);
	$params = array_merge($params, $argparams);

	if (! $params['link'])
		return plugin_ls2_1_show_lists($prefix, $params);

	if ( $params['link'] === TRUE )
	{
		$title = str_replace('$1', htmlspecialchars($prefix), $_ls2_msg_title);
	}
	else
	{
		$title = $params['link'];
	}

	$tmp = array();
	$tmp[] = 'plugin=ls2_1&amp;prefix=' . rawurlencode($prefix);
	foreach ($params as $key => $val)
	{
		if(strpos($key,'_')===0 or $key === 'link')
		{
			continue;
		}
		if($val)
		{
			$tmp[] = "$key=$val";
		}
	}

	return '<p><a href="' . $script . '?' . join('&amp;', $tmp) . '">' .
		$title . '</a></p>' . "\n";
}

function plugin_ls2_1_show_lists($prefix, & $params)
{
	global $_ls2_err_nopages, $non_list;

	$pages = array();

	//depth オプション解析
	if($params['depth'])
	{
		$prefixdepth = substr_count($prefix,"/") -1 ; //パターン文字列の階層数。$depthflag 判断に使用。
		list($lowdepth,$highdepth,$justdepth) = plugin_ls2_1_depth_option_analysis($params['depth']);
	}
	//number オプション解析
	if($params['number'])
	{
		list($headnumber,$tailnumber) = plugin_ls2_1_number_option_analysis($params['number']);
	}

	$pagestmp = array();
	foreach (get_existpages() as $_page)
	{
		//depth 制限
		if($params['depth'])
		{
			$flag = TRUE;
			$_pagedepth  = substr_count($_page,"/");
			if($lowdepth)
			{
				$flag &= $_pagedepth >= $prefixdepth + $lowdepth;
			}
			if($highdepth)
			{
				$flag &= $_pagedepth <= $prefixdepth + $highdepth;
			}
			elseif($justdepth>=1)
			{
				$flag &= $_pagedepth == $prefixdepth + $justdepth;
			}
			if (! $flag) continue;
		}

		//non_list 制限
		if( $params['non_list'] )
		{
			$flag = !preg_match("/$non_list/",$_page);
			if (! $flag) continue;
		}

		//except 制限
		if( $params['except'] )
		{
			$except = $params['except'];
			$flag = !preg_match("/$except/",$_page);
			if (! $flag) continue;
		}

		//パターン制限
		if( $prefix !== '' )
		{
			$flag = (strpos($_page,$prefix) === 0);
			if (! $flag) continue;
		}

		//filter 制限
		if( $params['filter'] )
		{
			$filter = $params['filter'];
			$flag = ereg($filter,$_page);
			if (! $flag) continue;
		}

		// すべて TRUE なら追加。
		$pagestmp[] = $_page;
	}

	// new オプション。更新日時順によるソート。plugin_ls2_1_timecmp は function
	if ( $params['new'] )
	{
		usort($pagestmp,"plugin_ls2_1_timecmp");
	}
	// 通常はページ名でソート。
	else
	{
		natcasesort($pagestmp);
	}

	//表示 number 制限
	if($params['number'])
	{
		$asize = sizeof($pagestmp);
		if($headnumber>0)
		{
			$start=0;
			$end= ( $headnumber < $asize ) ? $headnumber : $asize;
		}
		elseif($tailnumber>0)
		{
			$end=$asize;
			$start=( $end-$tailnumber > 0 ) ? $end-$tailnumber : 0;
		}
		for($i = $start; $i < $end ; $i++)
		{
			$pages[] = $pagestmp[$i];
		}
	}
	else
	{
		$pages = $pagestmp;
	}

	if ($params['reverse']) $pages = array_reverse($pages);

	foreach ($pages as $page) $params["page_$page"] = 0;

	if (empty($pages)) {
		return str_replace('$1', htmlspecialchars($prefix), $_ls2_err_nopages);
	}

	$params['result'] = array();
	$params['saved'] = array();

	//hierarchy オプション。リストレベル制御

	//階層指定をしている場合は、リスト表示上でのトップがずれる。
	$top_level = 1;
	if($params['depth'])
	{
		$top_level = ( $lowdepth > $justdepth ) ? $lowdepth : $justdepth;
	}
	if( $top_level < 1)
	{
		$top_level = 1;
	}

	//要修正: number=- と併用された場合は、$pages をすべて parse して一番階層が少ないページを見つけ、それを基準にする
	if($params['hierarchy'])
	{
		if($params['compact'])
		{
			//パターンの最後の / 以下を取り除く。例) sample/test/d -> sample/test
			//$prefix_dir = preg_replace('/[^\/]+$/','',$prefix);
			if( ( $pos = strrpos($prefix,'/') ) !== false )
			{
				$prefix_dir = substr($prefix,0,$pos+1);
			}
		}
		else
		{
			$prefixdepth = substr_count($prefix,"/");
		}

		foreach ($pages as $page)
		{
			if($params['compact'])
			{
				//パターンをとりのぞく
				$tmp = substr($page,strlen($prefix_dir));
				$level=1;

				//depth オプションが指定されていた場合 $top_level が変わります。
				while(substr_count($tmp,"/") > $top_level -1 )
				{
					//一階層ずつとりのぞく
					//$tmp = preg_replace('/\/[^\/]*$/','',$tmp);
					if( ( $pos = strrpos($tmp,'/') ) !== false )
					{
						$tmp = substr($tmp,0,$pos);
					}
					//compact なので上位のページが存在していればリスト階層レベルが増える
					if (in_array($prefix_dir . $tmp, $pages))
					{
						$level++;
					}
				}
			}
			// compact でない場合は上位のページの有無は問わないので / の数で十分。
			else
			{
				$level = substr_count($page,"/") - $prefixdepth + 1;
				//depth オプションが指定されていた場合 $top_level が変わります。
				if($params['depth'])
				{
					$level = $level - ( $top_level -1 );
				}
			}
			plugin_ls2_1_get_headings($page,$params,$level,FALSE,$prefix,$top_level,&$pages);
		}
	}
	else{
		foreach ($pages as $page)
		{
			plugin_ls2_1_get_headings($page,$params,1,FALSE,$prefix,1,&$pages);
		}
	}
	return join("\n",$params['result']).join("\n",$params['saved']);
}

function plugin_ls2_1_get_headings($page,& $params,$level = 1,$include = FALSE, $prefix, $top_level = 1, &$pages)
{
	global $script;
	static $_ls2_anchor = 0;

	// ページが未表示のとき
	$is_done = (isset($params["page_$page"]) && $params["page_$page"] > 0);
	if (! $is_done) $params["page_$page"] = ++$_ls2_anchor;

	$r_page = rawurlencode($page);
	$s_page = htmlspecialchars($page);
	$title = $s_page.' '.get_pg_passage($page,FALSE);
	$href = $script.'?cmd=read&amp;page='.$r_page;

	// relative オプション。リンク名制御。
	if($params['relative'])
	{
		//パターンの最後の / 以下を取り除く。例) sample/test/d -> sample/test
		//$prefix_dir = preg_replace('/[^\/]+$/','',$prefix);
		if( ( $pos = strrpos($prefix,'/') ) !== false )
		{
			$prefix_dir = substr($prefix,0,$pos+1);
		}

		//ページ名からそのパターンをとり除く。
		//$s_page = ereg_replace("^$prefix_dir",'',$s_page);
		$s_page = substr($s_page,strlen($prefix_dir));

		// relative オプションと hierarchy オプションが同時に指定された場合は
		// パターンを取り除くだけでなく、上位の存在しているページ名も取り除く。
		if($params['hierarchy'])
		{
			$tmp = $s_page;

			//depth オプションが指定されていた場合 $top_level が変わります。
			while(substr_count($tmp,"/") > $top_level -1 )
			{
				//一階層ずつとりのぞく
				if( ( $pos = strrpos($tmp,'/') ) !== false )
				{
					$tmp = substr($tmp,0,$pos);
				}

				//上位のページが存在していれば、その文字列を取り除き、相対名にする。
				if(in_array($prefix_dir . $tmp, $pages))
				{
					//$s_page = ereg_replace("^$tmp/",'',$s_page);
					$s_page=substr($s_page,strlen("$tmp/"));
					break;
				}
			}
		}
	}

	//date オプション。更新日時の追加。
	$date='';
	if( $params['date'] )
	{
		if ( is_file(PLUGIN_DIR.'new.inc.php') )
		{
			require_once(PLUGIN_DIR.'new.inc.php');
			plugin_new_init();
			$date = plugin_new_inline(format_date(get_filetime($page)));
		}
		else
		{
			$date = format_date(get_filetime($page));
		}
	}

	plugin_ls2_1_list_push($params,$level);
	$ret = $include ? '<li>include ' : '<li>';
	if ($is_done && ($params['title'] || $params['include']))
	{
		$ret .= '<a href="' . $href . '" title="' . $title . '">' . $s_page . '</a> ';
		$ret .= '<a href="#list_' . $params["page_$page"] . '"><sup>&uarr;</sup></a>';
		array_push($params['result'], $ret);
		return;
	}

	$ret .= '<a id="list_' . $params["page_$page"] . '" href="' . $href .
		'" title="' . $title . '">' . $s_page . '</a> ' . $date;
	array_push($params['result'], $ret);

	$anchor = PLUGIN_LS2_ANCHOR_ORIGIN;
	$matches = array();
	if ($params['title'] or $params['include'])
	{
		//title_number オプション解析
		if($params['title_number'])
		{
			list($title_number,$garbage) = plugin_ls2_1_number_option_analysis($params['title_number']);
		}
		$titlenum=0;
		foreach (get_source($page) as $line) {
			if ($params['title'] && preg_match('/^(\*{1,3})/', $line, $matches)) {
				if( $params['title_number'] )
				{
					/* ただの件数制限なので途中で抜けても $anchor には不整合がでずにすむはず */
					if( $titlenum >= $title_number )
					{
						if(! $params['include'])
						{
							break;
						}
						else
						{
							continue;
						}
					}
					$titlenum++;
				}

				$id    = make_heading($line);
				$hlevel = strlen($matches[1]);
				$id    = PLUGIN_LS2_1_ANCHOR_PREFIX . $anchor++;
				plugin_ls2_1_list_push($params, $level + $hlevel);
				array_push($params['result'],
					'<li><a href="' . $href . $id . '">' . $line . '</a>');
			} else if ($params['include'] &&
				preg_match('/^#include\((.+)\)/', $line, $matches) &&
				is_page($matches[1]))
			{
				plugin_ls2_1_get_headings($matches[1],$params,$level + $hlevel + 1,TRUE, $prefix, $top_level);
			}
		}
	}
}

//リスト構造を構築する
function plugin_ls2_1_list_push(& $params, $level)
{
	global $_ul_left_margin, $_ul_margin, $_list_pad_str;

	$result = & $params['result'];
	$saved  = & $params['saved'];
	$cont   = TRUE;
	$open   = '<ul%s>';
	$close  = '</li></ul>';

	while (count($saved) > $level || (! empty($saved) && $saved[0] != $close))
		array_push($result, array_shift($saved));

	$margin = $level - count($saved);

	// count($saved)を増やす
	while (count($saved) < ($level - 1)) array_unshift($saved, '');

	if (count($saved) < $level)
	{
		$cont = FALSE;
		array_unshift($saved, $close);

		$left = ($level == $margin) ? $_ul_left_margin : 0;

		if ($params['title_compact'])
		{
			$left  += $_ul_margin;   // マージンを固定
			$level -= ($margin - 1); // レベルを修正
		} 
		else
		{
			$left += $margin * $_ul_margin;
		}
		$str = sprintf($_list_pad_str, $level, $left, $left);
		array_push($result, sprintf($open, $str));
	}

	if ($cont) array_push($result, '</li>');
}

//オプション \d?[+-]?\d? を解析。
function plugin_ls2_1_depth_option_analysis($arg)
{
	$low=0;
	$high=0;
	$just='';
	if( !preg_match('/^\d*\-?\d*$/',$arg) or $arg == '' )
	{
		return;
	}

	if(substr_count($arg,"-")) // \d-\d の場合
	{
		list($low,$high)=split("-",$arg,2);
	}
	elseif(substr_count($arg,"+")) // \d+\d の場合
	{
		list($low,$high)=split("+",$arg,2);
		$high += $low;
	}
	else // \d だけの場合
	{
		$just = $arg;
	}
	return array($low,$high,$just);
}

//オプション -?\d+ を解析。
function plugin_ls2_1_number_option_analysis($arg)
{
	$head=0;
	$tail=0;
	if( !preg_match('/^-?\d+$/',$arg) or $arg == '' )
	{
		return;
	}
	if(substr_count($arg,"-")) // - がある場合
	{
		$tail=substr($arg,1);
	}
	else
	{
		$head=$arg;
	}
	return array($head,$tail);
}

// 日付ソート関数
function plugin_ls2_1_timecmp($a, $b) {
	$atime =  filemtime(get_filename($a));
	$btime =  filemtime(get_filename($b));

	if($atime == $btime) {
		return 0;
	}
	return ($atime < $btime) ? 1 : -1;
}

// true or false の値を持つオプションを解析する
function plugin_ls2_1_check_params($value, $key, & $params)
{
	// $value に depth=2-3 や relative のような値が入る。実質 $key は意味なし。
	// trim はあえてしていない。
	if ($value == '') return;

	list($key,$val)=split("=", $value);
	if( isset($params[$key]) )
	{
		if ( $val == '' || $val == "true" )
		{
			$params[$key] = TRUE;
		}
		elseif ( $val = "false" )
		{
			$params[$key] = FALSE;
		}
	}
}
// その他の値を持つオプションを解析する
function plugin_ls2_1_check_argparams($value, $key, & $params)
{
	// $value に depth=2-3 や relative のような値が入る。実質 $key は意味なし。
	// trim はあえてしていない。
	if ($value == '') return;

	list($key,$val)=split("=", $value);
	if( isset($params[$key]) )
	{
		if ( $key == 'link' )
		{
			if ( $val == '' )
			{
				$params['link'] = TRUE;
			}
			else
			{
				$val = htmlspecialchars($val);
				$params['link'] = $val;
			}
		}
		elseif ( $val != '' )
		{
			//$val = htmlspecialchars($val);
			$params[$key] = $val;
		}
	}
}
?>
