<?php // -*- mode: php -*- Time-stamp: <2009-12-14(Mon) 14:30:25 kahata>
/**
 * description: Wikiページの埋め込みタグのフック
 * 1. The examples work only on PHP5 and therefore not php4.
 *
 * history:
 * - ver 0.007:
 * <pre>
 * 拡張機能、拡張関数組み込み
 * 1. 仕様1   <{#拡張関数:引数1[|引数2|...]...}>
 *    wikiテキストを引数としてwikiテキストを返す(convert_htmlを通して表示)
 * 2. 仕様2   <[#拡張関数:引数1[|引数2|...]...]>
 *    htmlを引数としてhtmlを返す(convert_htmlを通さず、htmlを表示)
 * 3. 仕様3   <{テンプレートページ[|パラメータ1|パラメータ2..]..}>
 *    テンプレートページをinclude (plugin include_template.inc.phpの組み込みが必要)
 * </pre>
 * - ver 0.006:
 * <pre>
 * 1. convert()関数の修正 -- 同じページに＜tag /＞と
 *   ＜tag＞～＜/tag＞が混在する場合の不具合修正
 * 2. convert()関数の改良 -- ＜tag＞～＜/tag＞内部の～($input)のtrim化(前後の改行空白削除)
 * 3. retrieve()関数の改良 -- 置換順序を「先入れ後出し」方式に変更
 * 4. tagの追加・改良(必要に応じて $tagHooks に追加してお使いください) 
 *  a.＜comment＞コメント(行)＜/comment＞ -- コメント(行)をコメントアウト(複数行対応)
 *  b.＜wiki＞テキスト＜/wiki＞ -- テキストをconvert_htmlして出力 
 *    (＜wiki style="foo"＞ ＜wiki class="bar"＞が使用可能)
 *  c.＜span＞インライン＜/span＞ -- インライン出力 (ブロック型の＜div＞～＜/div＞は
 *    ＜html＞～＜/html＞の内部に配置すること) 
 *  d.＜plugin＞～＜/plugin＞ -- インライン型追加(type="inline"を指定)、
 * 複数行引数対応(タグ内部に複数行引数を配置) 
 * </pre>
 * - ver 0.0005:
 * <pre>
 * 1. 作動を #tag_Hook埋め込みページに限定するオーバーライド追加
 * 2. 伴って class _tag_Hook のconvert(),retrieve()をpublicに変更
 * 3. 余分なコードの整理
 * </pre>
 *
 * @package   tag_Hook
   @category  org.pukiwiki
 * @access    public
 * @author    kahata
 * @since     Time-stamp: <2009-08-28 03:05:07 kahata>
 * @version   0.007
 * @copyright kahata
 * @license   http://www.debian.org/misc/bsd.license  BSD License (3 Clause)
 * @link      http://ja.wikipedia.org/wiki/Help:%E6%9D%A1%E4%BB%B6%E6%96%87 WikiPedia:条件文
 * @link      http://www.php.net/manual/ja/function.preg-match-all.php
 * @link      http://www.mtblue.org/pc/web/regular_expression.php
 * @uses      Adds functionalities to PukiWiki.
   @see       
 * @filesource
 */

/**
 * 数値の四則演算構文解析 ParserFunction.class
 */
require_once('ParserFunctions.class.php');
/**
 * 構文解析にphpのeval()を使用するか? default:FALSE(使用しない)
 */
define('PKWKEXP_EXTENSION_EXPR_USE_EVAL', FALSE);
/**
 * 拡張関数にphp組み込み関数,pukiwiki組み込み関数を使用するか? default:FALSE(使用しない)
 * 
 * 注意:TRUEにすると危険が伴います。
 */
define('PKWKEXP_EXTENSION_USE_PREDEFINED_FUNCUTION', FALSE);
/**
 * 拡張機能にプラグイン#include_templateを使用するか? default:FALSE(使用しない)
 *
 * 注意:{@link http://pukiwiki.sourceforge.jp/?%E8%87%AA%E4%BD%9C%E3%83%97%E3%83%A9%E3%82%B0%E3%82%A4%E3%83%B3%2Finclude_template.inc.php include_template.inc.php}組み込みが必要。
 */
define('PKWKEXP_EXTENSION_USE_PLUGIN_INCLUDE_TEMPLETE', FALSE);

// 
/**
 * 関数セパレーター
 */
