<?php $T2TVersion = "20121206";
/**
  txt2tags.class.php
  Written by (c) Petko Yotov 2012 www.pmwiki.org/Petko
  Development sponsored by Eric Forgeot.
  
  txt2tags is a lightweight markup language created by 
  Aurelio Jargas and written in Python (www.txt2tags.org).
  
  This class here attempts to transpose some of the features 
  of the Python convertor to the PHP language.
  
  Most of this script was written by Petko Yotov except:
  - functions PSS(), Keep(), Restore(), RegExp $UEX based on 
    the PmWiki engine by Patrick R. Michaud www.pmichaud.com
  - the RegExp $imgrx based on txt2tags.py by Aurelio Jargas
  
  This text is Free software; you can redistribute it and/or
  modify it under the terms of the GNU General Public License
  as published by the Free Software Foundation; either version 3
  of the License, or (at your option) any later version.
  See http://www.gnu.org/copyleft/gpl.html for full details 
  and lack of warranty.
  
  
  == How to use this script ==
  
  This class should be included from other scripts, for example:
  
  # load the class
  require_once('txt2tags.class.php');
  
  ## initialize a new T2T object: two ways
  
  # either with an existing disk file:
  $x = new T2T("test.t2t", true);
  
  # or with a variable containing the entire T2T markup:
  $x = new T2T($page["text"]);
  
  # optional: some settings, after new/init, before go()
  $x->enabletoc = 1;
  $x->enableinclude = 1;
  $x->snippets['**'] = "<strong>%s</strong>"; # instead of <b>
  
  # run all processing
  $x->go();
  
  # get the complete HTML output including <head>
  $html = $x->fullhtml;
  
  # alternatively, get some other variables:
  $body = $x->bodyhtml;     # only the part inside the <body> tags
  $fullconfig = $x->config; # what was in the "config" area of the file?
  
  
  == Notes ==
  
  - This is an early public release, ready to be tested.
  - Including disk files is disabled out of the box, because of 
    potential vulnerabilities. Enabling it on systems where
    untrusted people could edit the t2t content is not recommended.
    A future version may allow the inclusion of files from the 
    current directory or only with *.t2t extension.
    To enable inclusions, use $x->enableinclude = 1;

*/

