2 // PukiWiki - Yet another WikiWikiWeb clone.
3 // $Id: make_link.php,v 1.17.2.6 2006/07/18 17:56:00 teanan Exp $
5 // 2003-2005 PukiWiki Developers Team
6 // 2001-2002 Originally written by yu-ji
7 // License: GPL v2 or (at your option) any later version
9 // Hyperlink-related functions
11 // Hyperlink decoration
12 function make_link($string, $page = '')
17 if (! isset($converter)) $converter = new InlineConverter();
19 $clone = $converter->get_clone($converter);
21 return $clone->convert($string, ($page != '') ? $page : $vars['page']);
24 // Converters of inline element
27 var $converters; // as array()
32 function get_clone($obj) {
35 if (! isset($clone_func)) {
36 if (version_compare(PHP_VERSION, '5.0.0', '<')) {
37 $clone_func = create_function('$a', 'return $a;');
39 $clone_func = create_function('$a', 'return clone $a;');
42 return $clone_func($obj);
46 $converters = array();
47 foreach ($this->converters as $key=>$converter) {
48 $converters[$key] = $this->get_clone($converter);
50 $this->converters = $converters;
53 function InlineConverter($converters = NULL, $excludes = NULL)
55 if ($converters === NULL) {
57 'plugin', // Inline plugins
60 'url_interwiki', // URLs (interwiki definition)
61 'mailto', // mailto: URL schemes
62 'interwikiname', // InterWikiNames
63 'autoalias', // AutoAlias
64 'autolink', // AutoLinks
65 'bracketname', // BracketNames
66 'wikiname', // WikiNames
67 'autoalias_a', // AutoAlias(alphabet)
68 'autolink_a', // AutoLinks(alphabet)
72 if ($excludes !== NULL)
73 $converters = array_diff($converters, $excludes);
75 $this->converters = $patterns = array();
78 foreach ($converters as $name) {
79 $classname = 'Link_' . $name;
80 $converter = new $classname($start);
81 $pattern = $converter->get_pattern();
82 if ($pattern === FALSE) continue;
84 $patterns[] = '(' . "\n" . $pattern . "\n" . ')';
85 $this->converters[$start] = $converter;
86 $start += $converter->get_count();
89 $this->pattern = join('|', $patterns);
92 function convert($string, $page)
95 $this->result = array();
97 $string = preg_replace_callback('/' . $this->pattern . '/x',
98 array(& $this, 'replace'), $string);
100 $arr = explode("\x08", make_line_rules(htmlspecialchars($string)));
102 while (! empty($arr)) {
103 $retval .= array_shift($arr) . array_shift($this->result);
108 function replace($arr)
110 $obj = $this->get_converter($arr);
112 $this->result[] = ($obj !== NULL && $obj->set($arr, $this->page) !== FALSE) ?
113 $obj->toString() : make_line_rules(htmlspecialchars($arr[0]));
115 return "\x08"; // Add a mark into latest processed part
118 function get_objects($string, $page)
120 $matches = $arr = array();
121 preg_match_all('/' . $this->pattern . '/x', $string, $matches, PREG_SET_ORDER);
122 foreach ($matches as $match) {
123 $obj = $this->get_converter($match);
124 if ($obj->set($match, $page) !== FALSE) {
125 $arr[] = $this->get_clone($obj);
126 if ($obj->body != '')
127 $arr = array_merge($arr, $this->get_objects($obj->body, $page));
133 function & get_converter(& $arr)
135 foreach (array_keys($this->converters) as $start) {
136 if ($arr[$start] == $arr[0])
137 return $this->converters[$start];
143 // Base class of inline elements
146 var $start; // Origin number of parentheses (0 origin)
147 var $text; // Matched string
156 function Link($start)
158 $this->start = $start;
161 // Return a regex pattern to match
162 function get_pattern() {}
164 // Return number of parentheses (except (?:...) )
165 function get_count() {}
167 // Set pattern that matches
168 function set($arr, $page) {}
170 function toString() {}
172 // Private: Get needed parts from a matched array()
173 function splice($arr)
175 $count = $this->get_count() + 1;
176 $arr = array_pad(array_splice($arr, $this->start, $count), $count, '');
177 $this->text = $arr[0];
181 // Set basic parameters
182 function setParam($page, $name, $body, $type = '', $alias = '')
184 static $converter = NULL;
190 if (! PKWK_DISABLE_INLINE_IMAGE_FROM_URI &&
191 is_url($alias) && preg_match('/\.(gif|png|jpe?g)$/i', $alias)) {
192 $alias = '<img src="' . htmlspecialchars($alias) . '" alt="' . $name . '" />';
193 } else if ($alias != '') {
194 if ($converter === NULL)
195 $converter = new InlineConverter(array('plugin'));
197 $alias = make_line_rules($converter->convert($alias, $page));
199 // BugTrack/669: A hack removing anchor tags added by AutoLink
200 $alias = preg_replace('#</?a[^>]*>#i', '', $alias);
202 $this->alias = $alias;
209 class Link_plugin extends Link
214 function Link_plugin($start)
216 parent::Link($start);
219 function get_pattern()
221 $this->pattern = <<<EOD
224 (\w+) # (2) plugin name
227 ((?:(?!\)[;{]).)*) # (3) parameter
236 ((?:(?R)|(?!};).)*) # (4) body
248 function set($arr, $page)
250 list($all, $this->plain, $name, $this->param, $body) = $this->splice($arr);
252 // Re-get true plugin name and patameters (for PHP 4.1.2)
254 if (preg_match('/^' . $this->pattern . '/x', $all, $matches)
255 && $matches[1] != $this->plain)
256 list(, $this->plain, $name, $this->param) = $matches;
258 return parent::setParam($page, $name, $body, 'plugin');
263 $body = ($this->body == '') ? '' : make_link($this->body);
266 // Try to call the plugin
267 if (exist_plugin_inline($this->name))
268 $str = do_plugin_inline($this->name, $this->param, $body);
270 if ($str !== FALSE) {
271 return $str; // Succeed
273 // No such plugin, or Failed
274 $body = (($body == '') ? '' : '{' . $body . '}') . ';';
275 return make_line_rules(htmlspecialchars('&' . $this->plain) . $body);
281 class Link_note extends Link
283 function Link_note($start)
285 parent::Link($start);
288 function get_pattern()
292 ((?:(?R)|(?!\)\)).)*) # (1) note body
302 function set($arr, $page)
304 global $foot_explain, $vars;
307 list(, $body) = $this->splice($arr);
309 if (PKWK_ALLOW_RELATIVE_FOOTNOTE_ANCHOR) {
312 $script = get_script_uri() . '?' . rawurlencode($page);
316 $note = make_link($body);
317 $page = isset($vars['page']) ? rawurlencode($vars['page']) : '';
320 $foot_explain[$id] = '<a id="notefoot_' . $id . '" href="' .
321 $script . '#notetext_' . $id . '" class="note_super">*' .
322 $id . '</a>' . "\n" .
323 '<span class="small">' . $note . '</span><br />';
325 // A hyperlink, content-body to footnote
326 if (! is_numeric(PKWK_FOOTNOTE_TITLE_MAX) || PKWK_FOOTNOTE_TITLE_MAX <= 0) {
329 $title = strip_tags($note);
330 $count = mb_strlen($title, SOURCE_ENCODING);
331 $title = mb_substr($title, 0, PKWK_FOOTNOTE_TITLE_MAX, SOURCE_ENCODING);
332 $abbr = (mb_strlen($title) < $count) ? '...' : '';
333 $title = ' title="' . $title . $abbr . '"';
335 $name = '<a id="notetext_' . $id . '" href="' . $script .
336 '#notefoot_' . $id . '" class="note_super"' . $title .
339 return parent::setParam($page, $name, $body);
349 class Link_url extends Link
351 function Link_url($start)
353 parent::Link($start);
356 function get_pattern()
358 $s1 = $this->start + 1;
360 (\[\[ # (1) open bracket
361 ((?:(?!\]\]).)+) # (2) alias
365 (?:(?:https?|ftp|news):\/\/|mailto:)[\w\/\@\$()!?&%#:;.,~'=*+-]+
367 (?($s1)\]\]) # close bracket
376 function set($arr, $page)
378 list(, , $alias, $name) = $this->splice($arr);
379 return parent::setParam($page, htmlspecialchars($name),
380 '', 'url', $alias == '' ? $name : $alias);
388 $rel = ' rel="nofollow"';
390 return '<a href="' . $this->name . '"' . $rel . '>' . $this->alias . '</a>';
394 // URLs (InterWiki definition on "InterWikiName")
395 class Link_url_interwiki extends Link
397 function Link_url_interwiki($start)
399 parent::Link($start);
402 function get_pattern()
407 (?:(?:https?|ftp|news):\/\/|\.\.?\/)[!~*'();\/?:\@&=+\$,%#\w.-]*
420 function set($arr, $page)
422 list(, $name, $alias) = $this->splice($arr);
423 return parent::setParam($page, htmlspecialchars($name), '', 'url', $alias);
428 return '<a href="' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
432 // mailto: URL schemes
433 class Link_mailto extends Link
435 var $is_image, $image;
437 function Link_mailto($start)
439 parent::Link($start);
442 function get_pattern()
444 $s1 = $this->start + 1;
448 ((?:(?!\]\]).)+)(?:>|:) # (1) alias
450 ([\w.-]+@[\w-]+\.[\w.-]+) # (2) mailto
451 (?($s1)\]\]) # close bracket if (1)
460 function set($arr, $page)
462 list(, $alias, $name) = $this->splice($arr);
463 return parent::setParam($page, $name, '', 'mailto', $alias == '' ? $name : $alias);
468 return '<a href="mailto:' . $this->name . '" rel="nofollow">' . $this->alias . '</a>';
472 // InterWikiName-rendered URLs
473 class Link_interwikiname extends Link
479 function Link_interwikiname($start)
481 parent::Link($start);
484 function get_pattern()
486 $s2 = $this->start + 2;
487 $s5 = $this->start + 5;
491 ((?:(?!\]\]).)+)> # (1) alias
493 (\[\[)? # (2) open bracket
494 ((?:(?!\s|:|\]\]).)+) # (3) InterWiki
495 (?<! > | >\[\[ ) # not '>' or '>[['
498 (\[\[)? # (5) open bracket
500 (?($s5)\]\]) # close bracket if (5)
502 (?($s2)\]\]) # close bracket if (2)
512 function set($arr, $page)
516 list(, $alias, , $name, $this->param) = $this->splice($arr);
519 if (preg_match('/^([^#]+)(#[A-Za-z][\w-]*)$/', $this->param, $matches))
520 list(, $this->param, $this->anchor) = $matches;
522 $url = get_interwiki_url($name, $this->param);
523 $this->url = ($url === FALSE) ?
524 $script . '?' . rawurlencode('[[' . $name . ':' . $this->param . ']]') :
525 htmlspecialchars($url);
527 return parent::setParam(
529 htmlspecialchars($name . ':' . $this->param),
532 $alias == '' ? $name . ':' . $this->param : $alias
538 return '<a href="' . $this->url . $this->anchor . '" title="' .
539 $this->name . '" rel="nofollow">' . $this->alias . '</a>';
544 class Link_bracketname extends Link
548 function Link_bracketname($start)
550 parent::Link($start);
553 function get_pattern()
555 global $WikiName, $BracketName;
557 $s2 = $this->start + 2;
560 (?:((?:(?!\]\]).)+)>)? # (1) Alias
561 (\[\[)? # (2) Open bracket
567 (\#(?:[a-zA-Z][\w-]*)?)? # (4) Anchor
568 (?($s2)\]\]) # Close bracket if (2)
578 function set($arr, $page)
582 list(, $alias, , $name, $this->anchor) = $this->splice($arr);
583 if ($name == '' && $this->anchor == '') return FALSE;
585 if ($name == '' || ! preg_match('/^' . $WikiName . '$/', $name)) {
586 if ($alias == '') $alias = $name . $this->anchor;
588 $name = get_fullname($name, $page);
589 if (! is_pagename($name)) return FALSE;
593 return parent::setParam($page, $name, '', 'pagename', $alias);
598 return make_pagelink(
608 class Link_wikiname extends Link
610 function Link_wikiname($start)
612 parent::Link($start);
615 function get_pattern()
617 global $WikiName, $nowikiname;
619 return $nowikiname ? FALSE : '(' . $WikiName . ')';
627 function set($arr, $page)
629 list($name) = $this->splice($arr);
630 return parent::setParam($page, $name, '', 'pagename', $name);
635 return make_pagelink(
645 class Link_autolink extends Link
647 var $forceignorepages = array();
649 var $auto_a; // alphabet only
651 function Link_autolink($start)
655 parent::Link($start);
657 if (! $autolink || ! file_exists(CACHE_DIR . 'autolink.dat'))
660 @list($auto, $auto_a, $forceignorepages) = file(CACHE_DIR . 'autolink.dat');
662 $this->auto_a = $auto_a;
663 $this->forceignorepages = explode("\t", trim($forceignorepages));
666 function get_pattern()
668 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
676 function set($arr, $page)
680 list($name) = $this->splice($arr);
682 // Ignore pages listed, or Expire ones not found
683 if (in_array($name, $this->forceignorepages) || ! is_page($name))
686 return parent::setParam($page, $name, '', 'pagename', $name);
691 return make_pagelink($this->name, $this->alias, '', $this->page, TRUE);
695 class Link_autolink_a extends Link_autolink
697 function Link_autolink_a($start)
699 parent::Link_autolink($start);
702 function get_pattern()
704 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
709 class Link_autoalias extends Link
711 var $forceignorepages = array();
713 var $auto_a; // alphabet only
716 function Link_autoalias($start)
718 global $autoalias, $aliaspage;
720 parent::Link($start);
722 if (!$autoalias || !file_exists(CACHE_DIR.'autoalias.dat') || $this->page==$aliaspage)
726 @list($auto,$auto_a,$forceignorepages) = file(CACHE_DIR.'autoalias.dat');
728 $this->auto_a = $auto_a;
729 $this->forceignorepages = explode("\t", trim($forceignorepages));
732 function get_pattern()
734 return isset($this->auto) ? '(' . $this->auto . ')' : FALSE;
740 function set($arr,$page)
742 list($name) = $this->splice($arr);
743 // Ignore pages listed
744 if (in_array($name, $this->forceignorepages)) {
747 return parent::setParam($page,$name,'','pagename',$name);
752 $this->alias = $this->get_alias($this->name);
753 if ($this->alias != '') {
754 $link = '[[' . $this->name . '>' . $this->alias . ']]';
755 return make_link($link);
760 function get_alias($name)
764 if (!isset($aliases)) {
765 $aliases = get_autoaliases();
768 if (array_key_exists($name, $aliases)) {
769 $result = $aliases[$this->name];
775 class Link_autoalias_a extends Link_autoalias
777 function Link_autoalias_a($start)
779 parent::Link_autoalias($start);
781 function get_pattern()
783 return isset($this->auto_a) ? '(' . $this->auto_a . ')' : FALSE;
787 // Make hyperlink for the page
788 function make_pagelink($page, $alias = '', $anchor = '', $refer = '', $isautolink = FALSE)
790 global $script, $vars, $link_compact, $related, $_symbol_noexists;
792 $s_page = htmlspecialchars(strip_bracket($page));
793 $s_alias = ($alias == '') ? $s_page : $alias;
795 if ($page == '') return '<a href="' . $anchor . '">' . $s_alias . '</a>';
797 $r_page = rawurlencode($page);
798 $r_refer = ($refer == '') ? '' : '&refer=' . rawurlencode($refer);
800 if (! isset($related[$page]) && $page != $vars['page'] && is_page($page))
801 $related[$page] = get_filetime($page);
803 if ($isautolink || is_page($page)) {
804 // Hyperlink to the page
808 $title = ' title="' . $s_page . get_pg_passage($page, FALSE) . '"';
813 $al_left = '<!--autolink-->';
814 $al_right = '<!--/autolink-->';
816 $al_left = $al_right = '';
819 return $al_left . '<a ' . 'href="' . $script . '?' . $r_page . $anchor .
820 '"' . $title . '>' . $s_alias . '</a>' . $al_right;
823 if (PKWK_READONLY) return $s_alias; // No dacorations
825 $retval = $s_alias . '<a href="' .
826 $script . '?cmd=edit&page=' . $r_page . $r_refer . '">' .
827 $_symbol_noexists . '</a>';
832 return '<span class="noexists">' . $retval . '</span>';
837 // Resolve relative / (Unix-like)absolute path of the page
838 function get_fullname($name, $refer)
843 if ($name == '' || $name == './') return $refer;
846 if ($name{0} == '/') {
847 $name = substr($name, 1);
848 return ($name == '') ? $defaultpage : $name;
851 // Relative path from 'Here'
852 if (substr($name, 0, 2) == './') {
853 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
855 return join('/', $arrn);
858 // Relative path from dirname()
859 if (substr($name, 0, 3) == '../') {
860 $arrn = preg_split('#/#', $name, -1, PREG_SPLIT_NO_EMPTY);
861 $arrp = preg_split('#/#', $refer, -1, PREG_SPLIT_NO_EMPTY);
863 while (! empty($arrn) && $arrn[0] == '..') {
867 $name = ! empty($arrp) ? join('/', array_merge($arrp, $arrn)) :
868 (! empty($arrn) ? $defaultpage . '/' . join('/', $arrn) : $defaultpage);
874 // Render an InterWiki into a URL
875 function get_interwiki_url($name, $param)
877 global $WikiName, $interwiki;
878 static $interwikinames;
879 static $encode_aliases = array('sjis'=>'SJIS', 'euc'=>'EUC-JP', 'utf8'=>'UTF-8');
881 if (! isset($interwikinames)) {
882 $interwikinames = $matches = array();
883 foreach (get_source($interwiki) as $line)
884 if (preg_match('/\[(' . '(?:(?:https?|ftp|news):\/\/|\.\.?\/)' .
885 '[!~*\'();\/?:\@&=+\$,%#\w.-]*)\s([^\]]+)\]\s?([^\s]*)/',
887 $interwikinames[$matches[2]] = array($matches[1], $matches[3]);
890 if (! isset($interwikinames[$name])) return FALSE;
892 list($url, $opt) = $interwikinames[$name];
897 case '': /* FALLTHROUGH */
898 case 'std': // Simply URL-encode the string, whose base encoding is the internal-encoding
899 $param = rawurlencode($param);
902 case 'asis': /* FALLTHROUGH */
903 case 'raw' : // Truly as-is
906 case 'yw': // YukiWiki
907 if (! preg_match('/' . $WikiName . '/', $param))
908 $param = '[[' . mb_convert_encoding($param, 'SJIS', SOURCE_ENCODING) . ']]';
911 case 'moin': // MoinMoin
912 $param = str_replace('%', '_', rawurlencode($param));
916 // Alias conversion of $opt
917 if (isset($encode_aliases[$opt])) $opt = & $encode_aliases[$opt];
919 // Encoding conversion into specified encode, and URLencode
920 $param = rawurlencode(mb_convert_encoding($param, $opt, SOURCE_ENCODING));
923 // Replace or Add the parameter
924 if (strpos($url, '$1') !== FALSE) {
925 $url = str_replace('$1', $param, $url);
931 if ($len > 512) die_message('InterWiki URL too long: ' . $len . ' characters');