define('PKWKEXP_EXTENSION_FUNCTION_SEPARATOR', ':');
/**
 * 引数セパレーター
 */
define('PKWKEXP_EXTENSION_ARGUMENT_SEPARATOR', '|');

/**
 * extensionFunctions.class
 *
 * 拡張関数の実装
   @category   
 * @package  tag_Hook
 * @access     public
 */
class extensionFunctions
{
	static $function_type=0;

	/**
	 * テスト用サンプル関数 引数を加工
	 *
	 * {@source }
	   @global array used for stuff
	   @staticvar array $bar_val
	 * @param array $args 可変引数
	 * @return string wikiテキストまたはhtmlテキストを返す
	 * @access public
 	 */
	public function do_test(){
		$args = func_get_args();
		if(self::$function_type){
			return '<font color=green>&lt;[ ]&gt;</font>書式--'.$args[0];
		}
		else {
			return  '&color(green){&lt;{ }&gt;};書式--'.$args[0];
		}
	}

	/**
	 * このextensionFunctionsクラスに登録されている拡張関数の一覧を表示する
	 *
	 * {@source }
	   @global array used for stuff
	   @staticvar array $bar_val
	 * @param array $args 可変引数
	 * @return string 登録済み拡張関数の一覧
	 * @access public
 	 */
	public function do_help(){
		$regfunc = array_diff(get_class_methods(__CLASS__),array('ExtensionFunction'));
		$output = 'registered functions: ';
		$output .= join(' , ',str_replace('do_','#',$regfunc));
		return $output;
	}

	/**
	 * 第1引数の評価が真なら第2引数をさもなくば第3引数を返す
	 *
	 * {@source }
	   @global array used for stuff
	   @staticvar array $bar_val
	 * @param array $args 可変引数
	 * @return string 第1引数の評価結果
	 * @access public
 	 */
	public function do_if(){
    	$use_eval = PKWKEXP_EXTENSION_EXPR_USE_EVAL;

		$argc = func_num_args();
		$args = func_get_args();
    	$expr = $args[0]; $do_true = $args[1]; $do_false = $args[2];

		if($use_eval) {
			$result = eval("\$cond = " . "\(" . $args[0] . "\);");
		}
		else {
			if (class_exists('ParserFunctions')) {$p = new ParserFunctions();}
    		else {return 'ParserFunctions() not available.';}
       		$cond = $p->parse($args[0]);
		}

    	if($cond) {
			$output = $do_true;
    	} else {
			$output= $do_false;
    	}
    	return trim($output);
	}

	/**
	 * 第1引数のラベルに相当する値を返す
	 *
	 * {@source }
	   @global array used for stuff
	   @staticvar array $bar_val
	 * @param array $args 可変引数
	 * @return string 第1引数の評価結果
	 * @access public
 	 */
	public function do_switch(){
		$args = func_get_args();
    	$expr = trim(array_shift($args));
    	foreach($args as $value ){
			$tmp = explode('=',$value);
			$output[trim($tmp[0])] = trim($tmp[1]);
		}
		if(array_key_exists($expr,$output)) return $output[$expr];
        elseif(array_key_exists('default',$output)) return 'default';
        else return '';
	}

	/**
	 * 第1引数の演算結果を返す
	 *
	 * {@source }
	   @global array used for stuff
	   @staticvar array $bar_val
	 * @param array $args 可変引数
	 * @return string 演算結果
	 * @access public
 	 */
	public function do_expr(){
    	$use_eval = PKWKEXP_EXTENSION_EXPR_USE_EVAL;
		if (func_num_args() < 1) 
			return 'Usage: &lt;{#expr:numerical expression}&gt;' . "\n";
    	$args = func_get_args();
    	$expr = array_shift($args);

		if($use_eval) {$flag = eval("\$result = " . "\(" . $expr . "\);");}
		else {
			if (class_exists('ParserFunctions')) {$p = new ParserFunctions();}
    		else {return 'ParserFunctions() not available.';}
       		$result = $p->parse($expr);
		}
    	return trim($result);
	}

