<?php // vim: ts=4 sw=4 sts=4 fdm=marker ai noet:
// PukiWiki - Yet another WikiWikiWeb clone.
// $Id: make_link.php,v 1.5 2005/02/11 14:47:14 ishii Exp $
//
// Hyperlink-related functions

// {{{ make_link() リンクを付加する
// Hyperlink decoration
function make_link($string, $page = '')
{
	global $vars;
	static $converter;

	if (! isset($converter)) $converter = new InlineConverter();

	$clone = $converter->get_clone($converter);

	return $clone->convert($string, ($page != '') ? $page : $vars['page']);
}

// }}}
// {{{ class InlineConverter インライン要素を置換する
// Converters of inline element
class InlineConverter
{
	var $converters; // as array()
	var $pattern;
	var $pos;
	var $result;

	// {{{ get_clone()

	function get_clone($obj) {
		static $clone_func;

		if (! isset($clone_func)) {
			if (version_compare(PHP_VERSION, '5.0.0', '<')) {
				$clone_func = create_function('$a', 'return $a;');
			} else {
				$clone_func = create_function('$a', 'return clone $a;');
			}
		}
		return $clone_func($obj);
	}

	// }}}
	// {{{ __clone()

	function __clone() {
		$converters = array();
		foreach ($this->converters as $key=>$converter) {
			$converters[$key] = $this->get_clone($converter);
		}
		$this->converters = $converters;
	}

	// }}}
	// {{{ InlineConverter()

	function InlineConverter($converters = NULL, $excludes = NULL)
	{
		if ($converters === NULL) {
			$converters = array(
				'plugin',        // Inline plugins
				'note',          // Footnotes
				'url',           // URLs
				'url_interwiki', // URLs (interwiki definition)
				'mailto',        // mailto: URL schemes
				'interwikiname', // InterWikiNames
				'autolink',      // AutoLinks
				'bracketname',   // BracketNames
				'wikiname',      // WikiNames
				'autolink_a',    // AutoLinks(alphabet)
			);
		}

		if ($excludes !== NULL)
			$converters = array_diff($converters, $excludes);

		$this->converters = $patterns = array();
		$start = 1;

		foreach ($converters as $name) {
			$classname = 'Link_' . $name;
			$converter = new $classname($start);
			$pattern   = $converter->get_pattern();
			if ($pattern === FALSE) continue;

			$patterns[] = '(' . "\n" . $pattern . "\n" . ')';
			$this->converters[$start] = $converter;
			$start += $converter->get_count();
			++$start;
		}
		$this->pattern = join('|', $patterns);
	}

	// }}}
	// {{{ convert()

	function convert($string, $page)
	{
		$this->page   = $page;
		$this->result = array();

		$string = preg_replace_callback('/' . $this->pattern . '/x',
			array(& $this, 'replace'), $string);

		$arr = explode("\x08", make_line_rules(htmlspecialchars($string)));
		$retval = '';
		while (! empty($arr)) {
			$retval .= array_shift($arr) . array_shift($this->result);
		}
		return $retval;
	}

	// }}}
	// {{{ replace()

	function replace($arr)
	{
		$obj = $this->get_converter($arr);

		$this->result[] = ($obj !== NULL && $obj->set($arr, $this->page) !== FALSE) ?
			$obj->toString() : make_line_rules(htmlspecialchars($arr[0]));

		return "\x08"; // Add a mark into latest processed part
	}

	// }}}
	// {{{ get_objects()

	function get_objects($string, $page)
	{
		$matches = $arr = array();
		preg_match_all('/' . $this->pattern . '/x', $string, $matches, PREG_SET_ORDER);
		foreach ($matches as $match) {
			$obj = $this->get_converter($match);
			if ($obj->set($match, $page) !== FALSE) {
				$arr[] = $this->get_clone($obj);
				if ($obj->body != '')
					$arr = array_merge($arr, $this->get_objects($obj->body, $page));
			}
		}
		return $arr;
	}

	// }}}
	// {{{ get_converter()

	function & get_converter(& $arr)
	{
		foreach (array_keys($this->converters) as $start) {
			if ($arr[$start] == $arr[0])
				return $this->converters[$start];
		}
		return NULL;
	}

	// }}}
}

// }}}
// {{{ class Link インライン要素集合のベースクラス
// Base class of inline elements
class Link
{
	var $start;   // Origin number of parentheses (0 origin)
	var $text;    // Matched string

