]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/InlineParser.php
Cleanup of new inline parser code.
[SourceForge/phpwiki.git] / lib / InlineParser.php
1 <?php rcs_id('$Id: InlineParser.php,v 1.3 2002-01-29 19:28:16 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 require_once('lib/HtmlElement.php');
21
22 //FIXME: intubate ESCAPE_CHAR into BlockParser.php.
23 define('ESCAPE_CHAR', '~');
24
25 /**
26  * Return type from RegexpSet::match and RegexpSet::nextMatch.
27  *
28  * @see RegexpSet
29  */
30 class RegexpSet_match {
31     /**
32      * The text leading up the the next match.
33      */
34     var $prematch;
35
36     /**
37      * The matched text.
38      */
39     var $match;
40
41     /**
42      * The text following the matched text.
43      */
44     var $postmatch;
45
46     /**
47      * Index of the regular expression which matched.
48      */
49     var $regexp_ind;
50 }
51
52 /**
53  * A set of regular expressions.
54  *
55  * This class is probably only useful for InlineTransformer.
56  */
57 class RegexpSet
58 {
59     /** Constructor
60      *
61      * @param $regexps array A list of regular expressions.  The
62      * regular expressions should not include any sub-pattern groups
63      * "(...)".  (Anonymous groups, like "(?:...)", as well as
64      * look-ahead and look-behind assertions are fine.)
65      */
66     function RegexpSet ($regexps) {
67         $this->_regexps = $regexps;
68     }
69
70     /**
71      * Search text for the next matching regexp from the Regexp Set.
72      *
73      * @param $text string The text to search.
74      *
75      * @return object  A RegexpSet_match object, or false if no match.
76      */
77     function match ($text) {
78         return $this->_match($text, $this->_regexps, '*?');
79     }
80
81     /**
82      * Search for next matching regexp.
83      *
84      * Here, 'next' has two meanings:
85      *
86      * Match the next regexp(s) in the set, at the same position as the last match.
87      *
88      * If that fails, match the whole RegexpSet, starting after the position of the
89      * previous match.
90      *
91      * @param $text string Text to search.
92      *
93      * @param $prevMatch A RegexpSet_match object
94      *
95      * $prevMatch should be a match object obtained by a previous
96      * match upon the same value of $text.
97      *
98      * @return object  A RegexpSet_match object, or false if no match.
99      */
100     function nextMatch ($text, $prevMatch) {
101         // Try to find match at same position.
102         $pos = strlen($prevMatch->prematch);
103         $regexps = array_slice($this->_regexps, $prevMatch->regexp_ind + 1);
104         if ($regexps) {
105             $repeat = sprintf('{%d}', $pos);
106             if ( ($match = $this->_match($text, $regexps, $repeat)) )
107                 return $match;
108         }
109         
110         // Failed.  Look for match after current position.
111         $repeat = sprintf('{%d,}?', $pos + 1);
112         return $this->_match($text, $this->_regexps, $repeat);
113     }
114     
115
116     function _match ($text, $regexps, $repeat) {
117     
118         $pat= "/ ( . $repeat ) ( (" . join(')|(', $regexps) . ") ) /Axs";
119
120         if (! preg_match($pat, $text, $m))
121             return false;
122         
123         $match = new RegexpSet_match;
124         $match->postmatch = substr($text, strlen($m[0]));
125         $match->prematch = $m[1];
126         $match->match = $m[2];
127         $match->regexp_ind = count($m) - 4;
128
129         /* DEBUGGING
130         PrintXML(HTML::dl(HTML::dt("input"),
131                           HTML::dd(HTML::pre($text)),
132                           HTML::dt("match"),
133                           HTML::dd(HTML::pre($match->match)),
134                           HTML::dt("regexp"),
135                           HTML::dd(HTML::pre($regexps[$match->regexp_ind])),
136                           HTML::dt("prematch"),
137                           HTML::dd(HTML::pre($match->prematch))));
138         */
139         
140         return $match;
141     }
142 }
143
144
145
146 /**
147  * A simple markup rule (i.e. terminal token).
148  *
149  * These are defined by a regexp.
150  *
151  * When a match is found for the regexp, the matching text is replaced.
152  * The replacement content is obtained by calling the SimpleMarkup::markup method.
153  */ 
154 class SimpleMarkup
155 {
156     var $_match_regexp;
157
158     /** Get regexp.
159      *
160      * @return string Regexp which matches this token.
161      */
162     function getMatchRegexp () {
163         return $this->_match_regexp;
164     }
165
166     /** Markup matching text.
167      *
168      * @param $match string The text which matched the regexp
169      * (obtained from getMatchRegexp).
170      *
171      * @return mixed The expansion of the matched text.
172      */
173     function markup ($match /*, $body */) {
174         trigger_error("pure virtual", E_USER_ERROR);
175     }
176 }
177
178 /**
179  * A balanced markup rule.
180  *
181  * These are defined by a start regexp, and and end regexp.
182  */ 
183 class BalancedMarkup
184 {
185     var $_start_regexp;
186
187     /** Get the starting regexp for this rule.
188      *
189      * @return string The starting regexp.
190      */
191     function getStartRegexp () {
192         return $this->_start_regexp;
193     }
194     
195     /** Get the ending regexp for this rule.
196      *
197      * @param $match string  The text which matched the starting regexp.
198      *
199      * @return string The ending regexp.
200      */
201     function getEndRegexp ($match) {
202         return $this->_end_regexp;
203     }
204
205     /** Get expansion for matching input.
206      *
207      * @param $match string  The text which matched the starting regexp.
208      *
209      * @param $body mixed Transformed text found between the starting
210      * and ending regexps.
211      *
212      * @return mixed The expansion of the matched text.
213      */
214     function markup ($match, $body) {
215         trigger_error("pure virtual", E_USER_ERROR);
216     }
217 }
218
219 class Markup_escape  extends SimpleMarkup
220 {
221     function getMatchRegexp () {
222         return ESCAPE_CHAR . ".";
223     }
224     
225     function markup ($match) {
226         return $match[1];
227     }
228 }
229
230 class Markup_bracketlink  extends SimpleMarkup
231 {
232     var $_match_regexp = "\\[ .*?\S.*? \\]";
233     
234     function markup ($match) {
235         $link = LinkBracketLink($match);
236         assert($link->isInlineElement());
237         return $link;
238     }
239 }
240
241 class Markup_url extends SimpleMarkup
242 {
243     function getMatchRegexp () {
244         global $AllowedProtocols;
245         return "(?<![[:alnum:]]) (?:$AllowedProtocols) : [^\s<>\"']+ (?<![ ,.?; \] \) ])";
246     }
247     
248     function markup ($match) {
249         return LinkURL($match);
250     }
251 }
252
253
254 class Markup_interwiki extends SimpleMarkup
255 {
256     function getMatchRegexp () {
257         global $InterWikiLinkRegexp;
258         return "(?<! [[:alnum:]]) $InterWikiLinkRegexp : \S+ (?<![ ,.?; \] \) \" \' ])";
259     }
260
261     function markup ($match) {
262         return LinkInterWikiLink($match);
263     }
264 }
265
266 class Markup_wikiword extends SimpleMarkup
267 {
268     function getMatchRegexp () {
269         global $WikiNameRegexp;
270         return " $WikiNameRegexp";
271     }
272         
273     function markup ($match) {
274         return LinkWikiWord($match);
275     }
276 }
277
278 class Markup_linebreak extends SimpleMarkup
279 {
280     var $_match_regexp = "(?: (?<! %) %%% (?! %) | <br> )";
281
282     function markup () {
283         return HTML::br();
284     }
285 }
286
287 class Markup_old_emphasis  extends BalancedMarkup
288 {
289     var $_start_regexp = "''|__";
290
291     function getEndRegexp ($match) {
292         return $match;
293     }
294     
295     function markup ($match, $body) {
296         $tag = $match == "''" ? 'em' : 'strong';
297         return new HtmlElement($tag, $body);
298     }
299 }
300
301 class Markup_nestled_emphasis extends BalancedMarkup
302 {
303     var $_start_regexp = "(?<! [[:alnum:]] ) [*_=] (?=[[:alnum:]])";
304
305     function getEndRegexp ($match) {
306         return "(?<= [[:alnum:]]) \\$match (?![[:alnum:]])";
307     }
308     
309     function markup ($match, $body) {
310         switch ($match) {
311         case '*': return new HtmlElement('b', $body);
312         case '=': return new HtmlElement('tt', $body);
313         default:  return new HtmlElement('i', $body);
314         }
315     }
316 }
317
318 class Markup_html_emphasis extends BalancedMarkup
319 {
320     var $_start_regexp = "<(?: b|big|i|small|tt|
321                                abbr|acronym|cite|code|dfn|kbd|samp|strong|var|
322                                sup|sub )>";
323
324     function getEndRegexp ($match) {
325         return "<\\/" . substr($match, 1);
326     }
327     
328     function markup ($match, $body) {
329         $tag = substr($match, 1, -1);
330         return new HtmlElement($tag, $body);
331     }
332 }
333
334 // FIXME: Do away with magic phpwiki forms.  (Maybe phpwiki: links too?)
335 // FIXME: Do away with plugin-links.  They seem not to be used.
336 //Plugin link
337
338
339 class InlineTransformer
340 {
341     var $_regexps = array();
342     var $_markup = array();
343     
344     function InlineTransformer () {
345         foreach (array('escape', 'bracketlink', 'url',
346                        'interwiki', 'wikiword', 'linebreak',
347                        'old_emphasis', 'nestled_emphasis',
348                        'html_emphasis') as $mtype) {
349             $class = "Markup_$mtype";
350             $this->_addMarkup(new $class);
351         }
352     }
353
354     function _addMarkup ($markup) {
355         if (isa($markup, 'SimpleMarkup'))
356             $regexp = $markup->getMatchRegexp();
357         else
358             $regexp = $markup->getStartRegexp();
359
360         assert(!isset($this->_markup[$regexp]));
361         $this->_regexps[] = $regexp;
362         $this->_markup[] = $markup;
363     }
364         
365     function parse (&$text, $end_re = '$') {
366         //static $depth;
367         $regexps = $this->_regexps;
368
369         // $end_re takes precedence: "favor reduce over shift"
370         array_unshift($regexps, $end_re);
371         $regexps = new RegexpSet($regexps);
372         
373         $input = $text;
374         $output = new XmlContent;
375
376         $match = $regexps->match($input);
377         
378         while ($match) {
379             if ($match->regexp_ind == 0) {
380                 // No start pattern found before end pattern.
381                 // We're all done!
382                 $output->pushContent($match->prematch);
383                 $text = $match->postmatch;
384                 return $output;
385             }
386
387             $markup = $this->_markup[$match->regexp_ind - 1];
388             $body = $this->_parse_markup_body($markup, $match->match, $match->postmatch);
389             if (!$body) {
390                 // Couldn't match balanced expression.
391                 // Ignore and look for next matching start regexp.
392                 $match = $regexps->nextMatch($input, $match);
393                 continue;
394             }
395
396             // Matched markup.  Eat input, push output.
397             // FIXME: combine adjacent strings.
398             $input = $match->postmatch;
399             $output->pushContent($match->prematch,
400                                  $markup->markup($match->match, $body));
401
402             $match = $regexps->match($input);
403         }
404
405         // No pattern matched, not even the end pattern.
406         // Parse fails.
407         return false;
408     }
409
410     function _parse_markup_body ($markup, $match, &$text) {
411         if (isa($markup, 'SimpleMarkup'))
412             return true;        // Done. SimpleMarkup is simple.
413
414         $end_regexp = $markup->getEndRegexp($match);
415         return $this->parse($text, $end_regexp);
416     }
417 }
418
419 function TransformInline($text) {
420     static $trfm;
421     if (empty($trfm))
422         $trfm = new InlineTransformer;
423     return $trfm->parse($text);
424 }
425
426 // (c-file-style: "gnu")
427 // Local Variables:
428 // mode: php
429 // tab-width: 8
430 // c-basic-offset: 4
431 // c-hanging-comment-ender-p: nil
432 // indent-tabs-mode: nil
433 // End:   
434 ?>