]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/InlineParser.php
The regexp that matches InterWiki links was too greedy; it didn't
[SourceForge/phpwiki.git] / lib / InlineParser.php
1 <?php rcs_id('$Id: InlineParser.php,v 1.20 2003-01-28 21:07:16 zorloc Exp $');
2 /* Copyright (C) 2002, Geoffrey T. Dairiki <dairiki@dairiki.org>
3  *
4  * This file is part of PhpWiki.
5  * 
6  * PhpWiki is free software; you can redistribute it and/or modify
7  * it under the terms of the GNU General Public License as published by
8  * the Free Software Foundation; either version 2 of the License, or
9  * (at your option) any later version.
10  * 
11  * PhpWiki is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  * 
16  * You should have received a copy of the GNU General Public License
17  * along with PhpWiki; if not, write to the Free Software
18  * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
19  */
20 /**
21  * This is the code which deals with the inline part of the (new-style)
22  * wiki-markup.
23  *
24  * @package Markup
25  * @author Geoffrey T. Dairiki
26  */
27 /**
28  */
29
30 require_once('lib/HtmlElement.php');
31 require_once('lib/interwiki.php');
32
33 //FIXME: intubate ESCAPE_CHAR into BlockParser.php.
34 define('ESCAPE_CHAR', '~');
35
36 /**
37  * Return type from RegexpSet::match and RegexpSet::nextMatch.
38  *
39  * @see RegexpSet
40  */
41 class RegexpSet_match {
42     /**
43      * The text leading up the the next match.
44      */
45     var $prematch;
46
47     /**
48      * The matched text.
49      */
50     var $match;
51
52     /**
53      * The text following the matched text.
54      */
55     var $postmatch;
56
57     /**
58      * Index of the regular expression which matched.
59      */
60     var $regexp_ind;
61 }
62
63 /**
64  * A set of regular expressions.
65  *
66  * This class is probably only useful for InlineTransformer.
67  */
68 class RegexpSet
69 {
70     /** Constructor
71      *
72      * @param array $regexps A list of regular expressions.  The
73      * regular expressions should not include any sub-pattern groups
74      * "(...)".  (Anonymous groups, like "(?:...)", as well as
75      * look-ahead and look-behind assertions are okay.)
76      */
77     function RegexpSet ($regexps) {
78         $this->_regexps = $regexps;
79     }
80
81     /**
82      * Search text for the next matching regexp from the Regexp Set.
83      *
84      * @param string $text The text to search.
85      *
86      * @return RegexpSet_match  A RegexpSet_match object, or false if no match.
87      */
88     function match ($text) {
89         return $this->_match($text, $this->_regexps, '*?');
90     }
91
92     /**
93      * Search for next matching regexp.
94      *
95      * Here, 'next' has two meanings:
96      *
97      * Match the next regexp(s) in the set, at the same position as the last match.
98      *
99      * If that fails, match the whole RegexpSet, starting after the position of the
100      * previous match.
101      *
102      * @param string $text Text to search.
103      *
104      * @param RegexpSet_match $prevMatch A RegexpSet_match object.
105      * $prevMatch should be a match object obtained by a previous
106      * match upon the same value of $text.
107      *
108      * @return RegexpSet_match A RegexpSet_match object, or false if no match.
109      */
110     function nextMatch ($text, $prevMatch) {
111         // Try to find match at same position.
112         $pos = strlen($prevMatch->prematch);
113         $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
114         if ($regexps) {
115             $repeat = sprintf('{%d}', $pos);
116             if ( ($match = $this->_match($text, $regexps, $repeat)) ) {
117                 $match->regexp_ind += $prevMatch->regexp_ind + 1;
118                 return $match;
119             }
120             
121         }
122         
123         // Failed.  Look for match after current position.
124         $repeat = sprintf('{%d,}?', $pos + 1);
125         return $this->_match($text, $this->_regexps, $repeat);
126     }
127     
128
129     function _match ($text, $regexps, $repeat) {
130         $pat= "/ ( . $repeat ) ( (" . join(')|(', $regexps) . ") ) /Axs";
131
132         if (! preg_match($pat, $text, $m)) {
133             return false;
134         }
135         
136         $match = new RegexpSet_match;
137         $match->postmatch = substr($text, strlen($m[0]));
138         $match->prematch = $m[1];
139         $match->match = $m[2];
140         $match->regexp_ind = count($m) - 4;
141
142         /* DEBUGGING
143         PrintXML(HTML::dl(HTML::dt("input"),
144                           HTML::dd(HTML::pre($text)),
145                           HTML::dt("match"),
146                           HTML::dd(HTML::pre($match->match)),
147                           HTML::dt("regexp"),
148                           HTML::dd(HTML::pre($regexps[$match->regexp_ind])),
149                           HTML::dt("prematch"),
150                           HTML::dd(HTML::pre($match->prematch))));
151         */
152         return $match;
153     }
154 }
155
156
157
158 /**
159  * A simple markup rule (i.e. terminal token).
160  *
161  * These are defined by a regexp.
162  *
163  * When a match is found for the regexp, the matching text is replaced.
164  * The replacement content is obtained by calling the SimpleMarkup::markup method.
165  */ 
166 class SimpleMarkup
167 {
168     var $_match_regexp;
169
170     /** Get regexp.
171      *
172      * @return string Regexp which matches this token.
173      */
174     function getMatchRegexp () {
175         return $this->_match_regexp;
176     }
177
178     /** Markup matching text.
179      *
180      * @param string $match The text which matched the regexp
181      * (obtained from getMatchRegexp).
182      *
183      * @return mixed The expansion of the matched text.
184      */
185     function markup ($match /*, $body */) {
186         // De-escape matched text.
187         $str = preg_replace('/' . ESCAPE_CHAR . '(.)/', '\1', $match);
188         return $this->_markup($str);
189     }
190
191     /** Markup matching text.
192      *
193      * @param string $match The text which matched the regexp
194      * with escaped charecters de-escaped.
195      *
196      * @return mixed The expansion of the matched text.
197      */
198     function _markup ($match) {
199         trigger_error("pure virtual", E_USER_ERROR);
200     }
201 }
202
203 /**
204  * A balanced markup rule.
205  *
206  * These are defined by a start regexp, and and end regexp.
207  */ 
208 class BalancedMarkup
209 {
210     var $_start_regexp;
211
212     /** Get the starting regexp for this rule.
213      *
214      * @return string The starting regexp.
215      */
216     function getStartRegexp () {
217         return $this->_start_regexp;
218     }
219     
220     /** Get the ending regexp for this rule.
221      *
222      * @param string $match The text which matched the starting regexp.
223      *
224      * @return string The ending regexp.
225      */
226     function getEndRegexp ($match) {
227         return $this->_end_regexp;
228     }
229
230     /** Get expansion for matching input.
231      *
232      * @param string $match The text which matched the starting regexp.
233      *
234      * @param mixed $body Transformed text found between the starting
235      * and ending regexps.
236      *
237      * @return mixed The expansion of the matched text.
238      */
239     function markup ($match, $body) {
240         trigger_error("pure virtual", E_USER_ERROR);
241     }
242 }
243
244 class Markup_escape  extends SimpleMarkup
245 {
246     function getMatchRegexp () {
247         return ESCAPE_CHAR . ".";
248     }
249     
250     function _markup ($match) {
251         return $match;
252     }
253 }
254
255 class Markup_bracketlink  extends SimpleMarkup
256 {
257     var $_match_regexp = "\\#? \\[ .*?\S.*? \\]";
258     
259     function _markup ($match) {
260         $link = LinkBracketLink($match);
261         assert($link->isInlineElement());
262         return $link;
263     }
264 }
265
266 class Markup_url extends SimpleMarkup
267 {
268     function getMatchRegexp () {
269         global $AllowedProtocols;
270         return "(?<![[:alnum:]]) (?:$AllowedProtocols) : [^\s<>\"']+ (?<![ ,.?; \] \) ])";
271     }
272     
273     function _markup ($match) {
274         return LinkURL($match);
275     }
276 }
277
278
279 class Markup_interwiki extends SimpleMarkup
280 {
281     function getMatchRegexp () {
282         global $request;
283         $map = InterWikiMap::GetMap($request);
284         return "(?<! [[:alnum:]])" . $map->getRegexp(). ": \S+ (?<![ ,.?;! \] \) \" \' ])";
285     }
286
287     function _markup ($match) {
288         global $request;
289         $map = InterWikiMap::GetMap($request);
290         return $map->link($match);
291     }
292 }
293
294 class Markup_wikiword extends SimpleMarkup
295 {
296     function getMatchRegexp () {
297         global $WikiNameRegexp;
298         return " $WikiNameRegexp";
299     }
300         
301     function _markup ($match) {
302         return WikiLink($match, 'auto');
303     }
304 }
305
306 class Markup_linebreak extends SimpleMarkup
307 {
308     var $_match_regexp = "(?: (?<! %) %%% (?! %) | <(?:br|BR)> )";
309
310     function _markup () {
311         return HTML::br();
312     }
313 }
314
315 class Markup_old_emphasis  extends BalancedMarkup
316 {
317     var $_start_regexp = "''|__";
318
319     function getEndRegexp ($match) {
320         return $match;
321     }
322     
323     function markup ($match, $body) {
324         $tag = $match == "''" ? 'em' : 'strong';
325         return new HtmlElement($tag, $body);
326     }
327 }
328
329 class Markup_nestled_emphasis extends BalancedMarkup
330 {
331     //var $_start_regexp = "(?<! [[:alnum:]] ) [*_=] (?=[[:alnum:]])";
332     var $_start_regexp = "(?<= \\s | ^ | [_=*(] )
333                           (?: (?<! _) _ (?! _)
334                             | (?<! \\*) \\* (?! \\*)
335                             | (?<! =) = (?! =) )
336                           (?= \S )";
337
338     function getEndRegexp ($match) {
339         $chr = preg_quote($match);
340         return "(?<= \S | ^ ) (?<! $chr) $chr (?! $chr) (?= \s | [.,:;\"'?_*=)] | $)";
341     }
342     
343     function markup ($match, $body) {
344         switch ($match) {
345         case '*': return new HtmlElement('b', $body);
346         case '=': return new HtmlElement('tt', $body);
347         case '_':  return new HtmlElement('i', $body);
348         }
349     }
350 }
351
352 class Markup_html_emphasis extends BalancedMarkup
353 {
354     var $_start_regexp = "<(?: b|big|i|small|tt|
355                                em|strong|
356                                abbr|acronym|cite|code|dfn|kbd|samp|var|
357                                sup|sub )>";
358
359     function getEndRegexp ($match) {
360         return "<\\/" . substr($match, 1);
361     }
362     
363     function markup ($match, $body) {
364         $tag = substr($match, 1, -1);
365         return new HtmlElement($tag, $body);
366     }
367 }
368
369 // FIXME: Do away with magic phpwiki forms.  (Maybe phpwiki: links too?)
370 // FIXME: Do away with plugin-links.  They seem not to be used.
371 //Plugin link
372
373
374 class InlineTransformer
375 {
376     var $_regexps = array();
377     var $_markup = array();
378     
379     function InlineTransformer ($markup_types = false) {
380         if (!$markup_types)
381             $markup_types = array('escape', 'bracketlink', 'url',
382                                   'interwiki', 'wikiword', 'linebreak',
383                                   'old_emphasis', 'nestled_emphasis',
384                                   'html_emphasis');
385
386         foreach ($markup_types as $mtype) {
387             $class = "Markup_$mtype";
388             $this->_addMarkup(new $class);
389         }
390     }
391
392     function _addMarkup ($markup) {
393         if (isa($markup, 'SimpleMarkup'))
394             $regexp = $markup->getMatchRegexp();
395         else
396             $regexp = $markup->getStartRegexp();
397
398         assert(!isset($this->_markup[$regexp]));
399         $this->_regexps[] = $regexp;
400         $this->_markup[] = $markup;
401     }
402         
403     function parse (&$text, $end_regexps = array('$')) {
404         $regexps = $this->_regexps;
405
406         // $end_re takes precedence: "favor reduce over shift"
407         array_unshift($regexps, $end_regexps[0]);
408         $regexps = new RegexpSet($regexps);
409         
410         $input = $text;
411         $output = new XmlContent;
412
413         $match = $regexps->match($input);
414         
415         while ($match) {
416             if ($match->regexp_ind == 0) {
417                 // No start pattern found before end pattern.
418                 // We're all done!
419                 $output->pushContent($match->prematch);
420                 $text = $match->postmatch;
421                 return $output;
422             }
423
424             $markup = $this->_markup[$match->regexp_ind - 1];
425             $body = $this->_parse_markup_body($markup, $match->match, $match->postmatch, $end_regexps);
426             if (!$body) {
427                 // Couldn't match balanced expression.
428                 // Ignore and look for next matching start regexp.
429                 $match = $regexps->nextMatch($input, $match);
430                 continue;
431             }
432
433             // Matched markup.  Eat input, push output.
434             // FIXME: combine adjacent strings.
435             $input = $match->postmatch;
436             $output->pushContent($match->prematch,
437                                  $markup->markup($match->match, $body));
438
439             $match = $regexps->match($input);
440         }
441
442         // No pattern matched, not even the end pattern.
443         // Parse fails.
444         return false;
445     }
446
447     function _parse_markup_body ($markup, $match, &$text, $end_regexps) {
448         if (isa($markup, 'SimpleMarkup'))
449             return true;        // Done. SimpleMarkup is simple.
450
451         array_unshift($end_regexps, $markup->getEndRegexp($match));
452         // Optimization: if no end pattern in text, we know the
453         // parse will fail.  This is an important optimization,
454         // e.g. when text is "*lots *of *start *delims *with
455         // *no *matching *end *delims".
456         $ends_pat = "/(?:" . join(").*(?:", $end_regexps) . ")/xs";
457         if (!preg_match($ends_pat, $text))
458             return false;
459         return $this->parse($text, $end_regexps);
460     }
461 }
462
463 class LinkTransformer extends InlineTransformer
464 {
465     function LinkTransformer () {
466         $this->InlineTransformer(array('escape', 'bracketlink', 'url',
467                                        'interwiki', 'wikiword'));
468     }
469 }
470
471 function TransformInline($text, $markup = 2.0) {
472     static $trfm;
473     
474     if (empty($trfm)) {
475         $trfm = new InlineTransformer;
476     }
477     
478     if ($markup < 2.0) {
479         $text = ConvertOldMarkup($text, 'inline');
480     }
481
482     return $trfm->parse($text);
483 }
484
485 function TransformLinks($text, $markup = 2.0) {
486     static $trfm;
487     
488     if (empty($trfm)) {
489         $trfm = new LinkTransformer;
490     }
491
492     if ($markup < 2.0) {
493         $text = ConvertOldMarkup($text, 'links');
494     }
495     
496     return $trfm->parse($text);
497 }
498
499 // (c-file-style: "gnu")
500 // Local Variables:
501 // mode: php
502 // tab-width: 8
503 // c-basic-offset: 4
504 // c-hanging-comment-ender-p: nil
505 // indent-tabs-mode: nil
506 // End:   
507 ?>