	var $type;
	var $page;
	var $name;
	var $body;
	var $alias;

	// Constructor
	function Link($start)
	{
		$this->start = $start;
	}

	// Return a regex pattern to match
	function get_pattern() {}

	// Return number of parentheses (except (?:...) )
	function get_count() {}

	// Set pattern that matches
	function set($arr, $page) {}

	function toString() {}

	// Private: Get needed parts from a matched array()
	function splice($arr) {
		$count = $this->get_count() + 1;
		$arr   = array_pad(array_splice($arr, $this->start, $count), $count, '');
		$this->text = $arr[0];
		return $arr;
	}

	// Set basic parameters
	function setParam($page, $name, $body, $type = '', $alias = '')
	{
		static $converter = NULL;

		$this->page = $page;
		$this->name = $name;
		$this->body = $body;
		$this->type = $type;
		if (is_url($alias) && preg_match('/\.(gif|png|jpe?g)$/i', $alias)) {
			$alias = htmlspecialchars($alias);
			$alias = '<img src="' . $alias . '" alt="' . $name . '" />';
		} else if ($alias != '') {
			if ($converter === NULL)
				$converter = new InlineConverter(array('plugin'));

			$alias = make_line_rules($converter->convert($alias, $page));

			// BugTrack/669: A hack removing anchor tags added by AutoLink
			$alias = preg_replace('#</?a[^>]*>#i', '', $alias);
		}
		$this->alias = $alias;

		return TRUE;
	}
}

// }}}
// {{{ class Link_plugin インラインプラグイン
// Inline plugins
class Link_plugin extends Link
{
	var $pattern;
	var $plain,$param;

	function Link_plugin($start)
	{
		parent::Link($start);
	}

	// {{{ get_pattern()

	function get_pattern()
	{
		$this->pattern = <<<EOD
&
(      # (1) plain
 (\w+) # (2) plugin name
 (?:
  \(
   ((?:(?!\)[;{]).)*) # (3) parameter
  \)
 )?
)
EOD;
		return <<<EOD
{$this->pattern}
(?:
 \{
  ((?:(?R)|(?!};).)*) # (4) body
 \}
)?
;
EOD;
	}

	// }}}
	// {{{ get_count()

	function get_count()
	{
		return 4;
	}

	// }}}
	// {{{ set()

	function set($arr, $page)
	{
		list($all, $this->plain, $name, $this->param, $body) = $this->splice($arr);

		// Re-get true plugin name and patameters (for PHP 4.1.2)
		$matches = array();
		if (preg_match('/^' . $this->pattern . '/x', $all, $matches)
			&& $matches[1] != $this->plain) 
			list(, $this->plain, $name, $this->param) = $matches;

		return parent::setParam($page, $name, $body, 'plugin');
	}

	// }}}
	// {{{ toString()

	function toString()
	{
		$body = ($this->body == '') ? '' : make_link($this->body);
		$str = FALSE;

		// Try to call the plugin
		if (exist_plugin_inline($this->name))
			$str = do_plugin_inline($this->name, $this->param, $body);

		if ($str !== FALSE) {
			return $str; // Succeed
		} else {
			// No such plugin, or Failed
			$body = (($body == '') ? '' : '{' . $body . '}') . ';';
			return make_line_rules(htmlspecialchars('&' . $this->plain) . $body);
		}
	}

	// }}}
}

// }}}
// {{{ class Link_note 注釈
// Footnotes
class Link_note extends Link
{
	function Link_note($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		return <<<EOD
\(\(
 ((?:(?R)|(?!\)\)).)*) # (1) note body
\)\)
EOD;
	}

	function get_count()
	{
		return 1;
	}

	function set($arr, $page)
	{
		global $foot_explain, $script, $vars;
		static $note_id = 0;

		list(, $body) = $this->splice($arr);

		$id   = ++$note_id;
		$note = make_link($body);
		$page = isset($vars['page']) ? rawurlencode($vars['page']) : '';

		// Footnote
		$foot_explain[$id] = '<a id="notefoot_' . $id . '" href="' .
			$script . '?' . $page . '#notetext_' . $id .
			'" class="note_super">*' . $id . '</a>' . "\n" .
			'<span class="small">' . $note . '</span><br />';

		// A hyperlink, content-body to footnote
		$name = '<a id="notetext_' . $id . '" href="' . $script . '?' . $page .
			'#notefoot_' . $id . '" class="note_super" title="' .
			htmlspecialchars(strip_tags($note)) . '">*' . $id . '</a>';

		return parent::setParam($page, $name, $body);
	}

	function toString()
	{
		return $this->name;
	}
}

