<?php
// PukiWiki - Yet another WikiWikiWeb clone.
//
// $Id: sonots $

/*
*説明 [#m17ed6bb]
[[contents>PukiWiki/1.4/マニュアル/プラグイン/c#vd4dabcd]] プラグインは実際のところプラグインではなく、本体組み込みの機能です。
拡張がしにくいのでプラグイン化しました。
アイデアの多くは[[自作プラグイン/ls2_1.inc.php]]からきています。ということで名前はcontents2_1 です。

~#contents をおいた位置より上の見出しが表示されても無駄だなと思っていた人が使うかもしれません。

MenuBar に
 #contents2_1(fromhere=false,hierarchy=false)
としておくのも便利かもしれません。hierarchy はお好みで。

contents よりも負荷がかかることは承知しておいてください。
とはいえ ls2 の title オプションとやっていることは同じです。対象が１ページなだけそれよりはましです。

関連:([[org:欲しいプラグイン/170]])([[org:欲しいプラグイン/91]])([[org:続・質問箱/221]])([[org:続・質問箱/220]])([[org:質問箱/287]])([[org:欲しいプラグイン/125]])
([[org:質問箱/59]])([[org:続・質問箱/447]])([[org:続・質問箱/306]])

**既知の問題点 [#l67274d6]

問題１．include オプションを完全にはできないことが発覚しました。アンカー番号は convert_html が呼ばれた回数が振られるのですが、#contents2_1 
が数えているのは結局のところ #include の数にしかすぎません。
つまり、convert_html を呼ぶ include 以外のプラグインが呼ばれるときっとずれます。

問題２．#include でとりこまれたページで表示される #contents2_1 
のアンカーリンクの番号がまた１からはじまりうまくいきません。これは痛いです。

考察１．convert_html 中の static $contents_id を global にしたらどうかと考えてみましたが、だめです。

考察２．plugin_contents2_1_convert を convert_html が呼んだときに引数で $contents_id を渡す
ことができたらどうかと考えてみましたが、そのように convert_html を改造できたとしても、問題２は直せますが問題１は変わりません。~
ちなみに本体組み込み #contents は convert_html が $contents_id を渡しているこのような形です。include オプションがないのでこれ以上は悩む必要がないのです。~
問題２のためにやりたいところですが、はてさて・・・。

*書式 [#jcb5f796]
 #contents2_1([オプション])

 &contents2_1([オプション]);
インライン型プラグイン時は強制的に display=inline となります。

**オプション [#r8a06bfd]
-page=ページ名
~見出しリストを行うページを指定。デフォルトはカレントページ。
-fromhere=true|false
~#contents2_1 がのある次の行以降の見出しのみをリストする。~
ファイル中の PLUGIN_CONTENTS2_1_FROMHEARE で初期値を設定できます。デフォルトは TRUE です。~
Note: 現状では #contents2_1 が２つあると一番上のものだけに反応してしまいます。
page オプションで表示ページと異なるページを指定した場合は発動しません。
-display=hierarchy|flat|inline
~リスト表示形式の指定。hierarchy では見出しのレベルに応じた階層的リスト表示。
flat では見出しのレベルによらず平らに表示。~
ファイル中の PLUGIN_CONTENTS2_1_DISPLAY で初期値を設定できます。デフォルトは hierarchy です。~
Note: inline では横一列に表示。インライン型プラグインとして使用する場合は強制的に display=inline となる。
display=inline 時の区切り文字はファイル中の PLUGIN_CONTENTS2_1_DISPLAY_INLINE_BEFORE などで編集できます。
-compact=true|false
~リストのレベルを調節する。display=hierarchy 用のオプションです。compact だけで compact=true になります。~
ファイル中の PLUGIN_CONTENTS2_1_COMPACT で初期値を設定できます。デフォルトは TRUE です。
-number=\d+ ((\d+ は正規表現による表記です。例えば \d は数字のことです。))
~表示件数の指定。~
Note: include ページ名の表示も１つとカウントします。
-depth=\d*[-+]?\d*((\d*[-+]?\d* は正規表現による表記です。\d は数字のことです。))
~見出しレベル指定。1 なら見出しレベル 1 のみを表示する。
2-3 のような指定も可能 (2,3 の意)。2- のように指定するとレベル 2 以上の見出し。。
2+1 のような指定も可能 (2 とそこから +1 。つまり 2,3 の意)。~
Note: include ページ名がレベル０、見出しはレベル１以降です。
現状は compact を使用していても(デフォルトで TRUE)、depth オプションで指定するレベルは変わりません。
よって見た目と指定すべきレベルが違うかもしれません。一旦 compact=false として調べれば確実です。
-except=正規表現
~リストしない見出しを正規表現にて指定。~
ヒント： [[ereg>http://php.s3.to/man/function.ereg.html]] で判定を行います。
except=Test|sample → Test または sample を含む見出しを除く。
-include=true|false
~#include プラグインで取り込んでいるページとその見出しも扱う。~
ファイル中の PLUGIN_CONTENTS2_1_INCLUDE で初期値を設定できます。デフォルトは TRUE です。~
Note: ページタイトルへのジャンプは #include がアンカーを張ってくれないと無理です。
下の[[もっと便利に - include プラグインの改造>#f0771d8a]] をご覧ください。

*設定

**スタイルシート
(どこだっけ？)を受けて、CSSクラスを contents2_1 と指定してあります。
 ul.contents2_1 {
     display=hierarchy|flat 用
 }
 ul.contents2_1 li {
     display=hierarchy|flat 用
 }
 div.contents2_1 {
     ブロック型プラグイン時の display=inline 用
 }
 span.contents2_1 {
     インライン型プラグイン時の display=inline 用
 }
ただしマージンは default.ini.php 中の $_ul_left_margin, $_ul_margin を編集しましょう。
なのでそこは contents2_1 用とはいかないかと思います。
*/