	/**
	 * 拡張機能の関数を前処理し実行を振り分ける
	 *
	 * {@source }
	   @global array   $vars  グローバル変数配列
	   @global string  $page  ページ名
	   @global string  $script scriptのURL
	   @staticvar array $bar_val
	 * @param  string  $element 拡張機能を保持する要素
	 * @param  string  $input   拡張機能を保持する要素の内部表現
	 * @param  integer $type    拡張機能のタグ型 <{..}>:0 <[..]>:1
	 * @return string  拡張機能実行結果
	 * @access public
 	 */
	public function ExtensionFunction($element,$input,$type=0){
  		global $vars,$page,$script;

		self::$function_type = $type;
  		$input = str_replace("\n",'',trim($input));
		$count = preg_match("'^#(.*?)$'i",$input, $match);

		// 拡張関数の処理
		if($count){
			$args = explode(PKWKEXP_EXTENSION_FUNCTION_SEPARATOR,$match[1],2);
			$func = 'do_'.$args[0];
			$argv = explode(PKWKEXP_EXTENSION_ARGUMENT_SEPARATOR,$args[1]);

			// php組み込み関数、pukiwiki実装関数の処理
			if(PKWKEXP_EXTENSION_USE_PREDEFINED_FUNCUTION && function_exists($args[0])){
				$output = call_user_func_array($args[0],$argv);
				if($output == FALSE){
					if(self::$function_type){
				 		return 
			 		"<a href=http://www.php.net/manual-lookup.php?pattern=$args[0]&lang=ja>$args[0]</a>"
			 		. ' <font color=red>syntax error! </font>';
					}
					else {
						return 
			 		"[[$args[0]:http://www.php.net/manual-lookup.php?pattern=$args[0]&lang=ja]]"
			 		. ' &color(red){syntax error! };';
					}
				}
				else return $output;
			}
			// extensionFunctionsクラスに実装された拡張関数の処理
			if(method_exists(__CLASS__,$func)){
				return call_user_func_array(array(__CLASS__, $func),$argv);
			}
			else return htmlspecialchars($element);
		}
		// <{ページ名|引数1[|引数2.....]}>書式でのページインクルード
		// プラグイン #include_templateの組み込みが必要
		elseif(PKWKEXP_EXTENSION_USE_PLUGIN_INCLUDE_TEMPLETE) {
			if(self::$function_type){return ' '. htmlspecialchars($element);}
			else {
				$params = explode('|',$input);
				$plugin_include_template = PLUGIN_DIR.'include_template.inc.php';
				if( !file_exists($plugin_include_template)) {return 'no plugin: include_template';}
 				$tmpstr ='';$option='';
				if (!is_page($params[0])) {return 'no such page:'.$params[0];}
				else { $page = $params[0];}
				for($i=1; $i<count($params); $i++) {
					if ($params[$i] == 'title' || $params[$i] == 'notitle') $option = $params[$i];
					else {$tmpstr .= $params[$i]."\n";}
				}
				if($option) {$output = "\n#iclude_template(".$page.','.$option."){{\n".$tmpstr."\n}}\n";}
				else {$output = "\n#include_template(".$page."){{\n".$tmpstr."\n}}\n";}
 				return $output;
 			}
 		}
 		else {return htmlspecialchars($element);}
 	}
}

/**
 * tag_Hook extends
 *
 * 拡張クラス tagのcallback 関数の実装
   @category   
 * @package  tag_Hook
 * @access     public
 */