// }}}
// {{{ class Link_url url
// URLs
class Link_url extends Link
{
	function Link_url($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		$s1 = $this->start + 1;
		return <<<EOD
(\[\[             # (1) open bracket
 ((?:(?!\]\]).)+) # (2) alias
 (?:>|:)
)?
(                 # (3) url
 (?:(?:https?|ftp|news):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+
)
(?($s1)\]\])      # close bracket
EOD;
	}

	function get_count()
	{
		return 3;
	}

	function set($arr, $page)
	{
		list(, , $alias, $name) = $this->splice($arr);
		return parent::setParam($page, htmlspecialchars($name),
			'', 'url', $alias == '' ? $name : $alias);
	}

	function toString()
	{
		if (FALSE) {
			$rel = '';
		} else {
			$rel = ' rel="nofollow"';
		}
		return '<a href="' . $this->name . '"' . $rel . '>' . $this->alias . '</a>';
	}
}

// }}}
// {{{ class Link_url_interwiki url (InterWiki definition type)
// URLs (InterWiki definition on "InterWikiName")
class Link_url_interwiki extends Link
{
	function Link_url_interwiki($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		return <<<EOD
\[       # open bracket
(        # (1) url
 (?:(?:https?|ftp|news):\/\/|\.\.?\/)[!~*'();\/?:\@&=+\$,%#\w.-]*
)
\s
([^\]]+) # (2) alias
\]       # close bracket
EOD;
	}

	function get_count()
	{
		return 2;
	}

	function set($arr, $page)
	{
		list(, $name, $alias) = $this->splice($arr);
		return parent::setParam($page, htmlspecialchars($name), '', 'url', $alias);
	}

	function toString()
	{
		return '<a href="' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
	}
}

// }}}
// {{{ class Link_mailto mailto:
// mailto: URL schemes
class Link_mailto extends Link
{
	var $is_image, $image;

	function Link_mailto($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		$s1 = $this->start + 1;
		return <<<EOD
(?:
 \[\[
 ((?:(?!\]\]).)+)(?:>|:)  # (1) alias
)?
([\w.-]+@[\w-]+\.[\w.-]+) # (2) mailto
(?($s1)\]\])              # close bracket if (1)
EOD;
	}

	function get_count()
	{
		return 2;
	}

	function set($arr, $page)
	{
		list(, $alias, $name) = $this->splice($arr);
		return parent::setParam($page, $name, '', 'mailto', $alias == '' ? $name : $alias);
	}
	
	function toString()
	{
		return '<a href="mailto:' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
	}
}

// }}}
// {{{ class Link_interwikiname InterWikiName
// InterWikiName-rendered URLs
class Link_interwikiname extends Link
{
	var $url    = '';
	var $param  = '';
	var $anchor = '';

	function Link_interwikiname($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		$s2 = $this->start + 2;
		$s5 = $this->start + 5;
		return <<<EOD
\[\[                  # open bracket
(?:
 ((?:(?!\]\]).)+)>    # (1) alias
)?
(\[\[)?               # (2) open bracket
((?:(?!\s|:|\]\]).)+) # (3) InterWiki
(?<! > | >\[\[ )      # not '>' or '>[['
:                     # separator
(                     # (4) param
 (\[\[)?              # (5) open bracket
 (?:(?!>|\]\]).)+
 (?($s5)\]\])         # close bracket if (5)
)
(?($s2)\]\])          # close bracket if (2)
\]\]                  # close bracket
EOD;
	}

	function get_count()
	{
		return 5;
	}

	function set($arr, $page)
	{
		global $script;

		list(, $alias, , $name, $this->param) = $this->splice($arr);

		$matches = array();
		if (preg_match('/^([^#]+)(#[A-Za-z][\w-]*)$/', $this->param, $matches))
			list(, $this->param, $this->anchor) = $matches;

		$url = get_interwiki_url($name, $this->param);
		$this->url = ($url === FALSE) ?
			$script . '?' . rawurlencode('[[' . $name . ':' . $this->param . ']]') :
			htmlspecialchars($url);

		return parent::setParam(
			$page,
			htmlspecialchars($name . ':' . $this->param),
			'',
			'InterWikiName',
			$alias == '' ? $name . ':' . $this->param : $alias
		);
	}

	function toString()
	{
		return '<a href="' . $this->url . $this->anchor . '" title="' .
			$this->name . '" rel="nofollow">' . $this->alias . '</a>';
	}
}

