]> CyberLeo.Net >> Repos - SourceForge/phpwiki.git/blob - lib/transform.php
Finish conversion to new OO HTML generation scheme.
[SourceForge/phpwiki.git] / lib / transform.php
1 <?php rcs_id('$Id: transform.php,v 1.33 2002-01-22 03:17:47 dairiki Exp $');
2 require_once('lib/WikiPlugin.php');
3 require_once('lib/HtmlElement.php');
4
5 define('WT_SIMPLE_MARKUP', 0);
6 define('WT_TOKENIZER', 1);
7 define('WT_MODE_MARKUP', 2);
8
9 define("ZERO_LEVEL", 0);
10 define("NESTED_LEVEL", 1);
11
12 class WikiTransform
13 {
14    // public variables (only meaningful during do_transform)
15    var $linenumber;     // current linenumber
16    var $replacements;   // storage for tokenized strings of current line
17    var $user_data;      // can be used by the transformer functions
18                         // to store miscellaneous data.
19    
20    // private variables
21    var $content;        // wiki markup, array of lines
22    var $mode_set;       // stores if a HTML mode for this line has been set
23    var $trfrm_func;     // array of registered functions
24    var $stack;          // stack for SetHTMLMode (keeping track of open tags)
25
26    /** init function */
27    function WikiTransform()
28    {
29       $this->trfrm_func = array();
30       $this->stack = new Stack;
31    }
32
33    /**
34     * Register transformation functions
35     *
36     * This should be done *before* calling do_transform
37     *
38     * @param $type enum  <dl>
39     * <dt>WT_MODE_MARKUP</dt>
40     * <dd>If one WT_MODE_MARKUP really sets the html mode, then
41     *     all successive WT_MODE_MARKUP functions are skipped.</dd>
42     * <dt>WT_TOKENIZER</dt>
43     * <dd> The transformer function is called once for each match
44     *      of the $regexp in the line.  The matched values are tokenized
45     *      to protect them from further transformation.</dd>
46     *
47     * @param $function string  Function name
48     * @param $regexp string  Required for WT_TOKENIZER functions.
49     * Optional for others. If given, the transformer function will only be
50     * called if the line matches the $regexp.
51     */
52    function register($type, $function, $regexp = false)
53    {
54       $this->trfrm_func[] = array ($type, $function, $regexp);
55    }
56
57    /**
58     * Sets current mode like list, preformatted text, plain text
59     *
60     * Takes care of closing (open) tags
61     *
62     * This is a helper function used to keep track of what HTML
63     * block-level element we are currently processing.
64     * Block-level elements are things like paragraphs "<p>",
65     * pre-formatted text "<pre>", and the various list elements:
66     * "<ul>", "<ol>" and "<dl>".  Now, SetHTMLMode is also used to
67     * keep track of "<li>" and "<dd>" elements. Note that some of these elements
68     * can be nested, while others can not.  (In particular, according to
69     * the HTML 4.01 specification,  a paragraph "<p>" element is not
70     * allowed to contain any other block-level elements.  Also <pre>,
71     * <li>,  <dt>, <dd>, <h1> ... have this same restriction.)
72     *
73     * SetHTMLMode generates whatever HTML is necessary to get us into
74     * the requested element type at the requested nesting level.
75     *
76     * @param $tag string Type of HTML element to open.
77     *
78     * If $tag is an array, $tag[0] gives the element type,
79     * and $tag[1] should be a hash containing attribute-value
80     * pairs for the element.
81     *
82     * If $tag is the empty string, all open elements (down to the
83     * level requested by $level) are closed.  Use
84     * SetHTMLMode('',0) to close all open block-level elements.
85     *
86     * @param $level string  Rrequested nesting level for current element.
87     * The nesting level for top level block is one (which is
88     * the default).
89     * Nesting is arbitrary limited to 20 levels.
90     *
91     * @return string Returns the HTML markup to open the specified element.
92     */
93    function SetHTMLMode($tag, $level = 1)
94    {
95       if (is_array($tag))
96          $el = new HtmlElement($tag[0], $tag[1]);
97       else
98          $el = new HtmlElement($tag);
99
100       $this->mode_set = 1;      // in order to prevent other mode markup
101                                 // to be executed
102       $retvar = '';
103          
104       if ($level > 20) {
105          // arbitrarily limit tag nesting
106          ExitWiki(gettext ("Lists nested too deep in SetHTMLOutputMode"));
107       }
108       
109       if ($level <= $this->stack->cnt()) {
110          // $tag has fewer nestings (old: tabs) than stack,
111          // reduce stack to that tab count
112          while ($this->stack->cnt() > $level) {
113             $closetag = $this->stack->pop();
114             assert('$closetag != false');
115             $retvar .= "</$closetag>\n";
116          }
117
118          // if list type isn't the same,
119          // back up one more and push new tag
120          if ($tag && $tag != $this->stack->top()) {
121             $closetag = $this->stack->pop();
122             $retvar .= "</$closetag>" . $el->_startTag() . "\n";
123             $this->stack->push($tag);
124          }
125    
126       }
127       else {// $level > $this->stack->cnt()
128          // Test for and close top level elements which are not allowed to contain
129          // other block-level elements.
130          if ($this->stack->cnt() == 1 and
131              preg_match('/^(p|pre|h\d)$/i', $this->stack->top()))
132          {
133             $closetag = $this->stack->pop();
134             $retvar .= "</$closetag>";
135          }
136                
137          // we add the diff to the stack
138          // stack might be zero
139          if ($this->stack->cnt() < $level) {
140             while ($this->stack->cnt() < $level - 1) {
141                // This is a bit of a hack:
142                //
143                // We're not nested deep enough, and have to make up some kind of block
144                // element to nest within.
145                //
146                // Currently, this can only happen for nested list element
147                // (either <ul> <ol> or <dl>).  What we used to do here is
148                // to open extra lists of whatever type was requested.
149                // This would result in invalid HTML, since and list is
150                // not allowed to contain another list without first containing
151                // a list item.  ("<ul><ul><li>Item</ul></ul>" is invalid.)
152                //
153                // So now, when we need extra list elements, we use a <dl>, and
154                // open it with an empty <dd>.
155                $stuff =  $this->stack->cnt() % 2 == 0 ? 'dl' : 'dd';
156                $retvar .= "<$stuff>";
157                $this->stack->push($stuff);
158             }
159
160             $retvar .= $el->_startTag() . "\n";
161             $this->stack->push($tag);
162          }
163       }
164       
165       return $this->rawtoken($retvar);
166    }
167
168    /**
169     * Start new list item element.
170     *
171     * This closes any currently open list items at the specified level or deeper,
172     * then opens a new list item element.
173     *
174     * @param $list_type string  Type of list element to open.  This should
175     * be one of 'dl', 'ol', or 'ul'.
176     *
177     * @param $level integer  Nesting depth for list item.  Should be a positive integer.
178     *
179     * @param $defn_term string  Definition term.  Specifies the contents for the
180     * &lt;dt&gt; element.  Only used if $list_type is 'dl'.
181     *
182     * @return string HTML
183     */
184    function ListItem($list_type, $level, $defn_term = '')
185    {
186        $level = min($level, 10);
187        
188        $retval = $this->SetHTMLMode($list_type, 2 * $level - 1);
189        if ($list_type == 'dl') {
190            $retval .= AsXML(HTML::dt($defn_term));
191            $retval .= $this->SetHTMLMode('dd', 2 * $level);
192        }
193        else {
194            $retval .= $this->SetHTMLMode('li', 2 * $level);
195        }
196        return $retval;
197    }
198
199
200    /** Work horse and main loop.
201     *
202     * This function does the transform from wiki markup to HTML.
203     *
204     * Contains main-loop and calls transformer functions.
205     *
206     * @param $html string  HTML header (if needed, otherwise '')
207     * (This string is prepended to the return value.)
208     *
209     * @param $content array  Wiki markup as array of lines
210     *
211     * @return string HTML
212     */
213    function do_transform($html, $content)
214    {
215       global $FieldSeparator;
216
217       $this->content = $content;
218       $this->replacements = array();
219       $this->user_data = array();
220       
221       // Loop over all lines of the page and apply transformation rules
222       $numlines = count($this->content);
223       for ($lnum = 0; $lnum < $numlines; $lnum++)
224       {
225          
226          $this->linenumber = $lnum;
227          $line = $this->content[$lnum];
228
229          // blank lines clear the current mode (to force new paragraph)
230          if (!strlen($line) || $line == "\r") {
231             $html .= $this->SetHTMLMode('', 0);
232             continue;
233          }
234
235          $this->mode_set = 0;
236
237          // main loop applying all registered functions
238          // tokenizers, markup, html mode, ...
239          // functions are executed in order of registering
240          foreach ($this->trfrm_func as $trfrm) {
241             list($flags, $func, $regexp) = $trfrm;
242
243             // if HTMLmode is already set then skip all following
244             // WT_MODE_MARKUP functions
245             if ($this->mode_set && ($flags & WT_MODE_MARKUP) != 0) 
246                continue;
247
248             if (!empty($regexp) && !preg_match("/$regexp/", $line))
249                continue;
250
251             // call registered function
252             if (($flags & WT_TOKENIZER) != 0)
253                $line = $this->tokenize($line, $regexp, $func);
254             else
255                $line = $func($line, $this);
256          }
257     
258          $html .= $line . "\n";
259       }
260       // close all tags
261       $html .= $this->SetHTMLMode('', 0);
262       
263       return new RawXml($this->untokenize($html));
264    }
265    // end do_transfrom()
266
267    // Register a new token.
268    function rawtoken($repl) {
269       global $FieldSeparator;
270       $tok = $FieldSeparator . sizeof($this->replacements) . $FieldSeparator;
271       $this->replacements[] = $repl;
272       return $tok;
273    }
274
275    // Register a new token.
276    function token($repl) {
277       return $this->rawtoken(AsXML($repl));
278    }
279    
280    // helper function which does actual tokenizing
281    function tokenize($str, $pattern, $func) {
282       // Find any strings in $str that match $pattern and
283       // store them in $orig, replacing them with tokens
284       // starting at number $ntokens - returns tokenized string
285       $new = '';      
286       while (preg_match("/^(.*?)($pattern)/", $str, $matches)) {
287          $str = substr($str, strlen($matches[0]));
288          $new .= $matches[1] . $this->token($func($matches[2], $this));
289       }
290       return $new . $str;
291    }
292
293    function untokenize($line) {
294       global $FieldSeparator;
295       
296       $chunks = explode ($FieldSeparator, "$line ");
297       $line = $chunks[0];
298       for ($i = 1; $i < count($chunks); $i += 2)
299       {
300          $tok = $chunks[$i];
301          $line .= $this->replacements[$tok] . $chunks[$i + 1];
302       }
303       return $line;
304    }
305 }
306 // end class WikiTransform
307
308
309 //////////////////////////////////////////////////////////
310
311 class WikiPageTransform
312 extends WikiTransform {
313     function WikiPageTransform() {
314         global $WikiNameRegexp, $AllowedProtocols, $InterWikiLinkRegexp;
315
316         $this->WikiTransform();
317
318         // register functions
319         // functions are applied in order of registering
320
321         $this->register(WT_SIMPLE_MARKUP, 'wtm_plugin_link');
322         $this->register(WT_MODE_MARKUP, 'wtm_plugin');
323  
324         $this->register(WT_TOKENIZER, 'wtt_doublebrackets', '\[\[');
325         $this->register(WT_TOKENIZER, 'wtt_footnotes', '^\[\d+\]');
326         $this->register(WT_TOKENIZER, 'wtt_footnoterefs', '\[\d+\]');
327         $this->register(WT_TOKENIZER, 'wtt_bracketlinks', '\[.+?\]');
328         $this->register(WT_TOKENIZER, 'wtt_urls',
329                         "!?\b($AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]");
330
331         if (function_exists('wtt_interwikilinks')) {
332             $this->register(WT_TOKENIZER, 'wtt_interwikilinks',
333                             pcre_fix_posix_classes("!?(?<![[:alnum:]])") .
334                             "$InterWikiLinkRegexp:[^\\s.,;?()]+");
335         }
336         $this->register(WT_TOKENIZER, 'wtt_bumpylinks', "!?$WikiNameRegexp");
337
338         if (function_exists('wtm_table')) {
339             $this->register(WT_MODE_MARKUP, 'wtm_table', '^\|');
340         }
341         $this->register(WT_SIMPLE_MARKUP, 'wtm_htmlchars');
342         $this->register(WT_SIMPLE_MARKUP, 'wtm_linebreak');
343         $this->register(WT_SIMPLE_MARKUP, 'wtm_bold_italics');
344         
345         $this->register(WT_MODE_MARKUP, 'wtm_list_ul');
346         $this->register(WT_MODE_MARKUP, 'wtm_list_ol');
347         $this->register(WT_MODE_MARKUP, 'wtm_list_dl');
348         $this->register(WT_MODE_MARKUP, 'wtm_preformatted');
349         $this->register(WT_MODE_MARKUP, 'wtm_headings');
350         $this->register(WT_MODE_MARKUP, 'wtm_hr');
351         $this->register(WT_MODE_MARKUP, 'wtm_paragraph');
352     }
353 };
354
355 function do_transform ($lines, $class = 'WikiPageTransform') {
356     if (is_string($lines))
357         $lines = preg_split('/[ \t\r]*\n/', trim($lines));
358
359     $trfm = new $class;
360     return $trfm->do_transform('', $lines);
361 }
362
363 class LinkTransform
364 extends WikiTransform {
365     function LinkTransform() {
366         global $WikiNameRegexp, $AllowedProtocols, $InterWikiLinkRegexp;
367
368         $this->WikiTransform();
369
370         // register functions
371         // functions are applied in order of registering
372
373         $this->register(WT_TOKENIZER, 'wtt_doublebrackets', '\[\[');
374         $this->register(WT_TOKENIZER, 'wtt_quotetoken', '\[\d+\]');
375         $this->register(WT_TOKENIZER, 'wtt_bracketlinks', '\[.+?\]');
376         $this->register(WT_TOKENIZER, 'wtt_urls',
377                         "!?\b($AllowedProtocols):[^\s<>\[\]\"'()]*[^\s<>\[\]\"'(),.?]");
378
379         if (function_exists('wtt_interwikilinks')) {
380             $this->register(WT_TOKENIZER, 'wtt_interwikilinks',
381                             pcre_fix_posix_classes("!?(?<![[:alnum:]])") .
382                             "$InterWikiLinkRegexp:[^\\s.,;?()]+");
383         }
384         $this->register(WT_TOKENIZER, 'wtt_bumpylinks', "!?$WikiNameRegexp");
385         $this->register(WT_SIMPLE_MARKUP, 'wtm_htmlchars');
386     }
387 };
388
389 /*
390 Requirements for functions registered to WikiTransform:
391
392 Signature:  function wtm_xxxx($line, &$transform)
393
394 $line ... current line containing wiki markup
395         (Note: it may already contain HTML from other transform functions)
396 &$transform ... WikiTransform object -- public variables of this
397         object and their use see above.
398
399 Functions have to return $line (doesn't matter if modified or not)
400 All conversion should take place inside $line.
401
402 Tokenizer functions should use $transform->replacements to store
403 the replacement strings. Also, they have to keep track of
404 $transform->tokencounter. See functions below. Back substitution
405 of tokenized strings is done by do_transform().
406 */
407
408
409
410    //////////////////////////////////////////////////////////
411    // Tokenizer functions
412
413
414 function  wtt_doublebrackets($match, &$trfrm)
415 {
416    return '[';
417 }
418
419 function wtt_footnotes($match, &$trfrm)
420 {
421    // FIXME: should this set HTML mode?
422    $ftnt = trim(substr($match,1,-1)) + 0;
423    $fntext = "[$ftnt]";
424    $html[] = HTML::br();
425    
426    $fnlist = $trfrm->user_data['footnotes'][$ftnt];
427    if (!is_array($fnlist)) {
428        $html[] = $fntext;
429    }
430    else {
431        $trfrm->user_data['footnotes'][$ftnt] = 'footnote_seen';
432        
433        while (list($k, $anchor) = each($fnlist)) {
434            $html[] = HTML::a(array("name" => "footnote-$ftnt",
435                                    "href" => "#$anchor",
436                                    "class" => "footnote-rev"),
437                              $fntext);
438            $fntext = '+';
439        }
440    }
441    return $html;
442 }
443
444 function wtt_footnoterefs($match, &$trfrm)
445 {
446    $ftnt = trim(substr($match,1,-1)) + 0;
447
448    $footnote_definition_seen = false;
449
450    if (empty($trfrm->user_data['footnotes']))
451       $trfrm->user_data['footnotes'] = array();
452    if (empty($trfrm->user_data['footnotes'][$ftnt]))
453       $trfrm->user_data['footnotes'][$ftnt] = array();
454    else if (!is_array($trfrm->user_data['footnotes'][$ftnt]))
455       $footnote_definition_seen = true;
456    
457
458    $link = HTML::a(array('href' => "#footnote-$ftnt"), "[$ftnt]");
459    if (!$footnote_definition_seen) {
460        $name = "footrev-$ftnt-" . count($trfrm->user_data['footnotes'][$ftnt]);
461        $link->setAttr('name', $name);
462        $trfrm->user_data['footnotes'][$ftnt][] = $name;
463    }
464    return HTML::sup(array('class' => 'footnote'), $link);
465 }
466
467 function wtt_bracketlinks($match, &$trfrm)
468 {
469     if (preg_match('/^\[\s*\]$/', $match))
470         return $match;
471     
472     $link = LinkBracketLink($match);
473     if ($link->isInlineElement())
474         return $link;
475
476     // FIXME: BIG HACK:
477     return new RawXml("</p>" . $link->asXML() . "<p>");
478 }
479
480
481 // replace all URL's with tokens, so we don't confuse them
482 // with Wiki words later. Wiki words in URL's break things.
483 // URLs preceeded by a '!' are not linked
484 function wtt_urls($match, &$trfrm) {
485     return $match[0] == "!"
486         ? substr($match,1)
487         : LinkURL($match);
488 }
489
490 // Link Wiki words (BumpyText)
491 // Wikiwords preceeded by a '!' are not linked
492 function wtt_bumpylinks($match, &$trfrm) {
493     return $match[0] == "!" ? substr($match,1) : LinkWikiWord($match);
494 }
495
496
497 // Just quote the token.
498 function wtt_quotetoken($match, &$trfrm) {
499     return $match;
500 }
501
502     
503      
504 // end of tokenizer functions
505 //////////////////////////////////////////////////////////
506
507
508 //////////////////////////////////////////////////////////
509 // basic simple markup functions
510
511 // escape HTML metachars
512 function wtm_htmlchars($line, &$transformer) {
513     return XmlElement::_quote($line);
514 }
515
516 // %%% are linebreaks
517 function wtm_linebreak($line, &$transformer) {
518     return str_replace('%%%', '<br />', $line);
519 }
520
521    // bold and italics
522    function wtm_bold_italics($line, &$transformer) {
523       $line = preg_replace('|(__)(.*?)(__)|', '<strong>\2</strong>', $line);
524       $line = preg_replace("|('')(.*?)('')|", '<em>\2</em>', $line);
525       return $line;
526    }
527
528
529
530    //////////////////////////////////////////////////////////
531    // some tokens to be replaced by (dynamic) content
532
533 // FIXME: some plugins are in-line (maybe?) and some are block level.
534 // Here we treat them all as inline, which will probably
535 // generate some minorly invalid HTML in some cases.
536 //
537 function wtm_plugin_link($line, &$transformer) {
538     // FIXME: is this good syntax?
539     global $dbi, $request;      // FIXME: make these non-global?
540     
541     if (preg_match('/^(.*?)(<\?plugin-link\s+.*?\?>)(.*)$/', $line, $m)) {
542         list(, $prematch, $plugin_pi, $postmatch) = $m;
543         $loader = new WikiPluginLoader;
544         $html = $loader->expandPI($plugin_pi, $dbi, $request);
545         $line = $prematch . $transformer->token($html) . $postmatch;
546     }
547     return $line;
548 }
549
550 function wtm_plugin($line, &$transformer) {
551     // FIXME: is this good syntax?
552     global $dbi, $request;      // FIXME: make these non-global?
553     
554     if (preg_match('/^<\?plugin(-form)?\s.*\?>\s*$/', $line)) {
555         $loader = new WikiPluginLoader;
556         $html = $loader->expandPI($line, $dbi, $request);
557         $line = $transformer->SetHTMLMode('', 0) . $transformer->token($html);
558     }
559     return $line;
560 }
561
562
563    //////////////////////////////////////////////////////////
564    // mode markup functions
565
566
567    // tabless markup for unordered, ordered, and dictionary lists
568    // ul/ol list types can be mixed, so we only look at the last
569    // character. Changes e.g. from "**#*" to "###*" go unnoticed.
570    // and wouldn't make a difference to the HTML layout anyway.
571
572    // unordered lists <UL>: "*"
573    // has to be registereed before list OL
574    function wtm_list_ul($line, &$trfrm) {
575       if (preg_match("/^([#*;]*\*)[^#]/", $line, $matches)) {
576          $numtabs = strlen($matches[1]);
577          $line = preg_replace("/^([#*]*\*)/", '', $line);
578          $line = $trfrm->ListItem('ul', $numtabs) . $line;
579       }
580       return $line;
581    }
582
583    // ordered lists <OL>: "#"
584    function wtm_list_ol($line, &$trfrm) {
585       if (preg_match("/^([#*;]*\#)/", $line, $matches)) {
586          $numtabs = strlen($matches[1]);
587          $line = preg_replace("/^([#*]*\#)/", "", $line);
588          $line = $trfrm->ListItem('ol', $numtabs) . $line;
589       }
590       return $line;
591    }
592
593
594    // definition lists <DL>: ";text:text"
595    function wtm_list_dl($line, &$trfrm) {
596       if (preg_match("/^([#*;]*;)(.*?):(.*$)/", $line, $matches)) {
597          $numtabs = strlen($matches[1]);
598          $line = $trfrm->ListItem('dl', $numtabs, $matches[2]) . $matches[3];
599       }
600       return $line;
601    }
602
603    // mode: preformatted text, i.e. <pre>
604    function wtm_preformatted($line, &$trfrm) {
605       if (preg_match("/^\s+/", $line)) {
606          $line = $trfrm->SetHTMLMode('pre') . $line;
607       }
608       return $line;
609    }
610
611    // mode: headings, i.e. <h1>, <h2>, <h3>
612    // lines starting with !,!!,!!! are headings
613    // Patch from steph/tara <tellme@climbtothestars.org>:
614    //    use <h2>, <h3>, <h4> since <h1> is page title.
615    function wtm_headings($line, &$trfrm) {
616       if (preg_match("/^(!{1,3})[^!]/", $line, $whichheading)) {
617          if($whichheading[1] == '!') $heading = 'h4';
618          elseif($whichheading[1] == '!!') $heading = 'h3';
619          elseif($whichheading[1] == '!!!') $heading = 'h2';
620          $line = preg_replace("/^!+/", '', $line);
621          $line = $trfrm->SetHTMLMode($heading) . $line;
622       }
623       return $line;
624    }
625
626 // markup for tables
627 function wtm_table($line, &$trfrm)
628 {
629    $row = '';
630    while (preg_match('/^(\|+)(v*)([<>^]?)([^|]*)/', $line, $m))
631    {
632       $line = substr($line, strlen($m[0]));
633
634       $td = HTML::td();
635       
636       if (strlen($m[1]) > 1)
637          $td->setAttr('colspan', strlen($m[1]));
638       if (strlen($m[2]) > 0)
639          $td->setAttr('rowspan', strlen($m[2]) + 1);
640       
641       if ($m[3] == '^')
642          $td->setAttr('align', 'center');
643       else if ($m[3] == '>')
644          $td->setAttr('align', 'right');
645       else
646          $td->setAttr('align', 'left');
647
648       // FIXME: this is a hack: can't tokenize whole <td></td> since we
649       // haven't marked up italics, etc... yet
650       $row .= $trfrm->rawtoken($td->_startTag() . "&nbsp;");
651       $row .= trim($m[4]);
652       $row .= $trfrm->rawtoken("&nbsp;" . $td->_endTag());
653    }
654    assert(empty($line));
655    $row = $trfrm->rawtoken("<tr>") . $row . $trfrm->rawtoken("</tr>");
656    
657    return $trfrm->SetHTMLMode(array('table',
658                                     array('cellpadding' => 1,
659                                           'cellspacing' => 1,
660                                           'border' => 1))) .
661        $row;
662 }
663
664    // four or more dashes to <hr>
665    // Note this is of type WT_MODE_MARKUP becuase <hr>'s aren't
666    // allowed within <p>'s. (e.g. "<p><hr></p>" is not valid HTML.)
667    function wtm_hr($line, &$trfrm) {
668       if (preg_match('/^-{4,}(.*)$/', $line, $m)) {
669          $line = $trfrm->SetHTMLMode('', 0) . '<hr />';
670          if ($m[1])
671             $line .= $trfrm->SetHTMLMode('p') . $m[1];
672       }
673       return $line;
674    }
675
676    // default mode: simple text paragraph
677    function wtm_paragraph($line, &$trfrm) {
678       return $trfrm->SetHTMLMode('p') . $line;
679    }
680
681 // (c-file-style: "gnu")
682 // Local Variables:
683 // mode: php
684 // tab-width: 8
685 // c-basic-offset: 4
686 // c-hanging-comment-ender-p: nil
687 // indent-tabs-mode: nil
688 // End:   
689 ?>