//見出しアンカーの書式
define('PLUGIN_CONTENTS2_1_ANCHOR_PREFIX', '#content_');

//見出しアンカーの開始番号
define('PLUGIN_CONTENTS2_1_ANCHOR_ORIGIN',0);
define('PLUGIN_CONTENTS2_1_PAGE_ANCHOR_ORIGIN',1);

//#contents2_1 が書いてある次の行以降の見出しのみをリストする(デフォルト TRUE)
define('PLUGIN_CONTENTS2_1_FROMHERE', TRUE);

//リストのレベルを調整する(デフォルト TRUE)
define('PLUGIN_CONTENTS2_1_COMPACT',TRUE);

//リスト表示形式(デフォルト 'hierarchy')
define('PLUGIN_CONTENTS2_1_DISPLAY','hierarhcy');

//#include プラグインで取り込んでいるページの見出しも扱う(デフォルト TRUE)
define('PLUGIN_CONTENTS2_1_INCLUDE',TRUE);

//display=inline 時に前、間、後ろにつける文字
define('PLUGIN_CONTENTS2_1_DISPLAY_INLINE_BEFORE', '[ ');
define('PLUGIN_CONTENTS2_1_DISPLAY_INLINE_MIDDLE', ' | ');
define('PLUGIN_CONTENTS2_1_DISPLAY_INLINE_AFTER',  ' ]');

//CSSクラス設定
define('PLUGIN_CONTENTS2_1_CSS_CLASS','contents2_1');

function plugin_contents2_1_init()
{
	$messages['_contents2_1_msg_err'] = '<div>\'%s\' does not exist.</div>';
	set_plugin_messages($messages);
}

function plugin_contents2_1_inline()
{
	$args = func_get_args();
	array_pop($args);
	return plugin_contents2_1($args, 'inline');
}

function plugin_contents2_1_convert()
{
	return plugin_contents2_1(func_get_args(), 'convert');
}

function plugin_contents2_1($args, $calledby = 'convert')
{
	global $vars;
	global $script;
	global $_contents2_1_msg_err;
	
	// true or false のパラメーター
	$params = array(
		'fromhere'   => PLUGIN_CONTENTS2_1_FROMHERE,
		'compact' => PLUGIN_CONTENTS2_1_COMPACT,
		'include' => PLUGIN_CONTENTS2_1_INCLUDE,
	);
	// その他の引数を持つパラメーター
	$argparams = array(
		'page'    => '',
		'depth'   => '',
		'number' => '',
		'except' => '',
		'display' => PLUGIN_CONTENTS2_1_DISPLAY,
	);

	array_walk($args, 'plugin_contents2_1_check_params', & $params);
	array_walk($args, 'plugin_contents2_1_check_argparams', & $argparams);
	$params = array_merge($params, $argparams);
	
	// inline プラグイン時は強制 display=inline。
	if ($calledby == 'inline')
	{
		$params['display'] = 'inline';
	}

	// ページ名処理
	if ($params['page'] == '')
	{
		$page = $vars['page'];
	}
	else
	{
		$page = $params['page'];
	}
	if (! is_page($page) || ! check_readable($page, false, false ) )
	{
		return sprintf($_contents2_1_msg_err, htmlspecialchars($page));
	}
	// page オプションを利用し、現在表示ページと違うページの見出しリンクを作る場合アンカーだけでは足りない。
	if ( $page != $vars['page'] )
	{
		$r_page = rawurlencode($page);
		$href   = $script . '?cmd=read&amp;page=' . $r_page;
		$params['href'] = $href;
		//表示ページと違うページが指定されていれば強制 FALSE
		$params['fromhere'] = FALSE;
	}
	else
	{
		$params['href'] = '';
	}
	
	//depth オプション解析
	if($params['depth'] != '')
	{
		list($params['lowdepth'],$params['highdepth']) 
			= plugin_contents2_1_depth_option_analysis($params['depth']);
	}
	//number オプション解析
	if($params['number'] != '')
	{
		if(! preg_match('/^\d+$/',$params['number']) )
		{
			$params['number'] = '';
		}
	}

	$params['result'] = $params['saved'] = array();
	$params['page_anchor'] = PLUGIN_CONTENTS2_1_PAGE_ANCHOR_ORIGIN;
	$params['number_counter'] = 0;
	$params['fromhere_detected'] = FALSE;
	plugin_contents2_1_get_headings($page, $params);
	
	if ($params['display'] == 'inline')
	{
		if ($calledby == 'inline')
		{
			$tag = 'span';
		}
		else
		{
			$tag = 'div';
		}
		return "<$tag class=\"" . PLUGIN_CONTENTS2_1_CSS_CLASS . "\">" 
			. join("", $params['result']) . join("", $params['saved']) . "</$tag>";
	}
	else
	{
		return join("\n", $params['result']) . join("\n", $params['saved']);
	}
}