// }}}
// {{{ class Link_bracketname BracketName
// BracketNames
class Link_bracketname extends Link
{
	var $anchor, $refer;

	function Link_bracketname($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		global $WikiName, $BracketName;

		$s2 = $this->start + 2;
		return <<<EOD
\[\[                     # Open bracket
(?:((?:(?!\]\]).)+)>)?   # (1) Alias
(\[\[)?                  # (2) Open bracket
(                        # (3) PageName
 (?:$WikiName)
 |
 (?:$BracketName)
)?
(\#(?:[a-zA-Z][\w-]*)?)? # (4) Anchor
(?($s2)\]\])             # Close bracket if (2)
\]\]                     # Close bracket
EOD;
	}

	function get_count()
	{
		return 4;
	}

	function set($arr, $page)
	{
		global $WikiName;

		list(, $alias, , $name, $this->anchor) = $this->splice($arr);
		if ($name == '' && $this->anchor == '') return FALSE;

		if ($name == '' || ! preg_match('/^' . $WikiName . '$/', $name)) {
			if ($alias == '') $alias = $name . $this->anchor;
			if ($name != '') {
				$name = get_fullname($name, $page);
				if (! is_pagename($name)) return FALSE;
			}
		}

		return parent::setParam($page, $name, '', 'pagename', $alias);
	}

	function toString()
	{
		return make_pagelink(
			$this->name,
			$this->alias,
			$this->anchor,
			$this->page
		);
	}
}

// }}}
// {{{ class Link_wikiname WikiName
// WikiNames
class Link_wikiname extends Link
{
	function Link_wikiname($start)
	{
		parent::Link($start);
	}

	function get_pattern()
	{
		global $WikiName, $nowikiname;

		return $nowikiname ? FALSE : '(' . $WikiName . ')';
	}

	function get_count()
	{
		return 1;
	}

	function set($arr, $page)
	{
		list($name) = $this->splice($arr);
		return parent::setParam($page, $name, '', 'pagename', $name);
	}

	function toString()
	{
		return make_pagelink(
			$this->name,
			$this->alias,
			'',
			$this->page
		);
	}
}

// }}}
// {{{ class Link_autolink AutoLink
// AutoLinks
class Link_autolink extends Link
{
	var $forceignorepages = array();
	var $auto;
	var $auto_a; // alphabet only

	function Link_autolink($start)
	{
		global $autolink;

		parent::Link($start);

		if (! $autolink || ! file_exists(CACHE_DIR . 'autolink.dat'))
			return;

		@list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . 'autolink.dat');
		$this->auto   = $auto;
		$this->auto_a = $auto_a;
		$this->forceignorepages = explode("\t", trim($forceignorepages));
	}

	function get_pattern()
	{
		return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
	}

	function get_count()
	{
		return 1;
	}

	function set($arr, $page)
	{
		global $WikiName;

		list($name) = $this->splice($arr);

		// Ignore pages listed, or Expire ones not found
		if (in_array($name, $this->forceignorepages) || ! is_page($name))
			return FALSE;

		return parent::setParam($page, $name, '', 'pagename', $name);
	}

	function toString()
	{
		return make_pagelink($this->name, $this->alias, '', $this->page);
	}
}

// }}}
// {{{ class Link_autolink_a

class Link_autolink_a extends Link_autolink
{
	function Link_autolink_a($start)
	{
		parent::Link_autolink($start);
	}

	function get_pattern()
	{
		return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
	}
}

