2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // $Id: make_link.php,v 1.17.2.3 2005/03/21 17:57:41 teanan Exp $
5 // Hyperlink-related functions
7 // Hyperlink decoration
8 function make_link($string, $page = '')
13 if (! isset($converter)) $converter = new InlineConverter();
15 $clone = $converter->get_clone($converter);
17 return $clone->convert($string, ($page != '') ? $page : $vars['page']);
20 // Converters of inline element
23 var $converters; // as array()
28 function get_clone($obj) {
31 if (! isset($clone_func)) {
32 if (version_compare(PHP_VERSION, '5.0.0', '<')) {
33 $clone_func = create_function('$a', 'return $a;');
35 $clone_func = create_function('$a', 'return clone $a;');
38 return $clone_func($obj);
42 $converters = array();
43 foreach ($this->converters as $key=>$converter) {
44 $converters[$key] = $this->get_clone($converter);
46 $this->converters = $converters;
49 function InlineConverter($converters = NULL, $excludes = NULL)
51 if ($converters === NULL) {
53 'plugin', // Inline plugins
56 'url_interwiki', // URLs (interwiki definition)
57 'mailto', // mailto: URL schemes
58 'interwikiname', // InterWikiNames
59 'autoalias', // AutoAlias
60 'autolink', // AutoLinks
61 'bracketname', // BracketNames
62 'wikiname', // WikiNames
63 'autoalias_a', // AutoAlias(alphabet)
64 'autolink_a', // AutoLinks(alphabet)
68 if ($excludes !== NULL)
69 $converters = array_diff($converters, $excludes);
71 $this->converters = $patterns = array();
74 foreach ($converters as $name) {
75 $classname = 'Link_' . $name;
76 $converter = new $classname($start);
77 $pattern = $converter->get_pattern();
78 if ($pattern === FALSE) continue;
80 $patterns[] = '(' . "\n" . $pattern . "\n" . ')';
81 $this->converters[$start] = $converter;
82 $start += $converter->get_count();
85 $this->pattern = join('|', $patterns);
88 function convert($string, $page)
91 $this->result = array();
93 $string = preg_replace_callback('/' . $this->pattern . '/x',
94 array(& $this, 'replace'), $string);
96 $arr = explode("\x08", make_line_rules(htmlspecialchars($string)));
98 while (! empty($arr)) {
99 $retval .= array_shift($arr) . array_shift($this->result);
104 function replace($arr)
106 $obj = $this->get_converter($arr);
108 $this->result[] = ($obj !== NULL && $obj->set($arr, $this->page) !== FALSE) ?
109 $obj->toString() : make_line_rules(htmlspecialchars($arr[0]));
111 return "\x08"; // Add a mark into latest processed part
114 function get_objects($string, $page)
116 $matches = $arr = array();
117 preg_match_all('/' . $this->pattern . '/x', $string, $matches, PREG_SET_ORDER);
118 foreach ($matches as $match) {
119 $obj = $this->get_converter($match);
120 if ($obj->set($match, $page) !== FALSE) {
121 $arr[] = $this->get_clone($obj);
122 if ($obj->body != '')
123 $arr = array_merge($arr, $this->get_objects($obj->body, $page));
129 function & get_converter(& $arr)
131 foreach (array_keys($this->converters) as $start) {
132 if ($arr[$start] == $arr[0])
133 return $this->converters[$start];
139 // Base class of inline elements
142 var $start; // Origin number of parentheses (0 origin)
143 var $text; // Matched string
152 function Link($start)
154 $this->start = $start;
157 // Return a regex pattern to match
158 function get_pattern() {}
160 // Return number of parentheses (except (?:...) )
161 function get_count() {}
163 // Set pattern that matches
164 function set($arr, $page) {}
166 function toString() {}
168 // Private: Get needed parts from a matched array()
169 function splice($arr) {
170 $count = $this->get_count() + 1;
171 $arr = array_pad(array_splice($arr, $this->start, $count), $count, '');
172 $this->text = $arr[0];
176 // Set basic parameters
177 function setParam($page, $name, $body, $type = '', $alias = '')
179 static $converter = NULL;
185 if (is_url($alias) && preg_match('/\.(gif|png|jpe?g)$/i', $alias)) {
186 $alias = htmlspecialchars($alias);
187 $alias = '<img src="' . $alias . '" alt="' . $name . '" />';
188 } else if ($alias != '') {
189 if ($converter === NULL)
190 $converter = new InlineConverter(array('plugin'));
192 $alias = make_line_rules($converter->convert($alias, $page));
194 // BugTrack/669: A hack removing anchor tags added by AutoLink
195 $alias = preg_replace('#</?a[^>]*>#i', '', $alias);
197 $this->alias = $alias;
204 class Link_plugin extends Link
209 function Link_plugin($start)
211 parent::Link($start);
214 function get_pattern()
216 $this->pattern = <<<EOD
219 (\w+) # (2) plugin name
222 ((?:(?!\)[;{]).)*) # (3) parameter
231 ((?:(?R)|(?!};).)*) # (4) body
243 function set($arr, $page)
245 list($all, $this->plain, $name, $this->param, $body) = $this->splice($arr);
247 // Re-get true plugin name and patameters (for PHP 4.1.2)
249 if (preg_match('/^' . $this->pattern . '/x', $all, $matches)
250 && $matches[1] != $this->plain)
251 list(, $this->plain, $name, $this->param) = $matches;
253 return parent::setParam($page, $name, $body, 'plugin');
258 $body = ($this->body == '') ? '' : make_link($this->body);
261 // Try to call the plugin
262 if (exist_plugin_inline($this->name))
263 $str = do_plugin_inline($this->name, $this->param, $body);
265 if ($str !== FALSE) {
266 return $str; // Succeed
268 // No such plugin, or Failed
269 $body = (($body == '') ? '' : '{' . $body . '}') . ';';
270 return make_line_rules(htmlspecialchars('&' . $this->plain) . $body);
276 class Link_note extends Link
278 function Link_note($start)
280 parent::Link($start);
283 function get_pattern()
287 ((?:(?R)|(?!\)\)).)*) # (1) note body
297 function set($arr, $page)
299 global $foot_explain, $script, $vars;
302 list(, $body) = $this->splice($arr);
305 $note = make_link($body);
306 $page = isset($vars['page']) ? rawurlencode($vars['page']) : '';
309 $foot_explain[$id] = '<a id="notefoot_' . $id . '" href="' .
310 $script . '?' . $page . '#notetext_' . $id .
311 '" class="note_super">*' . $id . '</a>' . "\n" .
312 '<span class="small">' . $note . '</span><br />';
314 // A hyperlink, content-body to footnote
315 $name = '<a id="notetext_' . $id . '" href="' . $script . '?' . $page .
316 '#notefoot_' . $id . '" class="note_super" title="' .
317 htmlspecialchars(strip_tags($note)) . '">*' . $id . '</a>';
319 return parent::setParam($page, $name, $body);
329 class Link_url extends Link
331 function Link_url($start)
333 parent::Link($start);
336 function get_pattern()
338 $s1 = $this->start + 1;
340 (\[\[ # (1) open bracket
341 ((?:(?!\]\]).)+) # (2) alias
345 (?:(?:https?|ftp|news):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+
347 (?($s1)\]\]) # close bracket
356 function set($arr, $page)
358 list(, , $alias, $name) = $this->splice($arr);
359 return parent::setParam($page, htmlspecialchars($name),
360 '', 'url', $alias == '' ? $name : $alias);
368 $rel = ' rel="nofollow"';
370 return '<a href="' . $this->name . '"' . $rel . '>' . $this->alias . '</a>';
374 // URLs (InterWiki definition on "InterWikiName")
375 class Link_url_interwiki extends Link
377 function Link_url_interwiki($start)
379 parent::Link($start);
382 function get_pattern()
387 (?:(?:https?|ftp|news):\/\/|\.\.?\/)[!~*'();\/?:\@&=+\$,%#\w.-]*
400 function set($arr, $page)
402 list(, $name, $alias) = $this->splice($arr);
403 return parent::setParam($page, htmlspecialchars($name), '', 'url', $alias);
408 return '<a href="' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
412 // mailto: URL schemes
413 class Link_mailto extends Link
415 var $is_image, $image;
417 function Link_mailto($start)
419 parent::Link($start);
422 function get_pattern()
424 $s1 = $this->start + 1;
428 ((?:(?!\]\]).)+)(?:>|:) # (1) alias
430 ([\w.-]+@[\w-]+\.[\w.-]+) # (2) mailto
431 (?($s1)\]\]) # close bracket if (1)
440 function set($arr, $page)
442 list(, $alias, $name) = $this->splice($arr);
443 return parent::setParam($page, $name, '', 'mailto', $alias == '' ? $name : $alias);
448 return '<a href="mailto:' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
452 // InterWikiName-rendered URLs
453 class Link_interwikiname extends Link
459 function Link_interwikiname($start)
461 parent::Link($start);
464 function get_pattern()
466 $s2 = $this->start + 2;
467 $s5 = $this->start + 5;
471 ((?:(?!\]\]).)+)> # (1) alias
473 (\[\[)? # (2) open bracket
474 ((?:(?!\s|:|\]\]).)+) # (3) InterWiki
475 (?<! > | >\[\[ ) # not '>' or '>[['
478 (\[\[)? # (5) open bracket
480 (?($s5)\]\]) # close bracket if (5)
482 (?($s2)\]\]) # close bracket if (2)
492 function set($arr, $page)
496 list(, $alias, , $name, $this->param) = $this->splice($arr);
499 if (preg_match('/^([^#]+)(#[A-Za-z][\w-]*)$/', $this->param, $matches))
500 list(, $this->param, $this->anchor) = $matches;
502 $url = get_interwiki_url($name, $this->param);
503 $this->url = ($url === FALSE) ?
504 $script . '?' . rawurlencode('[[' . $name . ':' . $this->param . ']]') :
505 htmlspecialchars($url);
507 return parent::setParam(
509 htmlspecialchars($name . ':' . $this->param),
512 $alias == '' ? $name . ':' . $this->param : $alias
518 return '<a href="' . $this->url . $this->anchor . '" title="' .
519 $this->name . '" rel="nofollow">' . $this->alias . '</a>';
524 class Link_bracketname extends Link
528 function Link_bracketname($start)
530 parent::Link($start);
533 function get_pattern()
535 global $WikiName, $BracketName;
537 $s2 = $this->start + 2;
540 (?:((?:(?!\]\]).)+)>)? # (1) Alias
541 (\[\[)? # (2) Open bracket
547 (\#(?:[a-zA-Z][\w-]*)?)? # (4) Anchor
548 (?($s2)\]\]) # Close bracket if (2)
558 function set($arr, $page)
562 list(, $alias, , $name, $this->anchor) = $this->splice($arr);
563 if ($name == '' && $this->anchor == '') return FALSE;
565 if ($name == '' || ! preg_match('/^' . $WikiName . '$/', $name)) {
566 if ($alias == '') $alias = $name . $this->anchor;
568 $name = get_fullname($name, $page);
569 if (! is_pagename($name)) return FALSE;
573 return parent::setParam($page, $name, '', 'pagename', $alias);
578 return make_pagelink(
588 class Link_wikiname extends Link
590 function Link_wikiname($start)
592 parent::Link($start);
595 function get_pattern()
597 global $WikiName, $nowikiname;
599 return $nowikiname ? FALSE : '(' . $WikiName . ')';
607 function set($arr, $page)
609 list($name) = $this->splice($arr);
610 return parent::setParam($page, $name, '', 'pagename', $name);
615 return make_pagelink(
625 class Link_autolink extends Link
627 var $forceignorepages = array();
629 var $auto_a; // alphabet only
631 function Link_autolink($start)
635 parent::Link($start);
637 if (! $autolink || ! file_exists(CACHE_DIR . 'autolink.dat'))
640 @list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . 'autolink.dat');
642 $this->auto_a = $auto_a;
643 $this->forceignorepages = explode("\t", trim($forceignorepages));
646 function get_pattern()
648 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
656 function set($arr, $page)
660 list($name) = $this->splice($arr);
662 // Ignore pages listed, or Expire ones not found
663 if (in_array($name, $this->forceignorepages) || ! is_page($name))
666 return parent::setParam($page, $name, '', 'pagename', $name);
671 return make_pagelink($this->name, $this->alias, '', $this->page);
675 class Link_autolink_a extends Link_autolink
677 function Link_autolink_a($start)
679 parent::Link_autolink($start);
682 function get_pattern()
684 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
689 class Link_autoalias extends Link
691 var $forceignorepages = array();
693 var $auto_a; // alphabet only
695 function Link_autoalias($start)
697 global $autoalias, $aliaspage;
699 parent::Link($start);
701 if (!$autoalias || !file_exists(CACHE_DIR.'autoalias.dat') || $this->page==$aliaspage)
705 @list($auto,$auto_a,$forceignorepages) = file(CACHE_DIR.'autoalias.dat');
707 $this->auto_a = $auto_a;
708 $this->forceignorepages = explode("\t", trim($forceignorepages));
710 function get_pattern()
712 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
718 function set($arr,$page)
720 list($name) = $this->splice($arr);
721 // Ignore pages listed
722 if (in_array($name, $this->forceignorepages)) {
725 return parent::setParam($page,$name,'','pagename',$name);
732 if (!isset($aliases)) {
733 $aliases = get_autoaliases();
735 if (array_key_exists($this->name,$aliases)) {
736 return make_link($aliases[$this->name]);
741 class Link_autoalias_a extends Link_autoalias
743 function Link_autoalias_a($start)
745 parent::Link_autoalias($start);
747 function get_pattern()
749 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
753 // Make hyperlink for the page
754 function make_pagelink($page, $alias = '', $anchor = '', $refer = '')
756 global $script, $vars, $link_compact, $related, $_symbol_noexists;
758 $s_page = htmlspecialchars(strip_bracket($page));
759 $s_alias = ($alias == '') ? $s_page : $alias;
761 if ($page == '') return '<a href="' . $anchor . '">' . $s_alias . '</a>';
763 $r_page = rawurlencode($page);
764 $r_refer = ($refer == '') ? '' : '&refer=' . rawurlencode($refer);
766 if (! isset($related[$page]) && $page != $vars['page'] && is_page($page))
767 $related[$page] = get_filetime($page);
769 if (is_page($page)) {
771 $passage = get_pg_passage($page, FALSE);
772 $title = $link_compact ? '' : ' title="' . $s_page . $passage . '"';
773 return '<a href="' . $script . '?' . $r_page . $anchor . '"' . $title . '>' .
775 } else if (PKWK_READONLY) {
776 // Without hyperlink (= Suppress dangling link)
780 $retval = $s_alias . '<a href="' .
781 $script . '?cmd=edit&page=' . $r_page . $r_refer . '">' .
782 $_symbol_noexists . '</a>';
784 $retval = '<span class="noexists">' . $retval . '</span>';
789 // Resolve relative / (Unix-like)absolute path of the page
790 function get_fullname($name, $refer)
795 if ($name == '' || $name == './') return $refer;
798 if ($name{0} == '/') {
799 $name = substr($name, 1);
800 return ($name == '') ? $defaultpage : $name;
803 // Relative path from 'Here'
804 if (substr($name, 0, 2) == './') {
805 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
807 return join('/', $arrn);
810 // Relative path from dirname()
811 if (substr($name, 0, 3) == '../') {
812 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
813 $arrp = preg_split('#/#', $refer, -1, PREG_SPLIT_NO_EMPTY);
815 while (! empty($arrn) && $arrn[0] == '..') {
819 $name = ! empty($arrp) ? join('/', array_merge($arrp, $arrn)) :
820 (! empty($arrn) ? $defaultpage . '/' . join('/', $arrn) : $defaultpage);
826 // Render an InterWiki into a URL
827 function get_interwiki_url($name, $param)
829 global $WikiName, $interwiki;
830 static $interwikinames;
831 static $encode_aliases = array('sjis'=>'SJIS', 'euc'=>'EUC-JP', 'utf8'=>'UTF-8');
833 if (! isset($interwikinames)) {
834 $interwikinames = $matches = array();
835 foreach (get_source($interwiki) as $line)
836 if (preg_match('/\[(' . '(?:(?:https?|ftp|news):\/\/|\.\.?\/)' .
837 '[!~*\'();\/?:\@&=+\$,%#\w.-]*)\s([^\]]+)\]\s?([^\s]*)/',
839 $interwikinames[$matches[2]] = array($matches[1], $matches[3]);
842 if (! isset($interwikinames[$name])) return FALSE;
844 list($url, $opt) = $interwikinames[$name];
850 case 'std': // As-Is (Internal encoding of this PukiWiki will be used)
851 $param = rawurlencode($param);
854 case 'asis': // As-Is
856 // $param = htmlspecialchars($param);
859 case 'yw': // YukiWiki
860 if (! preg_match('/' . $WikiName . '/', $param))
861 $param = '[[' . mb_convert_encoding($param, 'SJIS', SOURCE_ENCODING) . ']]';
862 // $param = htmlspecialchars($param);
865 case 'moin': // MoinMoin
866 $param = str_replace('%', '_', rawurlencode($param));
871 if (isset($encode_aliases[$opt])) $opt = $encode_aliases[$opt];
872 // Encoding conversion into specified encode, and URLencode
873 $param = rawurlencode(mb_convert_encoding($param, $opt, 'auto'));
876 // Replace parameters
877 if (strpos($url, '$1') !== FALSE) {
878 $url = str_replace('$1', $param, $url);
884 if ($len > 512) die_message('InterWiki URL too long: ' . $len . ' characters');