function plugin_contents2_1_get_headings($page, & $params)
{
	static $_contents2_1_anchor = 0;

	// すでにこのページの見出しを表示したかどうかのフラグ
	$is_done = (isset($params["page_$page"]) && $params["page_$page"] > 0);
	if (! $is_done) $params["page_$page"] = ++$_contents2_1_anchor;

	// include ページの場合
	if ($params['page_anchor'] > 1)
	{
		// 表示済み
		if ($is_done) {
			$params['page_anchor']--;
			return;
		}
		
		// 現在は #include プラグインにはアンカーはつかないのでこのアンカーリンクは機能しない。
		// content_2 のようにつくようにちょっと改造すれば機能します。
		$id = PLUGIN_CONTENTS2_1_ANCHOR_PREFIX . $params['page_anchor'];
		
		// include ページ名のレベルは０
		$level = 0; 
		
		$link_string = htmlspecialchars($page);
		$title  = $link_string . ' ' . get_pg_passage($page, FALSE);
		
		plugin_contents2_1_push2result($page, & $params, $level, $id, $title, $link_string);
	}

	$anchor = PLUGIN_CONTENTS2_1_ANCHOR_ORIGIN;
	$matches = array();
	
	foreach (get_source($page) as $line)
	{
		// include ページかつ fromhere が false の場合、それは明らかに違う #contents2_1 なので
		// fromhere 検索すらしない（無論見出しも）。
		// ただ $params['page_anchor'] のために #include を辿って #include の数は数えないといけない。
		if ($params['fromhere'] && ! $params['fromhere_detected'] && $params['page_anchor'] > 1)
		{
			if ($params['include'] &&
				preg_match('/^#include\((.+)\)/', $line, $matches) &&
				is_page($matches[1]))
			{
				$params['page_anchor']++;
				plugin_contents2_1_get_headings($matches[1], $params);
			}
			continue;
		}

		// fromhere 判定。見つかっていないとしても $anchor++ のためにまだここでは continue できないことに注意。↓
		if ($params['fromhere'] && ! $params['fromhere_detected'])
		{
			$params['fromhere_detected'] = preg_match('/^#contents2\_1/', $line, $matches);
			// #contents2_1 とマッチしたなら見出し等とはマッチしないことは明白なので continue;
			if ($params['fromhere_detected']) continue;
		}

		// 見出し検出
		if (preg_match('/^(\*{1,3})/', $line, $matches))
		{
			// アンカー文字列をつくる。$anchor++ が重要。
			$id    = PLUGIN_CONTENTS2_1_ANCHOR_PREFIX . $params['page_anchor'] . '_' . $anchor++;

			// fromhere がまだ見つかっていなければ $anchor++ だけして continue。
			if ($params['fromhere'] && ! $params['fromhere_detected']) continue;
			
			// 見出しレベルは１以降
			$level = strlen($matches[1]);
			
			// $line は 'remove footnotes and HTML tags' される。見出し行のアンカーが返されるがいらない。
			make_heading($line);
			// 自動アンカーがつく設定の場合の [#438239] の前に勝手に挿入される空白が make_heading ではまだ残るようなので。
			$link_string = trim($line);
			$title = $line;
			
			plugin_contents2_1_push2result($line, & $params, $level, $id, $title, $link_string);
			// number 判定。制限を越えていれば抜けて終了。
			if ($params['number'] != '' && $params['number_counter'] >= $params['number'])
			{
				break;
			}
		}
		// include 検出
		elseif ($params['include'] &&
			preg_match('/^#include\((.+)\)/', $line, $matches) &&
			is_page($matches[1]))
		{
			$params['page_anchor']++;
			plugin_contents2_1_get_headings($matches[1], $params);
		}
	}
}