class tag_Hook extends _tag_Hook
{
	/**
	 * htmlタグ
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_html($tag_name,$element,$input,$argv){
//		return '<div>'.$input.'</div>';
		return $input;
	}
	
	/**
	 * comment out (複数行コメント対応)
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_comment($tag_name,$element,$input,$argv){
		return NULL;
	}

	/**
	 * wikiテキストの$inputをhtml変換して表示
	 *
	 * output_wiki wiki text Time-stamp: <09/09/10(木) 06:46:48 hata>
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_wiki($tag_name,$element,$input,$argv){
		$regexp  = "<wiki(.*?)>(.*?)</wiki>";
		$input = explode("\n", $input);
		$body = & new Body(++$contents_id);
		$body->parse($input);
		$output = $body->toString();
   		preg_match('/^<p>(.*)<\/p>/', $output, $match0);
		if($match0[1]) $output = $match0[1];
		preg_match("'".$regexp."'si",$element,$match);
		$output = '<div'.$match[1].'>'.$output.'</div><div class="clear"></div>';
		return $output;
	}

	/**
	 * spanタグ
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_span($tag_name,$element,$input,$argv){
		return $element;
	}

	/**
	 * output_php PHPコード
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_php($tag_name,$element,$input,$argv){
		$retstr = str_replace(array('<?php','?>'),array('',''), trim($input));
    	ob_start();
		eval($retstr);
    	$output = ob_get_contents();
    	ob_end_clean();
		return '<div>'.$output.'</div>';
	}

	/**
	 * output_plugin
	 *
	 * ブロック型,インライン型プラグインの埋め込み
	 * {@source }
	 *
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function output_plugin($tag_name,$element,$input,$argv){
		$plugin_file = PLUGIN_DIR.$argv['name'].'.inc.php';
		if( file_exists($plugin_file)) include_once($plugin_file);
		else return 'no such plugin '.$argv['name']. '! <br />';

		// インライン型プラグイン
		if($argv['type'] == 'inline') {
			$input = explode("\n", $input);
			$body = & new Body(++$contents_id);
			$body->parse($input);
			$input = $body->toString();
    		preg_match('/^<p>(.*)<\/p>/', $input, $match);
			if($match[1]) $input = $match[1];
			$output = do_plugin_inline($argv['name'],$argv['option'],$input);
		}
		else {
			// 複数行引数の対応 Time-stamp: <09/09/15(火) 21:25:22 hata>
			if(!PKWKEXP_DISABLE_MULTILINE_PLUGIN_HACK && $input){
				$input = "\r".str_replace("\n","\r", $input)."\r";
				if(!$argv['option']) $argv['option']= $input;
				else $argv['option'] = $argv['option'].','.$input;
			}
			$output = do_plugin_convert($argv['name'],$argv['option']);
		}
		return $output;
	}

	/**
	 * #tag_Hook埋め込みページに限定するオーバーライド ver. 0.005
	 *
	 * {@source }
	 * @param string $tag_name タグ名
	 * @param string $element  タグを含む要素全体
	 * @param string $input    タグ内部の内容(innerHTML)
	 * @param array  $argv     アトリビュート(引数)
	 * @return string 出力(htmlテキスト)
	 * @access public
 	 */
	public function toString($body){
		$count = 0;
		$body = preg_replace('/^(?:#tag_Hook(?!\w)\s*)+/im','',$body,$count);
//		str_ireplace('#tag_Hook','', $body, $count);
		if($count) {
			$retstr = $this->convert($body);
    		$retstr = convert_html($retstr);
			$output = $this->retrieve($retstr);
		}
		else {
			$output = convert_html($body);
		}
		return $output;
	}
}

/**
 * _tag_Hook
 *
 * 基本クラス tag_Hook
   @category   
 * @package  tag_Hook
 * @access     public
 */
class _tag_Hook
{
	private $count_all; // for test
	public $TransTable = array();
	private $delim = '=====';

	/**
	 * タグを仮識別子に置換,callbackに変換保存する
	 *
	 * {@source }
	 * @param string $body wikiテキスト
	 * @return string 仮識別子
	 * @access public
 	 */
	public function convert($body){
		global $tagHooks;

		if(is_array($body)) $output = join('',$body);
		else $output = $body;

		$delim = $this->delim;
		$count_all = 0;

		/**
		 * パーザ拡張関数のフック(1) 前置換(convert_htmlを通すタイプ)
		 */
		$count = preg_match_all("'<\{(.*?)\}>'si",$output,$matches);
		$count_all += $count;
		for ($i = 0; $i<count($matches[0]); $i++){
			$element = $matches[0][$i];
			$input = trim($matches[1][$i]);
			$callback = call_user_func(array("extensionFunctions", "ExtensionFunction"),$element,$input,$type=0);
			$output = str_replace($matches[0][$i],$callback, $output);
  		}

		/**
		 * パーザ拡張関数のフック(2) 後置換(convert_htmlをスキップするタイプ)
		 */
		$count = preg_match_all("'<\[(.*?)\]>'si",$output,$matches);
		$count_all += $count;
		for ($i = 0; $i<count($matches[0]); $i++){
			$key1 = 'ExtensionFunction';
			$identifier = $delim.'ExtensionFunction'.sprintf("%04d",$i).$delim;
			$output = str_replace($matches[0][$i],$identifier, $output);
			$element = $matches[0][$i];
			$input = trim($matches[1][$i]);
			$callback = call_user_func(array("extensionFunctions", "ExtensionFunction"),$element,$input,$type=1);
			$this->TransTable[$identifier] = array(
				'tag_name'=> $key1,          // タグ名
				'callback' => $callback,    // コールバック戻り値
			);
  		}

		foreach($tagHooks as $key => $value ){
			/**
		 	* tag_hook(1) blank tagの前置換処理
		 	*/
			$regexp  = "<".$key."(.*?)/>";
			$count = preg_match_all("'<".$key."(.*?)/>'i",$output,$matches);
			$count_all += $count;
			for ($i = 0; $i<count($matches[0]); $i++){
				$identifier = $delim.$key.'_blank'.sprintf("%04d",$i).$delim;
				$output = str_replace($matches[0][$i],$identifier, $output);
				$element = $matches[0][$i];
				$input = '';
				$argv = $this->get_argv($element, $key);
				$callback = call_user_func(array("tag_Hook", $value),
					$key,$element,$input,$argv);
					$this->TransTable[$identifier] = array(
					'tag_name'=> $key,          // タグ名
					'callback' => $callback,    // コールバック戻り値
				);
   			}

			/**
			 * tag_hook(2) tagの前置換処理
		 	*/
			$count = preg_match_all("'<".$key."(.*?)>(.*?)</".$key.">'si",$output,$matches);
			$count_all += $count;
			for ($i = 0; $i<count($matches[0]); $i++){
				$identifier = $delim.$key.sprintf("%04d",$i).$delim;
				$output = str_replace($matches[0][$i],$identifier, $output);
				$element = $matches[0][$i];
				$input = trim($matches[2][$i]);
				$argv = $this->get_argv($element, $key);
				$callback = call_user_func(array("tag_Hook", $value),
				$key,$element,$input,$argv);
				$this->TransTable[$identifier] = array(
					'tag_name'=> $key,          // タグ名
					'callback' => $callback,    // コールバック戻り値
				);
   			}
		}
    	$this->count_all = $count_all;
		return $output;
	}