class T2T {
  # these variables could be read or forced
  var $title = '';       # the document title
  var $content = '';     # the content of the t2t file
  var $headers = '';     # the first 3 lines of the t2t file
  var $config = '';      # the full config area, including ext.ref.
  var $bodytext = '\n\n';    # the full body text after inclusions
  var $bodyhtml = '';    # the innerHTML of the body of the output, no <html>...<head>
  var $fullhtml = '';    # the full <html>...</html> output
  var $enabletoc = 0;    # automatically enabled if %%toc or %!options: --toc
  var $enableinclude = 0;# allow file inclusions
  var $maxtoclevels = 5; # h1-h? titles go into toc, same as %!options: --toc-level 1
  var $mtime;            # last modified timestamp of the input file
  var $date;             # timestamp of the current date
  var $cssfile = '';     # the css file to be included in the HTML header
  var $maskemail = 0;    # rewrite plaintext e-mail links
  var $encoding = "UTF-8";             # assume default encoding if none in file
  var $parsetargets = "html|xhtml";    # accept %!command(html) and %!command(xhtml)
  var $snippets = array(
  'header1'         => "<h1>%s</h1>\n",# text (first line of file)
  'header2'         => "<h2>%s</h2>\n",# text
  'header3'         => "<h3>%s</h3>\n",# text
  'headerwrap'      => "<div style='text-align:center;'>\n%s</div>\n",# headers
  'title'           => '<h%d id="%s">%s</h%1$d>',# level, id, text
  'hrule'           => '<hr class="%s"/>', # light|heavy
  'verbatim'        => "<pre>\n%s</pre>", # content
  'mono'            => '<code>%s</code>', # content
  'center'          => "<center>%s</center>", # content
  'img'             => '<img align="%s" src="%s" border="0" alt=""/>', # align, url
  'link'            => '<a href="%s">%s</a>', # url, text
  'cssfile'         => '<link rel="stylesheet" href="%s" type="text/css"/>',
  
  '**'              => '<b>%s</b>', # bold content
  '//'              => '<i>%s</i>', # italics content
  '__'              => '<u>%s</u>', # underlined content
  '--'              => '<s>%s</s>', # striked content
  
  'tableopen'       => "<table %s cellpadding=\"4\">\n", # align, border
  'tableclose'      => "</table>\n",
  'tablerow'        => " <tr>\n%s </tr>\n", # tagged cells
  'tablehead'       => "  <th%s>%s</th>\n", # align, content
  'tablecell'       => "  <td%s>%s</td>\n", # align, content
  
  # special
  'blockquoteopen'  => '<blockquote>',
  'blockquoteclose' => '</blockquote>',
  'paraopen'        => '<p>',
  'paraclose'       => '</p>',
  
  '+listopen'       => "<ol>\n", 
  '+listclose'      => "</ol>\n", 
  '+itemopen'       => "<li>", 
  '+itemmiddle'     => "", 
  '+itemclose'      => "</li>\n",
  
  '-listopen'       => "<ul>\n", 
  '-listclose'      => "</ul>\n", 
  '-itemopen'       => "<li>", 
  '-itemmiddle'     => "", 
  '-itemclose'      => "</li>\n",
  
  ':listopen'       => "<dl>\n", 
  ':listclose'      => "</dl>\n", 
  ':itemopen'       => "<dt>", 
  ':itemmiddle'     => "</dt><dd>\n", 
  ':itemclose'      => "</dd>\n",
  'listindent'      => '   ', # nicer HTML output
  
  # title, encoding, version, styles, body
  'html'            => '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>%s</title>
<meta http-equiv="Content-Type" content="text/html; charset=%s"/>
<meta name="generator" content="txt2tags.class.php version %s"/>
%s
</head>
<body bgcolor="white" text="black">
%s
</body></html>');
  # used internally
  var $infile = array();
  var $outfile = array();
  var $preproc = array();
  var $postproc = array();
  var $KPV = array();
  var $KPCount = 0;
  var $csslink = '';
  function T2T($input, $isfile = 0) {    
    
    $this->set_macros($input, $isfile);     # macros
    # get content
    $this->content = $isfile 
      ? $this->read($input, true) 
      : str_replace("\r", '', $input);
    
    # get header, config, body
    $this->R = $this->head_conf_body($this->content);
    
    # public variables 
    $this->headers  = implode("\n", $this->R['header']);
    $this->config   = implode("\n", $this->R['config']);
    $this->bodytext = implode("\n", $this->R['body']);
  }
  
  function go($output = '') { # run the full processing
    
    $this->parse_config($this->R['config']); # read config settings
    $lines = $this->run_preproc($this->R['body']); # run %!preproc replacements
    
    # strip comments, identify areas and titles, tables and horizontal rules (bars)
    $lines = $this->firstpass($lines); 
    
    # second pass 
    $lines = $this->secondpass($this->R['header'], $lines);
    
    # restore
    $body = implode("\n", $lines);
    $body = $this->Restore($body);
    $body = str_replace(array("\032\032", "\033\033"), '', $body);
    
    # public variables 
    $this->title = $this->esc($this->run_macros($this->R['header'][0]));
    $this->bodyhtml = $this->run_postproc($body); # %!postproc replacements
    $html = sprintf($this->snippets['html'], $this->title, $this->encoding, 
      $GLOBALS['T2TVersion'], $this->csslink, $body);
    $this->fullhtml = $this->run_postproc($html); # %!postproc replacements
    
    if($output=='body') return $this->bodyhtml;
    if($output=='html') return $this->fullhtml;
    return;
  }
  
  function parse_config($lines) { # try to read the supported configuration options
    $opts = "encoding|style|postproc|preproc|options";
    $tgts = $this->parsetargets;
    foreach($lines as $c){
      if(preg_match("/^%!\\s*({$opts})(?:\\((?:{$tgts})\\))?\\s*:\\s*(.*)$/i", $c, $m)) {
        # options, preproc and postproc are cumulative
        list(, $setting, $val) = $m;
        switch(strtolower($setting)) {
          case "encoding" : $this->encoding = trim($val); break;
          case "style" : $this->csslink = sprintf($this->snippets['cssfile'], trim($val)); 
          case "preproc"  : $this->preproc[]  = $this->add_proc($setting, trim($val)); break;
          case "postproc" : $this->postproc[] = $this->add_proc($setting, trim($val)); break;
          case "options": 
            if(strpos(" $val", '--mask-email')) $this->maskemail = 1;
            if(preg_match('/--toc(?!-)/', $val)) $this->enabletoc = 1;
            if(preg_match('/--toc-level[= ]+([1-5])/', $val, $n)) $this->maxtoclevels = $n[1];
            if(preg_match('/--encoding[= ]+(\\S+)/', $val, $n)) $this->encoding = $n[1];
            if(preg_match('/--style[= ]+(\\S+)/', $val, $n)) 
              $this->csslink = sprintf($this->snippets['cssfile'], trim($val)); 
            break; 
        }
      }
    }
  }
  
  function add_proc($when, $x) { # add a pre-processor or a postprocessor
    if(! preg_match("/^ *(\"[^\"]+\"|'[^']+'|\\S+)\\s*(.*)$/", $x, $m)) return;
    
    $s = preg_replace('/^("|\')(.*?)\\1$/', '$2', trim($m[1]));
    $r = preg_replace('/^("|\')(.*?)\\1$/', '$2', trim(@$m[2]));
    $r = preg_replace('/\\\\(?=[0-9])/', '$', $r);
    $r = str_replace(array('\\n', '\\t'), array("\n", "\t"), $r);
    return array("\032$s\032m", $r);
  
  }
  function run_preproc($lines) { # make the replacements
    $body = implode("\n", $lines);
    foreach($this->preproc as $a) {
      $body = preg_replace($a[0], $a[1], $body);
    }
    return explode("\n", $body);
  }
  function run_postproc($body) { # make the replacements
    foreach($this->postproc as $a) {
      $body = preg_replace($a[0], $a[1], $body);
    }
    return $body; # end of processing
  }

  function firstpass($lines) { # strip comments, identify areas, titles, toc
    $snippets = $this->snippets;
    $toc = array();
    $postoc = 0;
    
    $lines2 = array();
    $openarea = '';
    $areacontent = '';
    $toccnt = 0;
    $tocnbs = array(0, 0, 0, 0, 0, 0);
    $table = '';
    foreach($lines as $line) {
    
      # special areas raw, tagged, verbatim, comments
      if($openarea) {
        if($line == $openarea) { # close area
          if(rtrim($line) != "%%%") # comment areas
            $lines2[] = $this->closeRTV($openarea, $areacontent);
        }
        else { # fill area
          $areacontent .= "$line\n";
        }
        continue;
      }
      if(preg_match('/^("""|```|\'\'\'|%%%) *$/', $line)) { # open area
        $openarea = trim($line);
        continue;
      }
      
      if($line!='' && $line{0}=='%' && ! preg_match('/^%%(infile|outfile|date|mtime|toc\\s*$)/i', $line) )
        continue; # remove comment lines
        
      # special lines raw, tagged, verbatim
      if(preg_match('/^("""|```|\'\'\') /', $line, $m)) { 
        $lines2[] = $this->closeRTV($m[1], substr($line, 4));
        continue;
      }
    
      # Title, Numbered Title 
      if(preg_match('/^ *((=){1,5})(?!=)\\s*(\\S.*[^=]|[^\\s=])\\1(?:\\[([\\w-]+)\\])?\\s*$/', $line, $m) ||
        preg_match('/^ *((\\+){1,5})(?!\\+)\\s*(\\S.*[^+]|[^\\s=])\\1(?:\\[([\\w-]+)\\])?\\s*$/', $line, $m)  ) {
        $toccnt++;
        $anchor = @$m[4] ? $m[4] : "toc$toccnt";
        $txt = $this->esc(trim($m[3]));
        $level = strlen($m[1]);
        
        if($m[2]=='+') {
          if($level>$tocnbs[0])$tocnbs[$level] = 1;
          else $tocnbs[$level]++;
          if($level<$tocnbs[0])for($i=$level+1; $i<6; $i++) $tocnbs[$i] = 0;
        
          $prefix = implode(".", array_slice($tocnbs, 1, $level));
          $prefix = preg_replace('/^(0\\.)+/', '', $prefix);
          $txt = "$prefix. $txt";
          $tocnbs[0] = $level;
        }
        
        $txt = $this->Keep($txt);
        $lines2[] = "\032\032".sprintf($snippets['title'], $level, $anchor, $txt); # \032: block that cannot be nested in lists
        
        # collect toc entries
        if($this->maxtoclevels>=$level)
          $toc[] = $this->sp($level) . "- [$txt #$anchor]";
        
        continue;
      }
      # tables
      if(preg_match('/^ *(\\|\\|?) /', $line, $m)) {
        if(!$table) { # open table
          $attr = ($line{0}==' ')? ' align="center"' : "";
          if(preg_match('/\\|\\s*$/', $line)) {
            $attr .=  ' border="1"';
          }
          
          $table = sprintf($snippets['tableopen'], $attr);
        }
        # fill table
        if($m[1]=='||') $fmt = $snippets['tablehead'];
        else $fmt = $snippets['tablecell'];
        
        $line = $this->run_inline($line);
        
        $row = substr($line, strlen($m[0]));
        
        if(! preg_match('/\\|\\s*$/', $row)) $row .= ' | ';
        $m = preg_split('/( \\|+(?: |$))/', $row, -1, PREG_SPLIT_DELIM_CAPTURE);

        $cells = '';
        for($i=1; $i<count($m); $i+=2){
          $c = $m[$i-1];
          $attr = '';
          if($c && $c{0}==' ') {
            $attr = (substr($c, -1)==' ') ? ' align="center"' : ' align="right"';
          }
          $span = strlen(trim($m[$i]));
          if($span>1) $attr .= " colspan=\"$span\"";
          $cells .= sprintf($fmt, $attr, $c);
        }
        $table .= sprintf($snippets['tablerow'], $cells);
        continue;
      }
      elseif($table) { # close table
        $lines2[] = "\033\033". $this->Keep($table . $snippets['tableclose']); # \033: block that can be nested in lists
        $table = '';
      }
      # horizontal rule (bar1, bar2)
      if(preg_match('/^ *(=|-|_){20,}\\s*$/', $line, $m)) {
        $class = $m[1] == "=" ? 'heavy':'light';
        $lines2[] = "\032\032".$this->Keep(sprintf($snippets['hrule'], $class));
        continue;
      }
      if(preg_match("/^ +\\[([\034\\w_,.+%$#@!?+~\\/-]+\\.(?:png|jpe?g|gif|bmp))\\] +$/i", $line)) {
        $lines2[] = "\033\033". $this->Keep(sprintf($snippets['center'], $this->run_inline($line)));
        continue;
      }
      
      if(trim(strtolower($line))=='%%toc') {
        $this->enabletoc = $postoc = 1;
        $line = '%%toc';
      }
      $lines2[] = $line;
    } # end foreach line
    
    # close ALL
    if($openarea && $openarea != '%%%') # close all areas
      $lines2[] = $this->closeRTV($openarea, $areacontent);
    
    if($table) 
      $lines2[] = "\033\033". $this->Keep($table . $snippets['tableclose']."\n");
      
    if($this->enabletoc && count($toc)) {
      if($postoc) { # there is %%toc in the page
        array_unshift($toc, "\032\032");
        $toc[] = "\032\032";
        foreach($lines2 as $k=>$v) {
          if($v=='%%toc') array_splice($lines2, $k, 1, $toc);
        }
      }
      else { # before the body: bar, toc-list, bar
        $bar = "\032\032".$this->Keep(sprintf($snippets['hrule'], 'light'));
        array_unshift($toc, $bar);
        $toc[] = $bar;
        
        $lines2 = array_merge($toc, $lines2);
      }
    }
    return $lines2;
  }
  
  function secondpass($headers, $lines) { # all other marks
    $snippets = $this->snippets;
    
    $lines2 = array();
    $html = '';
    for($i=0; $i<3; $i++) {
      $j = $i+1;
      $h = $this->esc($headers[$i]);
      if($h) $html .= sprintf($snippets["header$j"], $this->run_macros($h));
    }
    if($html) $lines2[] = $this->Keep(sprintf($snippets["headerwrap"], $html));
      
    $blockquote = 0;
    $blockquotecontent = '';
    $para = false;
    $openlist = 0;
    $listcontent = '';
    $ListLevels = array(array(-1, '', '')); # $level, $spaces, $type

    foreach($lines as $line) {
    
      # blockquote
      if(preg_match('/^(\\t+)([^\\t].*)$/', $line, $m)) {
        if($para) { 
          $para = false;
          $lines2[] = $snippets['paraclose'];
        }
        $level = strlen($m[1]);
        $blockquotecontent .= $this->fixblockquote($blockquote, $level) 
          . $m[1]. $this->run_inline($m[2])."\n";
        continue;
      }
      elseif($blockquote) { # close bq
        $lines2[] = "\032\032".$blockquotecontent . $this->fixblockquote($blockquote, 0);
        $blockquotecontent = '';
      }
      
      # List, Numbered List, Definition List
      if(preg_match('/^( *)([+-]) (\\S.*)$/', $line, $m) || 
        preg_match( '/^( *)(:) ( *\\S.*)$/', $line, $m)) {
        $openlist = 2;
        if($para) { 
          $para = false;
          $lines2[] = $snippets['paraclose'];
        }
        
        list(, $spaces, $type, $text) = $m;
        $text = $this->run_inline($text);
        $upped = 0;
                
        while(count($ListLevels)>0) {
          list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
          
          ## close previous list
          if($plevel>=0 && strcmp($spaces, $pspaces)<0) {
            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"] . $this->sp($plevel).$snippets["{$ptype}listclose"];
            $upped ++;
            continue;
          }
          if($upped) $pspaces = $spaces;
          $ListLevels[] = array($plevel, $pspaces, $ptype); # restore
          
          ## open list
          if($plevel<0 || strcmp($spaces, $pspaces)>0) { # new list/sublist
            $level = $plevel+1;
            $listcontent .= "\n" . $this->sp($level). $snippets["{$type}listopen"];
            $ListLevels[] = array($level, $spaces, $type);
          }
          else { ## close prev item
            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"];
            $level = $plevel;
            if($ptype!=$type) {
              $listcontent .= "\n".$this->sp($level).$snippets["{$ptype}listclose"]
                             ."\n".$this->sp($level).$snippets["{$type}listopen"];
              $ListLevels[count($ListLevels)-1][2] = $type; # restore
            }
          }
          ## open item
          $listcontent .= $this->sp($level).$snippets["{$type}itemopen"];
          
          ## fill content
          $listcontent .= $text . $snippets["{$type}itemmiddle"];
          
          break;
        }
        continue;
      }
      elseif($openlist) {
        if(trim($line)=='' || strpos($line, "\032\032")===0 ) {
          $openlist --;
          if(!$openlist || strpos($line, "\032\032")===0) { # second empty line, close ALL
            while(count($ListLevels)>1) {
              list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
              $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"]
                                  .$this->sp($plevel).$snippets["{$ptype}listclose"];
            }
            $lines2[] = $this->Keep($listcontent);
            $lines2[] = $line;
            $listcontent = "";$openlist=0;
            continue;
          }
          else {
            list($plevel, ,) = $ListLevels[count($ListLevels)-1];
            $listcontent .= $this->sp($plevel+1)."{$snippets["paraopen"]}{$snippets["paraclose"]}\n";
          }
        }
        else {
          list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
          if(preg_match('/^(( *)([+-:])) *$/', $line, $m) && $m[1]== "$pspaces$ptype") { # close 
            $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"] 
                              .  $this->sp($plevel).$snippets["{$ptype}listclose"];
            if($plevel) $openlist = 2;
            else {
              $lines2[] = $this->Keep($listcontent);
              $listcontent = "";$openlist=0;
              continue;
            }
          }
          else {
            if($openlist==1) 
              $listcontent .= $this->sp($plevel+1)."{$snippets["paraopen"]}{$snippets["paraclose"]}\n";
            $openlist = 2;
            $ListLevels[] = array($plevel, $pspaces, $ptype);
            $listcontent .= $this->sp($plevel+1).$this->run_inline($line)."\n";
          }
        }
        continue;
      }
      
      if(preg_match("/^[\032\033]{2}/", $line)) { 
        if($para) { 
          $para = false;
          $lines2[] = $snippets['paraclose'];
        }
        $lines2[] = $line;
        continue;
      }
      
      if($para) {
        if(trim($line)=='') {
          $para = false;
          $lines2[] = $snippets['paraclose'];
        }
        else {
          $lines2[] = $this->run_inline($line);
        }
        continue;
      }
      # $para = false;
      if(trim($line)!='') {
        $lines2[] = $snippets['paraopen'];
        $lines2[] = $this->run_inline($line);
        $para = true;
        continue;
      }
      
      $lines2[] = $line;
    }
    # close ALL
    if($blockquote) 
      $lines2[] = $this->Keep($blockquotecontent . $this->fixblockquote($blockquote, 0));
    if($openlist) {
      while(count($ListLevels)>1) {
        list($plevel, $pspaces, $ptype) = array_pop($ListLevels);
        $listcontent .= "\n".$this->sp($plevel).$snippets["{$ptype}itemclose"]
                            .$this->sp($plevel).$snippets["{$ptype}listclose"];
      }
      $lines2[] = $this->Keep($listcontent);
    }
    if($para) { 
      $para = false;
      $lines2[] = $snippets['paraclose'];
    }
    
    return $lines2;
  }
  
  function run_inline($line) { # inline transformations (links, images, bold, mono...)
    $snippets = $this->snippets;
    # inline Raw, Mono, Tagged
    if(preg_match_all('/(\'|"|`){2}([^\\s](.*?[^\\s])?\\1*)\\1\\1/', $line, $m, PREG_SET_ORDER)) {
      foreach($m as $a) {
        $type = $a[1].$a[1];
        $c = $this->PSS($a[2]);
        $tmp = $this->closeRTV($type, $c);
        $line = preg_replace('/(\'|"|`){2}([^\\s](.*?[^\\s])?\\1*)\\1\\1/', $tmp, $line, 1);
      }
    }
    # macros
    $line = $this->run_macros($line);
    
    # <[img]>
    $imgrx = "\\[([\034\\w_,.+%$#@!?+~\\/-]+\\.(?:png|jpe?g|gif|bmp))\\]";
    
    $line = preg_replace("/^$imgrx(?=.)/ei",
      "\$this->Keep(sprintf(\$snippets['img'], 'left', '$1'))", $line);
    $line = preg_replace("/(?<=.)$imgrx$/ei",
      "\$this->Keep(sprintf(\$snippets['img'], 'right', '$1'))", $line);
      
      
    $line = preg_replace("/$imgrx/ei",
      "\$this->Keep(sprintf(\$snippets['img'], 'middle', '$1', 'middle'))", $line);
    
    $UEX = '<>"{}|\\\\^`()\\[\\]\''; # UrlExcludeChars
    $PRT = '(?:https?|ftp|news|telnet|gopher|wais|mailto):';
    
    $Links = array(
      "{$PRT}[^\\s$UEX]+" =>'',
      "www\\d?\\.[^\\s$UEX]+" =>'http://', # lazy links
      "ftp\\d?\\.[^\\s$UEX]+" =>'ftp://',  # lazy links
      "\\w[\\w.-]+@[\\w-.]+[^\\s$UEX]+" =>'mailto:',  # lazy links
    ); # 
    
    # [txt link], [txt #anchor]
    foreach($Links as $k=>$v) {
      $line = preg_replace("/\\[([^\\]]+?) +($k)\\]/ei", 
        "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$v$2'), \$this->esc('$1', 1)))", $line);
    }
    # local links
    $line = preg_replace("/\\[([^\\]]+?) +([^\\s$UEX]+)\\]/ei", 
      "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$2'), \$this->esc('$1', 1)))", $line);
    
    # free links www.link, e@mail, http://link
    foreach($Links as $k=>$v) {
      if($v=='mailto:' && $this->maskemail) {
        $line = preg_replace("/\\b({$k}[^\\s.,?!$UEX])/ei",
          "\$this->Keep('&lt;' . str_replace(array('@', '.'), array(' (a) ', ' '), '$1') . '&gt;$2')", $line);
      }
      else
        $line = preg_replace("/\\b({$k}[^\\s.,?!$UEX])/ei", 
          "\$this->Keep(sprintf(\$snippets['link'], \$this->esc('$v$1'), \$this->esc('$1')))", $line);
    }
    
    $line = $this->esc($line);
    
    # Bold, Italic, Underline, Strike
    $b = array('*', '/', '_', '-');
    foreach($b as $c) {
      $q = preg_quote($c, '/');
      $line = preg_replace("/($q){2}([^\s](?:.*?\\S)?\\1*)\\1\\1/e", 
        "sprintf(\$snippets['$c$c'], \$this->PSS('$2'))", $line);
    }
    return $line;
  }
  
  function set_macros($input, $isfile = 0) {
    $this->date=time();
    if($isfile && file_exists($input)) {
      $this->mtime = filemtime($input);
      $this->infile = $this->fileattr($input);
    }
    else {
      $this->mtime = time();
      $this->infile = $this->fileattr('-');
    }
    $this->outfile = $this->fileattr('-');
  }
  function run_macros($line) {
    $line = preg_replace('/%%(date|mtime)(\\((.+?)\\))?/ie',  
      'strftime("$2"? $this->PSS("$3"):"%Y%m%d", $this->$1)', $line);
    $line = preg_replace('/%%infile(?:\\((.*?)\\))?/ie', 
      '"$1" ? str_replace(array_keys($this->infile), array_values($this->infile), "$1")
      : $this->infile["%f"]', $line);
    $line = preg_replace('/%%outfile(?:\\((.*?)\\))?/ie', 
      '"$1" ? str_replace(array_keys($this->outfile), array_values($this->outfile), "$1")
      : $this->outfile["%f"]', $line);
    return $line;
  }
  
  function fixblockquote(&$prev, $curr) { # close open blocks, open sub-blocks
    $s = $this->snippets;
    $x = '';
    while ($prev<$curr) $x .= str_repeat("\t", ++$prev) . $s['blockquoteopen'] . "\n";
    while ($prev>$curr) $x .= str_repeat("\t", $prev--) . $s['blockquoteclose'] . "\n";
    return $x;
  }
  
  function closeRTV(&$type, &$x) { # Raw, Tagged or Verbadim lines/areas
    switch($type{0}) {
      case '%': $type = $x = ''; return '';
      case "'": $y = $x; break;
      case '"': # raw
        $y = $this->esc($x);
        break;
      case '`': # verbatim, mono
        $s = $this->snippets;
        $fmt = (strlen($type)==2) ? $s['mono'] : $s['verbatim'];
        $y = sprintf($fmt, $this->esc($x));
        break;
    }
    $block = (strlen($type)==3) ? "\033\033" : '';
    $type = $x = '';
    return $block. $this->Keep($y);
  }
  function PSS($x) { return str_replace('\\"','"',$x); } # Strip RegExp slashes
  function Keep($x) { # preserves a string from being processed by wiki markups
    $x = $this->Restore($x);
    $this->KPCount++; $this->KPV[$this->KPCount]=$x;
    return "\034\034{$this->KPCount}\034\034";
  }
  function Restore($x) { # recovers all hidden strings
    return preg_replace("/\034\034(\\d.*?)\034\034/e", "\$this->KPV['\$1']", $x);
  }
  function sp($n){ # add spaces for nicer indented HTML source code
    return str_repeat($this->snippets['listindent'], $n);
  }
  
  function esc($x, $pss=0) { # htmlspecialchars
    if($pss) $x = $this->PSS($x);
    return str_replace(
      array('&', '<', '>', '$'),
      array('&amp;', '&lt;', '&gt;', '&#036;'),
      $x);
  }
  
  function fileattr($fname) { # variables that can be in %%infile()
    if($fname == '-') return array(
      '' => '-', '%f'=> '-', '%F'=> '-', '%e'=> '', 
      '%p'=> '-', '%d'=> '.', '%D'=> '.', '%%'=>'%');
    preg_match('/\\.([^.\\/]+)$/',$fname, $m); $ext=@$m[1];
    return array(
      ''  => basename($fname),
      '%f'=> basename($fname),
      '%F'=> preg_replace('/\\.[^.]+$/', '', basename($fname)),
      '%e'=> $m[1],
      '%p'=>realpath($fname),
      '%d'=>dirname(realpath($fname)),
      '%D'=>basename(dirname(realpath($fname))),
      '%%'=>'%');
  }
  
  function read($filename, $allowed = false) { # get a file content
    if(!$allowed) return '';
    if(!file_exists($filename)) return '';
    return str_replace("\r", '', implode('', @file($filename)));
  }
  
  # get the Header, Config area and Body of a t2t file
  # the function will include the content of any included files
  function head_conf_body($content) {
    $lines = explode("\n", $content);
    $R = array();
    $R['header'] = $R['config'] = $R['body'] = array();
    # headers 
    if($lines[0]=='') {
      $R['header'][0] = $R['header'][1] = $R['header'][2] = '';
      array_shift($lines);
    }
    else {
      for ($i=0; $i<3 && isset($lines[0]); $i++) {
        $R["header"][$i] = array_shift($lines);
      }
    }
    # config
    $mlcomment = false;
    while(isset($lines[0])) {
      $line = array_shift($lines);
      if(rtrim($line) == '%%%') { # comment areas in Config
        $mlcomment = ! $mlcomment;
        continue;
      }
      if($mlcomment || trim($line)=='') continue;
      
      if(preg_match('/^%!\\s*includeconf\\s*:\\s*(.+)$/', $line, $m)) {
        $f = trim($m[1]);
        if($f{0}!='/') $f =  $this->infile['%d'] . DIRECTORY_SEPARATOR . $f;
        $r = $this->head_conf_body($this->read($f, $this->enableinclude));
        for($i=count($r['config'])-1; $i>=0; $i--)
          array_unshift($lines, $r['config'][$i]); 
        continue;
      }
      
      if($line{0} != '%' || preg_match('/^%(%(date|mtime|toc|infile|outfile)|! *include)/i', $line)) {
        array_unshift($lines, $line); 
        break;
      }
      $R["config"][] = $line;
    }
    
    # body
    $mlcomment = false;
    while(isset($lines[0])) {
      $line = array_shift($lines);
      
      if(rtrim($line) == '%%%') { # comment areas
        $mlcomment = ! $mlcomment;
        continue;
      }
      if($mlcomment) continue;
      
      if(preg_match('/^%!\\s*include(?:\\(x?html\\))?\\s*:\\s*(``|\'\'|""|)(.+)\\1\\s*$/', $line, $m)) {
      
        $f = trim($m[2]);
        if($f{0}!='/') $f =  $this->infile['%d'] . DIRECTORY_SEPARATOR . $f;
        $r = $this->head_conf_body($this->read($f, $this->enableinclude));
      
        if($m[1]) {
          $q = implode("\n", $r['body'])."\n";
          $type = str_repeat($m[1]{1}, 3);
          $line = $this->closeRTV($type, $q);
        }
        else {
          for($i=count($r['body'])-1; $i>=0; $i--)
            array_unshift($lines, $r['body'][$i]);
          continue;
        }
      }
      $R['body'][] = $line;
    }
    return $R;
  }
}