// }}}
// {{{ make_pagelink() ページ名のリンクを作成
// Make hyperlink for the page
function make_pagelink($page, $alias = '', $anchor = '', $refer = '')
{
	global $script, $vars, $link_compact, $related, $_symbol_noexists;

	$s_page = htmlspecialchars(strip_bracket($page));
	$s_alias = ($alias == '') ? $s_page : $alias;

	if ($page == '') return '<a href="' . $anchor . '">' . $s_alias . '</a>';

	$r_page  = rawurlencode($page);
	$r_refer = ($refer == '') ? '' : '&amp;refer=' . rawurlencode($refer);

	if (! isset($related[$page]) && $page != $vars['page'] && is_page($page))
		$related[$page] = get_filetime($page);

	if (is_page($page)) {
		// Hyperlinks
		$passage = get_pg_passage($page, FALSE);
		$title   = $link_compact ? '' : ' title="' . $s_page . $passage . '"';
		return '<a href="' . $script . '?' . $r_page . $anchor . '"' . $title . '>' .
			$s_alias . '</a>';
	} else if (PKWK_READONLY) {
		// Without hyperlink (= Suppress dangling link)
		return $s_alias;
	} else {
		// Dangling links
		$retval = $s_alias . '<a href="' .
			$script . '?cmd=edit&amp;page=' . $r_page . $r_refer . '">' .
			$_symbol_noexists . '</a>';
		if (! $link_compact)
			$retval = '<span class="noexists">' . $retval . '</span>';
		return $retval;
	}
}

// }}}
// {{{ get_fullname() 相対参照を展開
// Resolve relative / (Unix-like)absolute path of the page
function get_fullname($name, $refer)
{
	global $defaultpage;

	// 'Here'
	if ($name == '' || $name == './') return $refer;

	// Absolute path
	if ($name{0} == '/') {
		$name = substr($name, 1);
		return ($name == '') ? $defaultpage : $name;
	}

	// Relative path from 'Here'
	if (substr($name, 0, 2) == './') {
		$arrn    = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
		$arrn[0] = $refer;
		return join('/', $arrn);
	}

	// Relative path from dirname()
	if (substr($name, 0, 3) == '../') {
		$arrn = preg_split('#/#', $name,  -1, PREG_SPLIT_NO_EMPTY);
		$arrp = preg_split('#/#', $refer, -1, PREG_SPLIT_NO_EMPTY);

		while (! empty($arrn) && $arrn[0] == '..') {
			array_shift($arrn);
			array_pop($arrp);
		}
		$name = ! empty($arrp) ? join('/', array_merge($arrp, $arrn)) :
			(! empty($arrn) ? $defaultpage . '/' . join('/', $arrn) : $defaultpage);
	}

	return $name;
}

// }}}
// {{{ get_interwiki_url() InterWikiNameを展開
// Render an InterWiki into a URL
function get_interwiki_url($name, $param)
{
	global $WikiName, $interwiki;
	static $interwikinames;
	static $encode_aliases = array('sjis'=>'SJIS', 'euc'=>'EUC-JP', 'utf8'=>'UTF-8');

	if (! isset($interwikinames)) {
		$interwikinames = $matches = array();
		foreach (get_source($interwiki) as $line)
			if (preg_match('/\[(' . '(?:(?:https?|ftp|news):\/\/|\.\.?\/)' .
			    '[!~*\'();\/?:\@&=+\$,%#\w.-]*)\s([^\]]+)\]\s?([^\s]*)/',
			    $line, $matches))
				$interwikinames[$matches[2]] = array($matches[1], $matches[3]);
	}

	if (! isset($interwikinames[$name])) return FALSE;

	list($url, $opt) = $interwikinames[$name];

	// Encoding
	switch ($opt) {

	case '':
	case 'std': // As-Is (Internal encoding of this PukiWiki will be used)
		$param = rawurlencode($param);
		break;

	case 'asis': // As-Is
	case 'raw':
		// $param = htmlspecialchars($param);
		break;

	case 'yw': // YukiWiki
		if (! preg_match('/' . $WikiName . '/', $param))
			$param = '[[' . mb_convert_encoding($param, 'SJIS', SOURCE_ENCODING) . ']]';
		// $param = htmlspecialchars($param);
		break;

	case 'moin': // MoinMoin
		$param = str_replace('%', '_', rawurlencode($param));
		break;

	default:
		// Alias conversion
		if (isset($encode_aliases[$opt])) $opt = $encode_aliases[$opt];
		// Encoding conversion into specified encode, and URLencode
		$param = rawurlencode(mb_convert_encoding($param, $opt, 'auto'));
	}

	// Replace parameters
	if (strpos($url, '$1') !== FALSE) {
		$url = str_replace('$1', $param, $url);
	} else {
		$url .= $param;
	}

	$len = strlen($url);
	if ($len > 512) die_message('InterWiki URL too long: ' . $len . ' characters');

	return $url;
}

// }}}

?>