	/**
	 * 仮識別子をcallbackに書き戻す
	 *
	 * {@source }
	 * @param string $text wikiテキスト
	 * @return string callbackに変換されたhtmlテキスト
	 * @access public
 	 */
	public function retrieve($text){
		global $tagHooks;
    	$srch  = array_reverse(array_keys($this->TransTable));
		$rep=array();
		foreach($this->TransTable as $key => $value){
			$rep[]  = $value['callback'];
		}
		$rep = array_reverse($rep);
		$output = str_replace($srch,$rep,$text);
		return trim($output);
	}

	/**
	 * dummy
	 *
	 * {@source }
	 * @param string $body wikiテキスト
	 * @return string $body wikiテキスト
	 * @access public
 	 */
	public function do_nothing($body){
		return $body;
	}

	/**
	 * tagのattributeを引数の配列に取得
	 *
	 * {@source }
	 * @param string $tag tag
	 * @param string $tag_name tag名
	 * @return array 引数の連想配列
	 * @access private
 	 */
	private function get_argv($tag, $tag_name){
		$regexp  = "<".$tag_name."(.*?)>(.*?)</".$tag_name.">|<".$tag_name."(.*?)/>";
		$count = preg_match("'".$regexp."'s",$tag,$matches);
		if($count) {
			if( $matches[1]){
    			$str = trim(ereg_replace(" +", " ", $matches[1]));
				$argv = $this->get_values($str,' ','=');
			}
			else if ( $matches[3] ) {
    			$str = trim(ereg_replace(" +", " ", $matches[3]));
				$argv = $this->get_values($str,' ','=');
			}
			else {
				$argv = array();
			}
		}
		return $argv;
	}

	/**
	 * tagのattributeの分割処理
	 *
	 * {@source }
	 * @param string $params パラメータ
	 * @param string $delim1 第1デリミタ
	 * @param string $delim2 第2デリミタ
	 * @return array 引数の連想配列
	 * @access private
 	 */
	private function get_values($params, $delim1 = " ", $delim2 = '='){
    	$data   = explode($delim1, $params);
    	for($i=0;$i<count($data);$i++){
	  		$temp = explode($delim2, $data[$i] , 2);
      		$key = trim($temp[0]);

     		$v = trim($temp[1]);
			$v = trim($v,"\"\'");
      		$value[$key] = $v;
    	}
    	return $value;
	}


	/**
	 * tag_hookの全体処理
	 *
	 * {@source }
	 * @param string $body wikitext
	 * @return string convert_htmlされたhtmlテキスト
	 * @access public
 	 */
	public function toString($body){
		$retstr = $this->convert($body);
    	$retstr = convert_html($retstr);
		$output = $this->retrieve($retstr);
		return $output;
	}
}
?>
