1<?php $T2TVersion = "20140518";
2/**
3  txt2tags.class.php
4  Written by (c) Petko Yotov 2012 www.pmwiki.org/Petko
5  Development sponsored by Eric Forgeot.
6
7  txt2tags is a lightweight markup language created by
8  Aurelio Jargas and written in Python (www.txt2tags.org).
9
10  This class here attempts to transpose some of the features
11  of the Python convertor to the PHP language.
12
13  Most of this script was written by Petko Yotov except:
14  - functions PSS(), Keep(), Restore(), RegExp $UEX based on
15    the PmWiki engine by Patrick R. Michaud www.pmichaud.com
16  - the RegExp $imgrx based on txt2tags.py by Aurelio Jargas
17
18  This text is Free software; you can redistribute it and/or
19  modify it under the terms of the GNU General Public License
20  as published by the Free Software Foundation; either version 3
21  of the License, or (at your option) any later version.
22  See http://www.gnu.org/copyleft/gpl.html for full details
23  and lack of warranty.
24
25
26  == How to use this script ==
27
28  This class should be included from other scripts, for example:
29
30  # load the class
31  require_once('txt2tags.class.php');
32
33  ## initialize a new T2T object: two ways
34
35  # either with an existing disk file:
36  $x = new T2T("test.t2t", true);
37
38  # or with a variable containing the entire T2T markup:
39  $x = new T2T($page["text"]);
40
41  # optional: some settings, after new/init, before go()
42  $x->enabletoc = 1;
43  $x->enableinclude = 1;
44  $x->snippets['**'] = "<strong>%s</strong>"; # instead of <b>
45
46  # run all processing
47  $x->go();
48
49  # get the complete HTML output including <head>
50  $html = $x->fullhtml;
51
52  # alternatively, get some other variables:
53  $body = $x->bodyhtml;     # only the part inside the <body> tags
54  $fullconfig = $x->config; # what was in the "config" area of the file?
55
56
57  == Notes ==
58
59  - This is an early public release, ready to be tested.
60  - Including disk files is disabled out of the box, because of
61    potential vulnerabilities. Enabling it on systems where
62    untrusted people could edit the t2t content is not recommended.
63    A future version may allow the inclusion of files from the
64    current directory or only with *.t2t extension.
65    To enable inclusions, use $x->enableinclude = 1;
66
67*/
68
69class T2T {
70  # these variables could be read or forced
71  var $title = '';         # the document title
72  var $content = '';       # the content of the t2t file
73  var $headers = '';       # the first 3 lines of the t2t file
74  var $enableheaders = 0;  # enables the first 3 lines headers    (default=1)
75  var $enableproc = 1;     # enables the pre and post processor   (default=1)
76  var $enabletagged = 1;   # enables the tagged mark (''tagged'') (default=1)
77  var $enableraw = 1;      # enables the raw mark (""raw"")       (default=1)
78  var $enableverbatim = 1; # enables the verbatim mark (``raw``)  (default=1)
79  var $enablehotlinks = 1; # enables hotlinks [http://www.externalserver.com/images.jpg]  (default=1) (note: it's not enabled in the python implementation of txt2tags)
80  var $config = '';       # the full config area, including ext.ref.
81  var $bodytext = '';     # the full body text after inclusions
82  var $bodyhtml = '';     # the innerHTML of the body of the output, no <html>...<head>
83  var $fullhtml = '';     # the full <html>...</html> output
84  var $enabletoc = 0;     # automatically enabled if %%toc or %!options: --toc
85  var $enableinclude = 1; # allow file inclusions
86  var $maxtoclevels = 5;  # h1-h? titles go into toc, same as %!options: --toc-level 1
87  var $mtime;             # last modified timestamp of the input file
88  var $date;              # timestamp of the current date
89  var $cssfile = '';      # the css file to be included in the HTML header
90  var $maskemail = 0;     # rewrite plaintext e-mail links
91  var $encoding = "UTF-8";             # assume default encoding if none in file
92  var $parsetargets = "html|xhtml";    # accept %!command(html) and %!command(xhtml)
93  var $snippets = array(
94  'header1'         => "<h1>%s</h1>\n", # text (first line of file)
95  'header2'         => "<h2>%s</h2>\n", # text
96  'header3'         => "<h3>%s</h3>\n", # text
97  'headerwrap'      => "<div style='text-align:center;'>\n%s</div>\n", # headers
98  'title'           => '<h%d id="%s">%s</h%1$d>', # level, id, text
99  'hrule'           => '<hr class="%s"/>', # light|heavy
100  'verbatim'        => "<pre>\n%s</pre>",  # content
101  'mono'            => '<code>%s</code>',  # content
102  'center'          => "<center>%s</center>", # content
103  'img'             => '<img align="%s" src="%s" border="0" alt=""/>', # align, url
104  'link'            => '<a href="%s">%s</a>', # url, text
105  'cssfile'         => '<link rel="stylesheet" href="%s" type="text/css"/>',
106
107  '**'              => '<b>%s</b>', # bold content
108  '//'              => '<i>%s</i>', # italics content
109  '__'              => '<u>%s</u>', # underlined content
110  '--'              => '<s>%s</s>', # striked content
111
112  'tableopen'       => "<table %s cellpadding=\"4\">\n", # align, border
113  'tableclose'      => "</table>\n",
114  'tablerow'        => " <tr>\n%s </tr>\n", # tagged cells
115  'tablehead'       => "  <th%s>%s</th>\n", # align, content
116  'tablecell'       => "  <td%s>%s</td>\n", # align, content
117
118  # special
119  'blockquoteopen'  => '<blockquote>',
120  'blockquoteclose' => '</blockquote>',
121  'paraopen'        => '<p>',
122  'paraclose'       => '</p>',
123
124  '+listopen'       => "<ol>\n",
125  '+listclose'      => "</ol>\n",
126  '+itemopen'       => "<li>",
127  '+itemmiddle'     => "",
128  '+itemclose'      => "</li>\n",
129
130  '-listopen'       => "<ul>\n",
131  '-listclose'      => "</ul>\n",
132  '-itemopen'       => "<li>",
133  '-itemmiddle'     => "",
134  '-itemclose'      => "</li>\n",
135
136  ':listopen'       => "<dl>\n",
137  ':listclose'      => "</dl>\n",
138  ':itemopen'       => "<dt>",
139  ':itemmiddle'     => "</dt><dd>\n",
140  ':itemclose'      => "</dd>\n",
141  'listindent'      => '   ', # nicer HTML output
142
143  # title, encoding, version, styles, body
144  'html'            => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
145<html xmlns="http://www.w3.org/1999/xhtml">
146<head>
147<title>%s</title>
148<meta http-equiv="Content-Type" content="text/html; charset=%s"/>
149<meta name="generator" content="txt2tags.class.php version %s"/>
150%s
151</head>
152<body bgcolor="white" text="black">
153%s
154</body></html>');
155  # used internally
156  var $infile = array();
157  var $outfile = array();
158  var $preproc = array();
159  var $postproc = array();
160  var $KPV = array();
161  var $KPCount = 0;
162  var $csslink = '';
163  function T2T($input, $isfile = 0) {
164
165    $this->set_macros($input, $isfile);     # macros
166    # get content
167    $this->content = $isfile
168      ? $this->read($input, true)
169      : str_replace("\r", '', $input);
170
171    # get header, config, body
172    $this->R = $this->head_conf_body($this->content);
173
174    # public variables
175    $this->headers  = implode("\n", $this->R['header']);
176    $this->config   = implode("\n", $this->R['config']);
177    $this->bodytext = implode("\n", $this->R['body']);
178  }
179
180  function go($output = '') { # run the full processing
181
182    $this->parse_config($this->R['config']); # read config settings
183    $lines = $this->run_preproc($this->R['body']); # run %!preproc replacements
184
185    # strip comments, identify areas and titles, tables and horizontal rules (bars)
186    $lines = $this->firstpass($lines);
187
188    # second pass
189    $lines = $this->secondpass($this->R['header'], $lines);
190
191    # restore
192    $body = implode("\n", $lines);
193    $body = $this->Restore($body);
194    $body = str_replace(array("\032\032", "\033\033"), '', $body);
195
196    # public variables
197    $this->title = $this->esc($this->run_macros($this->R['header'][0]));
198    $this->bodyhtml = $this->run_postproc($body); # %!postproc replacements
199    $html = sprintf($this->snippets['html'], $this->title, $this->encoding,
200      $GLOBALS['T2TVersion'], $this->csslink, $body);
201    $this->fullhtml = $this->run_postproc($html); # %!postproc replacements
202
203    if($output=='body') return $this->bodyhtml;
204    if($output=='html') return $this->fullhtml;
205    return;
206  }
207
208  function parse_config($lines) { # try to read the supported configuration options
209    $opts = "encoding|style|postproc|preproc|options";
210    $tgts = $this->parsetargets;
211    foreach($lines as $c){
212      if(preg_match("/^%!\\s*({$opts})(?:\\((?:{$tgts})\\))?\\s*:\\s*(.*)$/i", $c, $m)) {
213        # options, preproc and postproc are cumulative
214        list(, $setting, $val) = $m;
215        switch(strtolower($setting)) {
216          case "encoding" : $this->encoding = trim($val); break;
217          case "style" : $this->csslink = sprintf($this->snippets['cssfile'], trim($val));
218          case "preproc"  :
219			if ($this->enableproc == 1) {
220				$this->preproc[]  = $this->add_proc($setting, trim($val));
221				break;
222			}
223          case "postproc" :
224			if ($this->enableproc == 1) {
225				$this->postproc[] = $this->add_proc($setting, trim($val));
226				break;
227			}
228          case "options":
229            if(strpos(" $val", '--mask-email')) $this->maskemail = 1;
230            if(preg_match('/--toc(?!-)/', $val)) $this->enabletoc = 1;
231            if(preg_match('/--toc-level[= ]+([1-5])/', $val, $n)) $this->maxtoclevels = $n[1];
232            if(preg_match('/--encoding[= ]+(\\S+)/', $val, $n)) $this->encoding = $n[1];
233            if(preg_match('/--style[= ]+(\\S+)/', $val, $n))
234              $this->csslink = sprintf($this->snippets['cssfile'], trim($val));
235            break;
236        }
237      }
238    }
239  }
240
241  function add_proc($when, $x) { # add a pre-processor or a postprocessor
242    if(! preg_match("/^ *(\"[^\"]+\"|'[^']+'|\\S+)\\s*(.*)$/", $x, $m)) return;
243
244    $s = preg_replace('/^("|\')(.*?)\\1$/', '$2', trim($m[1]));
245    $r = preg_replace('/^("|\')(.*?)\\1$/', '$2', trim(@$m[2]));
246    $r = preg_replace('/\\\\(?=[0-9])/', '$', $r);
247    $r = str_replace(array('\\n', '\\t'), array("\n", "\t"), $r);
248    return array("\032$s\032m", $r);
249
250  }
251  function run_preproc($lines) { # make the replacements
252    $body = implode("\n", $lines);
253    foreach($this->preproc as $a) {
254      $body = preg_replace($a[0], $a[1], $body);
255    }
256    return explode("\n", $body);
257  }
258  function run_postproc($body) { # make the replacements
259    foreach($this->postproc as $a) {
260      $body = preg_replace($a[0], $a[1], $body);
261    }
262    return $body; # end of processing
263  }
264
265  function firstpass($lines) { # strip comments, identify areas, titles, toc
266    $snippets = $this->snippets;
267    $toc = array();
268    $postoc = 0;
269
270    $lines2 = array();
271    $openarea = '';
272    $areacontent = '';
273    $toccnt = 0;
274    $tocnbs = array(0, 0, 0, 0, 0, 0);
275    $table = '';
276    foreach($lines as $line) {
277
278      # special areas raw, tagged, verbatim, comments
279      if($openarea) {
280        if($line == $openarea) { # close area
281          if(rtrim($line) != "%%%") # comment areas
282            $lines2[] = $this->closeRTV($openarea, $areacontent);
283        }
284        else { # fill area
285          $areacontent .= "$line\n";
286        }
287        continue;
288      }
289      if(preg_match('/^("""|```|\'\'\'|%%%) *$/', $line)) { # open area
290        $openarea = trim($line);
291        continue;
292      }
293
294      if($line!='' && $line{0}=='%' && ! preg_match('/^%%(infile|outfile|date|mtime|rand|toc\\s*$)/i', $line) )
295        continue; # remove comment lines
296
297      # special lines raw, tagged, verbatim
298      if(preg_match('/^("""|```|\'\'\') /', $line, $m)) {
299        $lines2[] = $this->closeRTV($m[1], substr($line, 4));
300        continue;
301      }
302
303      # Title, Numbered Title
304      if(preg_match('/^ *((=){1,5})(?!=)\\s*(\\S.*[^=]|[^\\s=])\\1(?:\\[([\\w-]+)\\])?\\s*$/', $line, $m) ||
305        preg_match('/^ *((\\+){1,5})(?!\\+)\\s*(\\S.*[^+]|[^\\s=])\\1(?:\\[([\\w-]+)\\])?\\s*$/', $line, $m)  ) {
306        $toccnt++;
307        $anchor = @$m[4] ? $m[4] : "toc$toccnt";
308        $txt = $this->esc(trim($m[3]));
309        $level = strlen($m[1]);
310
311        if($m[2]=='+') {
312          if($level>$tocnbs[0])$tocnbs[$level] = 1;
313          else $tocnbs[$level]++;
314          if($level<$tocnbs[0])for($i=$level+1; $i<6; $i++) $tocnbs[$i] = 0;
315
316          $prefix = implode(".", array_slice($tocnbs, 1, $level));
317          $prefix = preg_replace('/^(0\\.)+/', '', $prefix);
318          $txt = "$prefix. $txt";
319          $tocnbs[0] = $level;
320        }
321
322        $txt = $this->Keep($txt);
323        $lines2[] = "\032\032".sprintf($snippets['title'], $level, $anchor, $txt); # \032: block that cannot be nested in lists
324
325        # collect toc entries
326        if($this->maxtoclevels>=$level)
327          $toc[] = $this->sp($level) . "- [$txt #$anchor]";
328
329        continue;
330      }
331      # tables
332      if(preg_match('/^ *(\\|\\|?) /', $line, $m)) {
333        if(!$table) { # open table
334          $attr = ($line{0}==' ')? ' align="center"' : "";
335          if(preg_match('/\\|\\s*$/', $line)) {
336            $attr .=  ' border="1"';
337          }
338
339          $table = sprintf($snippets['tableopen'], $attr);
340        }
341        # fill table
342        if($m[1]=='||') $fmt = $snippets['tablehead'];
343        else $fmt = $snippets['tablecell'];
344
345        $line = $this->run_inline($line);
346
347        $row = substr($line, strlen($m[0]));
348
349        if(! preg_match('/\\|\\s*$/', $row)) $row .= ' | ';
350        $m = preg_split('/( \\|+(?: |$))/', $row, -1, PREG_SPLIT_DELIM_CAPTURE);
351
352        $cells = '';
353        for($i=1; $i<count($m); $i+=2){
354          $c = $m[$i-1];
355          $attr = '';
356          if($c && $c{0}==' ') {
357            $attr = (substr($c, -1)==' ') ? ' align="center"' : ' align="right"';
358          }
359          $span = strlen(trim($m[$i]));
360          if($span>1) $attr .= " colspan=\"$span\"";
361          $cells .= sprintf($fmt, $attr, $c);
362        }
363        $table .= sprintf($snippets['tablerow'], $cells);
364        continue;
365      }
366      elseif($table) { # close table
367        $lines2[] = "\033\033". $this->Keep($table . $snippets['tableclose']); # \033: block that can be nested in lists
368        $table = '';
369      }
370      # horizontal rule (bar1, bar2)
371      if(preg_match('/^ *(=|-|_){20,}\\s*$/', $line, $m)) {
372        $class = $m[1] == "=" ? 'heavy':'light';
373        $lines2[] = "\032\032".$this->Keep(sprintf($snippets['hrule'], $class));
374        continue;
375      }
376      if(preg_match("/^ +\\[([\034\\w_,.+%$#@!?+~\\/-]+\\.(?:png|jpe?g|gif|bmp|svg))\\] +$/i", $line)) {
377        $lines2[] = "\033\033". $this->Keep(sprintf($snippets['center'], $this->run_inline($line)));
378        continue;
379      }
380
381      if(trim(strtolower($line))=='%%toc') {
382        $this->enabletoc = $postoc = 1;
383        $line = '%%toc';
384      }
385      $lines2[] = $line;
386    } # end foreach line
387
388    # close ALL
389    if($openarea && $openarea != '%%%') # close all areas
390      $lines2[] = $this->closeRTV($openarea, $areacontent);
391
392    if($table)
393      $lines2[] = "\033\033". $this->Keep($table . $snippets['tableclose']."\n");
394
395    if($this->enabletoc && count($toc)) {
396      if($postoc) { # there is %%toc in the page
397        array_unshift($toc, "\032\032");
398        $toc[] = "\032\032";
399        foreach($lines2 as $k=>$v) {
400          if($v=='%%toc') array_splice($lines2, $k, 1, $toc);
401        }
402      }
403      else { # before the body: bar, toc-list, bar
404        $bar = "\032\032".$this->Keep(sprintf($snippets['hrule'], 'light'));
405        array_unshift($toc, $bar);
406        $toc[] = $bar;
407
408        $lines2 = array_merge($toc, $lines2);
409      }
410    }
411    return $lines2;
412  }
413
414  function secondpass($headers, $lines) { # all other marks
415    $snippets = $this->snippets;
416
417    $lines2 = array();
418    $html = '';
419    for($i=0; $i<3; $i++) {
420      $j = $i+1;
421      $h = $this->esc($headers[$i]);
422      if($h) $html .= sprintf($snippets["header$j"], $this->run_macros($h));
423    }
424    if($html) $lines2[] = $this->Keep(sprintf($snippets["headerwrap"], $html));
425
426    $blockquote = 0;
427    $blockquotecontent = '';
428    $para = false;
429    $openlist = 0;
430    $listcontent = '';
431    $ListLevels = array(array(-1, '', '')); # $level, $spaces, $type
432
433    foreach($lines as $line) {
434
435      # blockquote
436      if(preg_match('/^(\\t+)([^\\t].*)$/', $line, $m)) {
437        if($para) {
438          $para = false;
439          $lines2[] = $snippets['paraclose'];
440        }
441        $level = strlen($m[1]);
442        $blockquotecontent .= $this->fixblockquote($blockquote, $level)
443          . $m[1]. $this->run_inline($m[2])."\n";
444        continue;
445      }
446      elseif($blockquote) { # close bq
447        $lines2[] = "\032\032".$blockquotecontent . $this->fixblockquote($blockquote, 0);
448        $blockquotecontent = '';
449      }
450
451      # List, Numbered List, Definition List
452      if(preg_match('/^( *)([+-]) (\\S.*)$/', $line, $m) ||
453        preg_match( '/^( *)(:) ( *\\S.*)$/', $line, $m)) {
454        $openlist = 2;
455        if($para) {
456          $para = false;
457          $lines2[] = $snippets['paraclose'];
458        }
459
460        list(, $spaces, $type, $text) = $m;
461        $text = $this->run_inline($text);
462        $upped = 0;
463
464        while(count($ListLevels)>0) {
465          list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
466
467          ## close previous list
468          if($plevel>=0 && strcmp($spaces, $pspaces)<0) {
469            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"] . $this->sp($plevel).$snippets["{$ptype}listclose"];
470            $upped ++;
471            continue;
472          }
473          if($upped) $pspaces = $spaces;
474          $ListLevels[] = array($plevel, $pspaces, $ptype); # restore
475
476          ## open list
477          if($plevel<0 || strcmp($spaces, $pspaces)>0) { # new list/sublist
478            $level = $plevel+1;
479            $listcontent .= "\n" . $this->sp($level). $snippets["{$type}listopen"];
480            $ListLevels[] = array($level, $spaces, $type);
481          }
482          else { ## close prev item
483            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"];
484            $level = $plevel;
485            if($ptype!=$type) {
486              $listcontent .= "\n".$this->sp($level).$snippets["{$ptype}listclose"]
487                             ."\n".$this->sp($level).$snippets["{$type}listopen"];
488              $ListLevels[count($ListLevels)-1][2] = $type; # restore
489            }
490          }
491          ## open item
492          $listcontent .= $this->sp($level).$snippets["{$type}itemopen"];
493
494          ## fill content
495          $listcontent .= $text . $snippets["{$type}itemmiddle"];
496
497          break;
498        }
499        continue;
500      }
501      elseif($openlist) {
502        if(trim($line)=='' || strpos($line, "\032\032")===0 ) {
503          $openlist --;
504          if(!$openlist || strpos($line, "\032\032")===0) { # second empty line, close ALL
505            while(count($ListLevels)>1) {
506              list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
507              $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"]
508                                  .$this->sp($plevel).$snippets["{$ptype}listclose"];
509            }
510            $lines2[] = $this->Keep($listcontent);
511            $lines2[] = $line;
512            $listcontent = "";$openlist=0;
513            continue;
514          }
515          else {
516            list($plevel, ,) = $ListLevels[count($ListLevels)-1];
517            $listcontent .= $this->sp($plevel+1)."{$snippets["paraopen"]}{$snippets["paraclose"]}\n";
518          }
519        }
520        else {
521          list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
522          if(preg_match('/^(( *)([+-:])) *$/', $line, $m) && $m[1]== "$pspaces$ptype") { # close
523            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"]
524                              .  $this->sp($plevel).$snippets["{$ptype}listclose"];
525            if($plevel) $openlist = 2;
526            else {
527              $lines2[] = $this->Keep($listcontent);
528              $listcontent = "";$openlist=0;
529              continue;
530            }
531          }
532          else {
533            if($openlist==1)
534              $listcontent .= $this->sp($plevel+1)."{$snippets["paraopen"]}{$snippets["paraclose"]}\n";
535            $openlist = 2;
536            $ListLevels[] = array($plevel, $pspaces, $ptype);
537            $listcontent .= $this->sp($plevel+1).$this->run_inline($line)."\n";
538          }
539        }
540        continue;
541      }
542
543      if(preg_match("/^[\032\033]{2}/", $line)) {
544        if($para) {
545          $para = false;
546          $lines2[] = $snippets['paraclose'];
547        }
548        $lines2[] = $line;
549        continue;
550      }
551
552      if($para) {
553        if(trim($line)=='') {
554          $para = false;
555          $lines2[] = $snippets['paraclose'];
556        }
557        else {
558          $lines2[] = $this->run_inline($line);
559        }
560        continue;
561      }
562      # $para = false;
563      if(trim($line)!='') {
564        $lines2[] = $snippets['paraopen'];
565        $lines2[] = $this->run_inline($line);
566        $para = true;
567        continue;
568      }
569
570      $lines2[] = $line;
571    }
572    # close ALL
573    if($blockquote)
574      $lines2[] = $this->Keep($blockquotecontent . $this->fixblockquote($blockquote, 0));
575    if($openlist) {
576      while(count($ListLevels)>1) {
577        list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
578        $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"]
579                            .$this->sp($plevel).$snippets["{$ptype}listclose"];
580      }
581      $lines2[] = $this->Keep($listcontent);
582    }
583    if($para) {
584      $para = false;
585      $lines2[] = $snippets['paraclose'];
586    }
587
588    return $lines2;
589  }
590
591  function run_inline($line) { # inline transformations (links, images, bold, mono...)
592    $snippets = $this->snippets;
593    # inline Raw, Mono, Tagged
594    if(preg_match_all('/(\'|"|`){2}([^\\s](.*?[^\\s])?\\1*)\\1\\1/', $line, $m, PREG_SET_ORDER)) {
595      foreach($m as $a) {
596        $type = $a[1].$a[1];
597        $c = $this->PSS($a[2]);
598        $tmp = $this->closeRTV($type, $c);
599        $line = preg_replace('/(\'|"|`){2}([^\\s](.*?[^\\s])?\\1*)\\1\\1/', $tmp, $line, 1);
600      }
601    }
602    # macros
603    $line = $this->run_macros($line);
604
605    # <[img]>
606    $imgrx = "\\[([\034\\w_,.+%$#@!?+~\\/-]+\\.(?:png|jpe?g|gif|bmp))\\]";
607
608    $line = preg_replace("/^$imgrx(?=.)/ei",
609      "\$this->Keep(sprintf(\$snippets['img'], 'left', '$1'))", $line);
610    $line = preg_replace("/(?<=.)$imgrx$/ei",
611      "\$this->Keep(sprintf(\$snippets['img'], 'right', '$1'))", $line);
612
613
614    $line = preg_replace("/$imgrx/ei",
615      "\$this->Keep(sprintf(\$snippets['img'], 'middle', '$1', 'middle'))", $line);
616
617    $UEX = '<>"{}|\\\\^`()\\[\\]\''; # UrlExcludeChars
618    $PRT = '(?:https?|ftp|news|telnet|gopher|wais|mailto):';
619
620    if ($this->enablehotlinks == 0) {
621		$Links = array(
622		  "{$PRT}[^\\s$UEX]+" =>'',
623		  "www\\d?\\.[^\\s$UEX]+" =>'http://', # lazy links
624		  "ftp\\d?\\.[^\\s$UEX]+" =>'ftp://',  # lazy links
625		  "\\w[\\w.-]+@[\\w-.]+[^\\s$UEX]+" =>'mailto:',  # lazy links
626			); #
627    }
628    else {
629		$Links = array(
630		  //"{$PRT}[^\\s$UEX]+" =>'',  # allows hotlinks by disabling this part
631		  //"www\\d?\\.[^\\s$UEX]+" =>'http://', # lazy links won't work here
632		  "ftp\\d?\\.[^\\s$UEX]+" =>'ftp://',  # lazy links
633		  "\\w[\\w.-]+@[\\w-.]+[^\\s$UEX]+" =>'mailto:',  # lazy links
634			); #
635	}
636
637    # [txt link], [txt #anchor]
638    foreach($Links as $k=>$v) {
639      $line = preg_replace("/\\[([^\\]]+?) +($k)\\]/ei",
640        "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$v$2'), \$this->esc('$1', 1)))", $line);
641    }
642    # local links
643    $line = preg_replace("/\\[([^\\]]+?) +([^\\s$UEX]+)\\]/ei",
644      "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$2'), \$this->esc('$1', 1)))", $line);
645
646    # free links www.link, e@mail, http://link
647    foreach($Links as $k=>$v) {
648      if($v=='mailto:' && $this->maskemail) {
649        $line = preg_replace("/\\b({$k}[^\\s.,?!$UEX])/ei",
650          "\$this->Keep('&lt;' . str_replace(array('@', '.'), array(' (a) ', ' '), '$1') . '&gt;$2')", $line);
651      }
652      else {
653        $line = preg_replace("/\\b({$k}[^\\s.,?!$UEX])/ei",
654          "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$v$1'), \$this->esc('$1')))", $line);
655	  }
656    }
657
658    $line = $this->esc($line);
659
660    # Bold, Italic, Underline, Strike
661    $b = array('*', '/', '_', '-');
662    foreach($b as $c) {
663      $q = preg_quote($c, '/');
664      $line = preg_replace("/($q){2}([^\s](?:.*?\\S)?\\1*)\\1\\1/e",
665        "sprintf(\$snippets['$c$c'], \$this->PSS('$2'))", $line);
666    }
667    return $line;
668  }
669
670  function set_macros($input, $isfile = 0) {
671    $this->date=time();
672    if($isfile && file_exists($input)) {
673      $this->mtime = filemtime($input);
674      $this->infile = $this->fileattr($input);
675    }
676    else {
677      $this->mtime = time();
678      $this->infile = $this->fileattr('-');
679    }
680    $this->outfile = $this->fileattr('-');
681  }
682  function run_macros($line) {
683    $line = preg_replace('/%%(date|mtime)(\\((.+?)\\))?/ie',
684      'strftime("$2"? $this->PSS("$3"):"%Y%m%d", $this->$1)', $line);
685    $line = preg_replace('/%%infile(?:\\((.*?)\\))?/ie',
686      '"$1" ? str_replace(array_keys($this->infile), array_values($this->infile), "$1")
687      : $this->infile["%f"]', $line);
688    $line = preg_replace('/%%outfile(?:\\((.*?)\\))?/ie',
689      '"$1" ? str_replace(array_keys($this->outfile), array_values($this->outfile), "$1")
690      : $this->outfile["%f"]', $line);
691    /*$line = preg_replace_callback('/%%rand\([0-9]+,[0-9]+\)/',  'create_function(return(rand($1,$2);))', $line);
692     $line = preg_replace('/%%rand\([0-9]+,[0-9]+\)/i', '<? rand($1,$2); ?>', $line);
693    $line = preg_replace_callback('/%%rand\\(([0-9]+),([0-9]+)\\)/',  'return(rand($1,$2);)', $line);
694*/
695    return $line;
696  }
697
698  function fixblockquote(&$prev, $curr) { # close open blocks, open sub-blocks
699    $s = $this->snippets;
700    $x = '';
701    while ($prev<$curr) $x .= str_repeat("\t", ++$prev) . $s['blockquoteopen'] . "\n";
702    while ($prev>$curr) $x .= str_repeat("\t", $prev--) . $s['blockquoteclose'] . "\n";
703    return $x;
704  }
705
706  function closeRTV(&$type, &$x) { # Raw, Tagged or Verbadim lines/areas
707    switch($type{0}) {
708      case '%': $type = $x = ''; return '';
709      case "'":
710		if ($this->enabletagged == 1) {
711			$y = $x;
712			break;
713		}
714
715      case '"': # raw
716		if ($this->enableraw == 1) {
717			$y = $this->esc($x);
718			break;
719		}
720      case '`': # verbatim, mono
721		if ($this->enableverbatim == 1) {
722			$s = $this->snippets;
723			$fmt = (strlen($type)==2) ? $s['mono'] : $s['verbatim'];
724			$y = sprintf($fmt, $this->esc($x));
725			break;
726		}
727		else {
728			$y = $this->esc($x);
729		}
730    }
731    $block = (strlen($type)==3) ? "\033\033" : '';
732    $type = $x = '';
733    return $block. $this->Keep($y);
734  }
735  function PSS($x) { return str_replace('\\"','"',$x); } # Strip RegExp slashes
736  function Keep($x) { # preserves a string from being processed by wiki markups
737    $x = $this->Restore($x);
738    $this->KPCount++; $this->KPV[$this->KPCount]=$x;
739    return "\034\034{$this->KPCount}\034\034";
740  }
741  function Restore($x) { # recovers all hidden strings
742    return preg_replace("/\034\034(\\d.*?)\034\034/e", "\$this->KPV['\$1']", $x);
743  }
744  function sp($n){ # add spaces for nicer indented HTML source code
745    return str_repeat($this->snippets['listindent'], $n);
746  }
747
748  function esc($x, $pss=0) { # htmlspecialchars
749    if($pss) $x = $this->PSS($x);
750    return str_replace(
751      array('&', '<', '>', '$'),
752      array('&amp;', '&lt;', '&gt;', '&#036;'),
753      $x);
754  }
755
756  function fileattr($fname) { # variables that can be in %%infile()
757    if($fname == '-') return array(
758      '' => '-', '%f'=> '-', '%F'=> '-', '%e'=> '',
759      '%p'=> '-', '%d'=> '.', '%D'=> '.', '%%'=>'%');
760    preg_match('/\\.([^.\\/]+)$/',$fname, $m); $ext=@$m[1];
761    return array(
762      ''  => basename($fname),
763      '%f'=> basename($fname),
764      '%F'=> preg_replace('/\\.[^.]+$/', '', basename($fname)),
765      '%e'=> $m[1],
766      '%p'=>realpath($fname),
767      '%d'=>dirname(realpath($fname)),
768      '%D'=>basename(dirname(realpath($fname))),
769      '%%'=>'%');
770  }
771
772  function read($filename, $allowed = false) { # get a file content
773    if(!$allowed) return '';
774    if(!file_exists($filename)) return '';
775    return str_replace("\r", '', implode('', @file($filename)));
776  }
777
778  # get the Header, Config area and Body of a t2t file
779  # the function will include the content of any included files
780  function head_conf_body($content) {
781    $lines = explode("\n", $content);
782    $R = array();
783    $R['header'] = $R['config'] = $R['body'] = array();
784    # headers
785	if ($this->enableheaders == 0) {
786		$R['header'][0] = $R['header'][1] = $R['header'][2] = '';
787	}
788    else if($lines[0]=='') {
789      $R['header'][0] = $R['header'][1] = $R['header'][2] = '';
790      array_shift($lines);
791    }
792    else {
793      for ($i=0; $i<3 && isset($lines[0]); $i++) {
794        $R["header"][$i] = array_shift($lines);
795      }
796    }
797    # config
798    $mlcomment = false;
799    while(isset($lines[0])) {
800      $line = array_shift($lines);
801      if(rtrim($line) == '%%%') { # comment areas in Config
802        $mlcomment = ! $mlcomment;
803        continue;
804      }
805      if($mlcomment || trim($line)=='') continue;
806
807      if(preg_match('/^%!\\s*includeconf\\s*:\\s*(.+)$/', $line, $m)) {
808        $f = trim($m[1]);
809        if($f{0}!='/') $f =  $this->infile['%d'] . DIRECTORY_SEPARATOR . $f;
810        $r = $this->head_conf_body($this->read($f, $this->enableinclude));
811        for($i=count($r['config'])-1; $i>=0; $i--)
812          array_unshift($lines, $r['config'][$i]);
813        continue;
814      }
815
816      if($line{0} != '%' || preg_match('/^%(%(date|mtime|toc|infile|outfile|rand)|! *include)/i', $line)) {
817        array_unshift($lines, $line);
818        break;
819      }
820      $R["config"][] = $line;
821    }
822
823    # body
824    $mlcomment = false;
825    while(isset($lines[0])) {
826      $line = array_shift($lines);
827
828      if(rtrim($line) == '%%%') { # comment areas
829        $mlcomment = ! $mlcomment;
830        continue;
831      }
832      if($mlcomment) continue;
833
834      if(preg_match('/^%!\\s*include(?:\\(x?html\\))?\\s*:\\s*(``|\'\'|""|)(.+)\\1\\s*$/', $line, $m)) {
835
836        $f = trim($m[2]);
837        if($f{0}!='/') $f =  $this->infile['%d'] . DIRECTORY_SEPARATOR . $f;
838        $r = $this->head_conf_body($this->read($f, $this->enableinclude));
839
840        if($m[1]) {
841          $q = implode("\n", $r['body'])."\n";
842          $type = str_repeat($m[1]{1}, 3);
843          $line = $this->closeRTV($type, $q);
844        }
845        else {
846          for($i=count($r['body'])-1; $i>=0; $i--)
847            array_unshift($lines, $r['body'][$i]);
848          continue;
849        }
850      }
851      $R['body'][] = $line;
852    }
853    return $R;
854  }
855}
856
857