]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/InlineParser.php
Added support for named anchors within wiki-text.
[SourceForge/phpwiki.git] / lib / InlineParser.php
1 <?php rcs_id('$Id: InlineParser.php,v 1.12 2002-09-16 22:12:48 dairiki 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         trigger_error("pure virtual", E_USER_ERROR);
187     }
188 }
189
190 /**
191  * A balanced markup rule.
192  *
193  * These are defined by a start regexp, and and end regexp.
194  */ 
195 class BalancedMarkup
196 {
197     var $_start_regexp;
198
199     /** Get the starting regexp for this rule.
200      *
201      * @return string The starting regexp.
202      */
203     function getStartRegexp () {
204         return $this->_start_regexp;
205     }
206     
207     /** Get the ending regexp for this rule.
208      *
209      * @param string $match The text which matched the starting regexp.
210      *
211      * @return string The ending regexp.
212      */
213     function getEndRegexp ($match) {
214         return $this->_end_regexp;
215     }
216
217     /** Get expansion for matching input.
218      *
219      * @param string $match The text which matched the starting regexp.
220      *
221      * @param mixed $body Transformed text found between the starting
222      * and ending regexps.
223      *
224      * @return mixed The expansion of the matched text.
225      */
226     function markup ($match, $body) {
227         trigger_error("pure virtual", E_USER_ERROR);
228     }
229 }
230
231 class Markup_escape  extends SimpleMarkup
232 {
233     function getMatchRegexp () {
234         return ESCAPE_CHAR . ".";
235     }
236     
237     function markup ($match) {
238         return $match[1];
239     }
240 }
241
242 class Markup_bracketlink  extends SimpleMarkup
243 {
244     var $_match_regexp = "\\#? \\[ .*?\S.*? \\]";
245     
246     function markup ($match) {
247         $link = LinkBracketLink($match);
248         assert($link->isInlineElement());
249         return $link;
250     }
251 }
252
253 class Markup_url extends SimpleMarkup
254 {
255     function getMatchRegexp () {
256         global $AllowedProtocols;
257         return "(?<![[:alnum:]]) (?:$AllowedProtocols) : [^\s<>\"']+ (?<![ ,.?; \] \) ])";
258     }
259     
260     function markup ($match) {
261         return LinkURL($match);
262     }
263 }
264
265
266 class Markup_interwiki extends SimpleMarkup
267 {
268     function getMatchRegexp () {
269         global $request;
270         $map = InterWikiMap::GetMap($request);
271         return "(?<! [[:alnum:]])" . $map->getRegexp(). ": \S+ (?<![ ,.?; \] \) \" \' ])";
272     }
273
274     function markup ($match) {
275         global $request;
276         $map = InterWikiMap::GetMap($request);
277         return $map->link($match);
278     }
279 }
280
281 class Markup_wikiword extends SimpleMarkup
282 {
283     function getMatchRegexp () {
284         global $WikiNameRegexp;
285         return " $WikiNameRegexp";
286     }
287         
288     function markup ($match) {
289         return WikiLink($match, 'auto');
290     }
291 }
292
293 class Markup_linebreak extends SimpleMarkup
294 {
295     var $_match_regexp = "(?: (?<! %) %%% (?! %) | <br> )";
296
297     function markup () {
298         return HTML::br();
299     }
300 }
301
302 class Markup_old_emphasis  extends BalancedMarkup
303 {
304     var $_start_regexp = "''|__";
305
306     function getEndRegexp ($match) {
307         return $match;
308     }
309     
310     function markup ($match, $body) {
311         $tag = $match == "''" ? 'em' : 'strong';
312         return new HtmlElement($tag, $body);
313     }
314 }
315
316 class Markup_nestled_emphasis extends BalancedMarkup
317 {
318     //var $_start_regexp = "(?<! [[:alnum:]] ) [*_=] (?=[[:alnum:]])";
319     var $_start_regexp = "(?<= \s | ^  ) [*_=] (?= \S)";
320
321     function getEndRegexp ($match) {
322         //return "(?<= [[:alnum:]]) \\$match (?![[:alnum:]])";
323         return "(?<= \S) \\$match (?= \s | $)";
324     }
325     
326     function markup ($match, $body) {
327         switch ($match) {
328         case '*': return new HtmlElement('b', $body);
329         case '=': return new HtmlElement('tt', $body);
330         default:  return new HtmlElement('i', $body);
331         }
332     }
333 }
334
335 class Markup_html_emphasis extends BalancedMarkup
336 {
337     var $_start_regexp = "<(?: b|big|i|small|tt|
338                                em|strong|
339                                abbr|acronym|cite|code|dfn|kbd|samp|var|
340                                sup|sub )>";
341
342     function getEndRegexp ($match) {
343         return "<\\/" . substr($match, 1);
344     }
345     
346     function markup ($match, $body) {
347         $tag = substr($match, 1, -1);
348         return new HtmlElement($tag, $body);
349     }
350 }
351
352 // FIXME: Do away with magic phpwiki forms.  (Maybe phpwiki: links too?)
353 // FIXME: Do away with plugin-links.  They seem not to be used.
354 //Plugin link
355
356
357 class InlineTransformer
358 {
359     var $_regexps = array();
360     var $_markup = array();
361     
362     function InlineTransformer ($markup_types = false) {
363         if (!$markup_types)
364             $markup_types = array('escape', 'bracketlink', 'url',
365                                   'interwiki', 'wikiword', 'linebreak',
366                                   'old_emphasis', 'nestled_emphasis',
367                                   'html_emphasis');
368
369         foreach ($markup_types as $mtype) {
370             $class = "Markup_$mtype";
371             $this->_addMarkup(new $class);
372         }
373     }
374
375     function _addMarkup ($markup) {
376         if (isa($markup, 'SimpleMarkup'))
377             $regexp = $markup->getMatchRegexp();
378         else
379             $regexp = $markup->getStartRegexp();
380
381         assert(!isset($this->_markup[$regexp]));
382         $this->_regexps[] = $regexp;
383         $this->_markup[] = $markup;
384     }
385         
386     function parse (&$text, $end_regexps = array('$')) {
387         $regexps = $this->_regexps;
388
389         // $end_re takes precedence: "favor reduce over shift"
390         array_unshift($regexps, $end_regexps[0]);
391         $regexps = new RegexpSet($regexps);
392         
393         $input = $text;
394         $output = new XmlContent;
395
396         $match = $regexps->match($input);
397         
398         while ($match) {
399             if ($match->regexp_ind == 0) {
400                 // No start pattern found before end pattern.
401                 // We're all done!
402                 $output->pushContent($match->prematch);
403                 $text = $match->postmatch;
404                 return $output;
405             }
406
407             $markup = $this->_markup[$match->regexp_ind - 1];
408             $body = $this->_parse_markup_body($markup, $match->match, $match->postmatch, $end_regexps);
409             if (!$body) {
410                 // Couldn't match balanced expression.
411                 // Ignore and look for next matching start regexp.
412                 $match = $regexps->nextMatch($input, $match);
413                 continue;
414             }
415
416             // Matched markup.  Eat input, push output.
417             // FIXME: combine adjacent strings.
418             $input = $match->postmatch;
419             $output->pushContent($match->prematch,
420                                  $markup->markup($match->match, $body));
421
422             $match = $regexps->match($input);
423         }
424
425         // No pattern matched, not even the end pattern.
426         // Parse fails.
427         return false;
428     }
429
430     function _parse_markup_body ($markup, $match, &$text, $end_regexps) {
431         if (isa($markup, 'SimpleMarkup'))
432             return true;        // Done. SimpleMarkup is simple.
433
434         array_unshift($end_regexps, $markup->getEndRegexp($match));
435         // Optimization: if no end pattern in text, we know the
436         // parse will fail.  This is an important optimization,
437         // e.g. when text is "*lots *of *start *delims *with
438         // *no *matching *end *delims".
439         $ends_pat = "/(?:" . join(").*(?:", $end_regexps) . ")/xs";
440         if (!preg_match($ends_pat, $text))
441             return false;
442         return $this->parse($text, $end_regexps);
443     }
444 }
445
446 class LinkTransformer extends InlineTransformer
447 {
448     function LinkTransformer () {
449         $this->InlineTransformer(array('escape', 'bracketlink', 'url',
450                                        'interwiki', 'wikiword'));
451     }
452 }
453
454 function TransformInline($text) {
455     static $trfm;
456     
457     if (empty($trfm)) {
458         $trfm = new InlineTransformer;
459     }
460     
461     return $trfm->parse($text);
462 }
463
464 function TransformLinks($text, $markup = 2.0) {
465     static $trfm;
466     
467     if (empty($trfm)) {
468         $trfm = new LinkTransformer;
469     }
470
471     if ($markup < 2.0)
472         $text = ConvertOldMarkup($text, 'links');
473     
474     return $trfm->parse($text);
475 }
476
477 // (c-file-style: "gnu")
478 // Local Variables:
479 // mode: php
480 // tab-width: 8
481 // c-basic-offset: 4
482 // c-hanging-comment-ender-p: nil
483 // indent-tabs-mode: nil
484 // End:   
485 ?>