//オプション判定を行い、問題なければリンクを作成し格納していく。
function plugin_contents2_1_push2result($line, & $params, $level, $link_id, $link_title, $link_string)
{
	// number 判定。include ページ名の表示も１つと数える。
	if ($params['number'] != '' && $params['number_counter'] >= $params['number'])
	{
		// do nothing
	}
	// except 判定
	elseif($params['except'] != '' && ereg($params['except'],$line) )
	{
		// do nothing
	}
	// depth 判定。
	elseif($params['lowdepth'] != '' && $level < $params['lowdepth'])
	{
		// do nothing
	}
	elseif ($params['highdepth'] != '' && $level > $params['highdepth'])
	{
		// do nothing
	}
	else
	{
		// display  オプション
		if ($params['display'] == 'inline')
		{
			$litag = '';
		}
		else
		{
			$litag = '<li>';
		}
		
		// include オプション時は include ページ名も表示しなければいけないので、レベル0から+1ずらす。他も
		if ($params['include'])
		{
			$level++;
		}
		// リスト作成
		plugin_contents2_1_list_push($params, $level);
		
		array_push($params['result'], $litag);
		$ret .= '<a id="list_' . $params["page_$page"] . '" href="' . $params['href'] . $link_id .
			'" title="' . $link_title . '">' . $link_string . '</a>';
		array_push($params['result'], $ret);

		$params['number_counter']++;
	}
}

//<ul> と </li></ul> を適宜挿入する。
function plugin_contents2_1_list_push(& $params, $level)
{
	global $_ul_left_margin, $_ul_margin, $_list_pad_str;

	$result = & $params['result']; // バッファ。これを出力することになる。
	$saved  = & $params['saved'];  // 閉じなければいけない文だけ </ul> をたくわえておく。
	if ($params['display'] == 'inline')
	{
		$ulopen = PLUGIN_CONTENTS2_1_DISPLAY_INLINE_BEFORE;
		$ulclose = PLUGIN_CONTENTS2_1_DISPLAY_INLINE_AFTER;
		$liclose = PLUGIN_CONTENTS2_1_DISPLAY_INLINE_MIDDLE;
	}
	else
	{
		$ulopen   = '<ul class="' . PLUGIN_CONTENTS2_1_CSS_CLASS . '"%s>';
		$ulclose  = "</li>\n</ul>";
		$liclose  = '</li>';
	}

	if ($params['display'] == 'flat' || $params['display'] == 'inline')
	{
		// 初期化がここにあるのはうれしくないが、まとめておきたかった。
		if ( count($saved) < 1 )
		{
			if ($params['display'] == 'flat')
			{
				$left = $_ul_margin;
			}
			else if ($params['display'] == 'inline')
			{
				$left = 0;
			}
			$level = 1;
			$str = sprintf($_list_pad_str, $level, $left, $left);
			array_push($result, sprintf($ulopen, $str));
			array_unshift($saved, $ulclose);
		}
		else
		{
			array_push($result, $liclose);
		}
	}
	else
	{
		while (count($saved) > $level || (! empty($saved) && $saved[0] != $ulclose))
			array_push($result, array_shift($saved));
	
		$margin = $level - count($saved);
	
		// count($saved)を増やす
		while (count($saved) < ($level - 1)) array_unshift($saved, '');
	
		if (count($saved) < $level)
		{
			array_unshift($saved, $ulclose);
	
			$left = ($level == $margin) ? $_ul_left_margin : 0;
	
			if ($params['compact'])
			{
				$left  += $_ul_margin;   // マージンを固定
				$level -= ($margin - 1); // レベルを修正
			} 
			else
			{
				$left += $margin * $_ul_margin;
			}
			
			$str = sprintf($_list_pad_str, $level, $left, $left);
			array_push($result, sprintf($ulopen, $str));
		}
		else
		{
			array_push($result, $liclose);
		}
	}
}

//オプション \d?[+-]?\d? を解析。
function plugin_contents2_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 だけの場合
	{
		$low = $high = $arg;
	}
	return array($low,$high);
}

// true or false の値を持つオプションを解析する
function plugin_contents2_1_check_params($value, $key, & $params)
{
	// $value に depth=2-3 や hierarchy のような値が入る。実質 $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_contents2_1_check_argparams($value, $key, & $params)
{
	// $value に depth=2-3 や hierarchy のような値が入る。実質 $key は意味なし。
	// trim はあえてしていない。
	if ($value == '') return;

	list($key,$val)=split("=", $value);
	if( isset($params[$key]) )
	{
		if ( $val != '' )
		{
			//$val = htmlspecialchars($val);
			$params[$key] = $val;
		}
	}
}
?>
