make doBlockLevels last parser stage again, and fix missing paragraphs from
[lhc/web/wiklou.git] / includes / Parser.php
index 61d893c..198c84d 100644 (file)
@@ -1,13 +1,60 @@
 <?php
 
+include_once('Tokenizer.php');
+
+if( $GLOBALS['wgUseWikiHiero'] ){
+       include_once('wikihiero.php');
+}
+
+# PHP Parser 
+# 
+# Processes wiki markup
+#
+# There are two main entry points into the Parser class: parse() and preSaveTransform(). 
+# The parse() function produces HTML output, preSaveTransform() produces altered wiki markup.
+#
 # Globals used: 
-#    major:  $wgUser, $wgTitle, 
-#    minor:  $wgUseTex
+#    objects:   $wgLang, $wgDateFormatter, $wgLinkCache, $wgCurParser
+#
+# NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
+#
+#    settings:  $wgUseTex*, $wgUseCategoryMagic*, $wgUseDynamicDates*, $wgInterwikiMagic*,
+#               $wgNamespacesWithSubpages, $wgLanguageCode, $wgAllowExternalImages*, 
+#               $wgLocaltimezone
+#
+#      * only within ParserOptions
+#
+#
+#----------------------------------------
+#    Variable substitution O(N^2) attack
+#-----------------------------------------
+# Without countermeasures, it would be possible to attack the parser by saving a page
+# filled with a large number of inclusions of large pages. The size of the generated 
+# page would be proportional to the square of the input size. Hence, we limit the number 
+# of inclusions of any given page, thus bringing any attack back to O(N).
+#
+define( "MAX_INCLUDE_REPEAT", 5 );
+
+# Recursion depth of variable/inclusion evaluation
+define( "MAX_INCLUDE_PASSES", 3 );
+
+# Allowed values for $mOutputType
+define( "OT_HTML", 1 );
+define( "OT_WIKI", 2 );
+define( "OT_MSG", 3 );
+
+# prefix for escaping, used in two functions at least
+define( "UNIQ_PREFIX", "NaodW29");
 
 class Parser
 {
-       var $mOutput, $mAutonumber, $mLastSection, $mDTopen;
-       
+       # Cleared with clearState():
+       var $mOutput, $mAutonumber, $mLastSection, $mDTopen, $mStripState = array();
+       var $mVariables, $mIncludeCount;
+
+       # Temporary:
+       var $mOptions, $mTitle, $mOutputType;
+
        function Parser()
        {
                $this->clearState();
@@ -19,6 +66,9 @@ class Parser
                $this->mAutonumber = 0;
                $this->mLastSection = "";
                $this->mDTopen = false;
+               $this->mVariables = false;
+               $this->mIncludeCount = array();
+               $this->mStripState = array();
        }
        
        # First pass--just handle <nowiki> sections, pass the rest off
@@ -26,162 +76,167 @@ class Parser
        #
        # Returns a ParserOutput
        #
-       function parse( $text, $linestart = true, $clearState = true )
+       function parse( $text, &$title, $options, $linestart = true, $clearState = true )
        {
-               global $wgUseTeX;
                $fname = "Parser::parse";
                wfProfileIn( $fname );
-               $unique  = "3iyZiyA7iMwg5rhxP0Dcc9oTnj8qD1jm1Sfv4";
-               $unique2 = "4LIQ9nXtiYFPCSfitVwDw7EYwQlL4GeeQ7qSO";
-               $unique3 = "fPaA8gDfdLBqzj68Yjg9Hil3qEF8JGO0uszIp";
-               $nwlist = array();
-               $nwsecs = 0;
-               $mathlist = array();
-               $mathsecs = 0;
-               $prelist = array ();
-               $presecs = 0;
-               $stripped = "";
-               $stripped2 = "";
-               $stripped3 = "";
 
                if ( $clearState ) {
                        $this->clearState();
                }
-                               
-               # Replace any instances of the placeholders
-               $text = str_replace( $unique, wfHtmlEscapeFirst( $unique ), $text );
-               $text = str_replace( $unique2, wfHtmlEscapeFirst( $unique2 ), $text );
-               $text = str_replace( $unique3, wfHtmlEscapeFirst( $unique3 ), $text );
                
-               /*
-               global $wgEnableParserCache;
-               $use_parser_cache = 
-                       $wgEnableParserCache && $action == "view" &&
-                       intval($wgUser->getOption( "stubthreshold" )) == 0 && 
-                       is_object($article) && $article->getID() > 0;
-
-               if( $use_parser_cache ){
-                       if( $this->fillFromParserCache() ){
-                               wfProfileOut( $fname );
-                               return;
-                       }
-               }
-               */
+               $this->mOptions = $options;
+               $this->mTitle =& $title;
+               $this->mOutputType = OT_HTML;
                
+               $stripState = NULL;
+               $text = $this->strip( $text, $this->mStripState );
+               $text = $this->doWikiPass2( $text, $linestart );
+               $text = $this->unstrip( $text, $this->mStripState );
+               
+               $this->mOutput->setText( $text );
+               wfProfileOut( $fname );
+               return $this->mOutput;
+       }
+
+       /* static */ function getRandomString()
+       {
+               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+       }
+
+       # Replaces all occurences of <$tag>content</$tag> in the text
+       # with a random marker and returns the new text. the output parameter
+       # $content will be an associative array filled with data on the form
+       # $unique_marker => content.
+
+       /* static */ function extractTags($tag, $text, &$content, $uniq_prefix = ""){
+               $result = array();
+               $rnd = $uniq_prefix . '-' . $tag . Parser::getRandomString();
+               $content = array( );
+               $n = 1;
+               $stripped = "";
+
                while ( "" != $text ) {
-                       $p = preg_split( "/<\\s*nowiki\\s*>/i", $text, 2 );
+                       $p = preg_split( "/<\\s*$tag\\s*>/i", $text, 2 );
                        $stripped .= $p[0];
-                       if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $text = ""; }
-                       else {
-                               $q = preg_split( "/<\\/\\s*nowiki\\s*>/i", $p[1], 2 );
-                               ++$nwsecs;
-                               $nwlist[$nwsecs] = wfEscapeHTMLTagsOnly($q[0]);
-                               $stripped .= $unique . $nwsecs . "s";
+                       if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { 
+                               $text = ""; 
+                       } else {
+                               $q = preg_split( "/<\\/\\s*$tag\\s*>/i", $p[1], 2 );
+                               $marker = $rnd . sprintf("%08X", $n++);
+                               $content[$marker] = $q[0];
+                               $stripped .= $marker;
                                $text = $q[1];
                        }
                }
+               return $stripped;
+       }       
 
-               if( $wgUseTeX ) {
-                       while ( "" != $stripped ) {
-                               $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
-                               $stripped2 .= $p[0];
-                               if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped = ""; }
-                               else {
-                                       $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
-                                       ++$mathsecs;
-                                       $mathlist[$mathsecs] = renderMath($q[0]);
-                                       $stripped2 .= $unique2 . $mathsecs . "s";
-                                       $stripped = $q[1];
-                               }
-                       }
-               } else {
-                       $stripped2 = $stripped;
-               }
-
-               while ( "" != $stripped2 ) {
-                       $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
-                       $stripped3 .= $p[0];
-                       if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { $stripped2 = ""; }
-                       else {
-                               $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
-                               ++$presecs;
-                               $prelist[$presecs] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>\n";
-                               $stripped3 .= $unique3 . $presecs . "s";
-                               $stripped2 = $q[1];
+       # Strips <nowiki>, <pre> and <math>
+       # Returns the text, and fills an array with data needed in unstrip()
+       #
+       function strip( $text, &$state )
+       {
+               $render = ($this->mOutputType == OT_HTML);
+               $nowiki_content = array(); 
+               $hiero_content = array();
+               $math_content = array();
+               $pre_content = array();
+
+               # Replace any instances of the placeholders
+               $uniq_prefix = UNIQ_PREFIX;
+               $text = str_replace( $uniq_prefix, wfHtmlEscapeFirst( $uniq_prefix ), $text );
+
+               $text = Parser::extractTags("nowiki", $text, $nowiki_content, $uniq_prefix);
+               foreach( $nowiki_content as $marker => $content ){
+                       if( $render ){
+                               $nowiki_content[$marker] = wfEscapeHTMLTagsOnly( $content );
+                       } else {
+                               $nowiki_content[$marker] = "<nowiki>$content</nowiki>";
                        }
                }
 
-               $text = $this->doWikiPass2( $stripped3, $linestart );
-               
-               $specialChars = array("\\", "$");
-               $escapedChars = array("\\\\", "\\$");
-
-               # Go backwards so that {$unique1}1 doesn't overwrite {$unique1}10
-               for ( $i = $presecs; $i >= 1; --$i ) {
-                       $text = preg_replace( "/{$unique3}{$i}s/", str_replace( $specialChars, 
-                               $escapedChars, $prelist[$i] ), $text );
+               if( $GLOBALS['wgUseWikiHiero'] ){
+                       $text = Parser::extractTags("hiero", $text, $hiero_content, $uniq_prefix);
+                       foreach( $hiero_content as $marker => $content ){
+                               if( $render ){
+                                       $hiero_content[$marker] = WikiHiero( $content, WH_MODE_HTML);
+                               } else {
+                                       $hiero_content[$marker] = "<hiero>$content</hiero>";
+                               }
+                       }
                }
 
-               for ( $i = $mathsecs; $i >= 1; --$i ) {
-                       $text = preg_replace( "/{$unique2}{$i}s/", str_replace( $specialChars, 
-                               $escapedChars, $mathlist[$i] ), $text );
+               if( $this->mOptions->getUseTeX() ){
+                       $text = Parser::extractTags("math", $text, $math_content, $uniq_prefix);
+                       foreach( $math_content as $marker => $content ){
+                               if( $render ){
+                                       $math_content[$marker] = renderMath( $content );
+                               } else {
+                                       $math_content[$marker] = "<math>$content</math>";
+                               }
+                       }
                }
 
-               for ( $i = $nwsecs; $i >= 1; --$i ) {
-                       $text = preg_replace( "/{$unique}{$i}s/", str_replace( $specialChars, 
-                               $escapedChars, $nwlist[$i] ), $text );
+               $text = Parser::extractTags("pre", $text, $pre_content, $uniq_prefix);
+               foreach( $pre_content as $marker => $content ){
+                       if( $render ){
+                               $pre_content[$marker] = "<pre>" . wfEscapeHTMLTagsOnly( $content ) . "</pre>";
+                       } else {
+                               $pre_content[$marker] = "<pre>$content</pre>";
+                       }
                }
                
-               /*
-               if($use_parser_cache ){
-                       $this->saveParserCache( $text );
+               # Must expand in reverse order, otherwise nested tags will be corrupted
+               $state = array( $pre_content, $math_content, $hiero_content, $nowiki_content );
+               return $text;
+       }
+
+       function unstrip( $text, &$state )
+       {
+               foreach( $state as $content_dict ){
+                       foreach( $content_dict as $marker => $content ){
+                               $text = str_replace( $marker, $content, $text );
+                       }
                }
-               */
-               
-               $this->mOutput->setText( $text );
-               wfProfileOut( $fname );
-               return $this->mOutput;
+               return $text;
        }
 
        function categoryMagic ()
        {
-               global $wgTitle , $wgUseCategoryMagic, $wgLang ;
-               if ( !isset ( $wgUseCategoryMagic ) || !$wgUseCategoryMagic ) return ;
-               $id = $wgTitle->getArticleID() ;
-               $cat = ucfirst ( wfMsg ( "category" ) ) ;
-               $ti = $wgTitle->getText() ;
+               global $wgLang , $wgUser ;
+               if ( !$this->mOptions->getUseCategoryMagic() ) return ;
+               $id = $this->mTitle->getArticleID() ;
+               $cat = $wgLang->ucfirst ( wfMsg ( "category" ) ) ;
+               $ti = $this->mTitle->getText() ;
                $ti = explode ( ":" , $ti , 2 ) ;
                if ( $cat != $ti[0] ) return "" ;
-               $r = "<br break=all>\n" ;
+               $r = "<br break='all' />\n" ;
 
                $articles = array() ;
                $parents = array () ;
                $children = array() ;
 
 
-               global $wgUser ;
-               $sk = $wgUser->getSkin() ;
+#              $sk =& $this->mGetSkin();
+               $sk =& $wgUser->getSkin() ;
 
-               $doesexist = false ;
-               if ( $doesexist ) {
-                       $sql = "SELECT l_from FROM links WHERE l_to={$id}" ;
-               } else {
-                       $sql = "SELECT cur_title,cur_namespace FROM cur,brokenlinks WHERE bl_to={$id} AND bl_from=cur_id" ;
-               }
+               $data = array () ;
+               $sql1 = "SELECT DISTINCT cur_title,cur_namespace FROM cur,links WHERE l_to={$id} AND l_from=cur_id";
+               $sql2 = "SELECT DISTINCT cur_title,cur_namespace FROM cur,brokenlinks WHERE bl_to={$id} AND bl_from=cur_id" ;
+
+               $res = wfQuery ( $sql1, DB_READ ) ;
+               while ( $x = wfFetchObject ( $res ) ) $data[] = $x ;
 
-               $res = wfQuery ( $sql, DB_READ ) ;
-               while ( $x = wfFetchObject ( $res ) )
+               $res = wfQuery ( $sql2, DB_READ ) ;
+               while ( $x = wfFetchObject ( $res ) ) $data[] = $x ;
+
+
+               foreach ( $data AS $x )
                {
-               #  $t = new Title ; 
-               #  $t->newFromDBkey ( $x->l_from ) ;
-               #  $t = $t->getText() ;
-                       if ( $doesexist ) {
-                               $t = $x->l_from ;
-                       } else {
-                               $t = $wgLang->getNsText ( $x->cur_namespace ) ;
-                               if ( $t != "" ) $t .= ":" ;
-                               $t .= $x->cur_title ;
-                       }
+                       $t = $wgLang->getNsText ( $x->cur_namespace ) ;
+                       if ( $t != "" ) $t .= ":" ;
+                       $t .= $x->cur_title ;
 
                        $y = explode ( ":" , $t , 2 ) ;
                        if ( count ( $y ) == 2 && $y[0] == $cat ) {
@@ -213,145 +268,145 @@ class Parser
                return $r ;
        }
 
-function getHTMLattrs ()
-{
-               $htmlattrs = array( # Allowed attributes--no scripting, etc.
-                       "title", "align", "lang", "dir", "width", "height",
-                       "bgcolor", "clear", /* BR */ "noshade", /* HR */
-                       "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
-                       /* FONT */ "type", "start", "value", "compact",
-                       /* For various lists, mostly deprecated but safe */
-                       "summary", "width", "border", "frame", "rules",
-                       "cellspacing", "cellpadding", "valign", "char",
-                       "charoff", "colgroup", "col", "span", "abbr", "axis",
-                       "headers", "scope", "rowspan", "colspan", /* Tables */
-                       "id", "class", "name", "style" /* For CSS */
-               );
-return $htmlattrs ;
-}
-
-function fixTagAttributes ( $t )
-{
-       if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
-       $htmlattrs = $this->getHTMLattrs() ;
-  
-       # Strip non-approved attributes from the tag
-       $t = preg_replace(
-               "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
-               "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
-               $t);
-       # Strip javascript "expression" from stylesheets. Brute force approach:
-       # If anythin offensive is found, all attributes of the HTML tag are dropped
-
-       if( preg_match( 
-               "/style\\s*=.*(expression|tps*:\/\/|url\\s*\().*/is",
-               wfMungeToUtf8( $t ) ) )
+       function getHTMLattrs ()
        {
-               $t="";
+               $htmlattrs = array( # Allowed attributes--no scripting, etc.
+                               "title", "align", "lang", "dir", "width", "height",
+                               "bgcolor", "clear", /* BR */ "noshade", /* HR */
+                               "cite", /* BLOCKQUOTE, Q */ "size", "face", "color",
+                               /* FONT */ "type", "start", "value", "compact",
+                               /* For various lists, mostly deprecated but safe */
+                               "summary", "width", "border", "frame", "rules",
+                               "cellspacing", "cellpadding", "valign", "char",
+                               "charoff", "colgroup", "col", "span", "abbr", "axis",
+                               "headers", "scope", "rowspan", "colspan", /* Tables */
+                               "id", "class", "name", "style" /* For CSS */
+                               );
+               return $htmlattrs ;
        }
 
-       return trim ( $t ) ;
-}
-
-function doTableStuff ( $t )
-{
-  $t = explode ( "\n" , $t ) ;
-  $td = array () ; # Is currently a td tag open?
-  $ltd = array () ; # Was it TD or TH?
-  $tr = array () ; # Is currently a tr tag open?
-  $ltr = array () ; # tr attributes
-  foreach ( $t AS $k => $x )
-    {
-      $x = rtrim ( $x ) ;
-      $fc = substr ( $x , 0 , 1 ) ;
-      if ( "{|" == substr ( $x , 0 , 2 ) )
+       function fixTagAttributes ( $t )
        {
-         $t[$k] = "<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
-         array_push ( $td , false ) ;
-         array_push ( $ltd , "" ) ;
-         array_push ( $tr , false ) ;
-         array_push ( $ltr , "" ) ;
-       }
-      else if ( count ( $td ) == 0 ) { } # Don't do any of the following
-      else if ( "|}" == substr ( $x , 0 , 2 ) )
-       {
-         $z = "</table>\n" ;
-          $l = array_pop ( $ltd ) ;
-          if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
-         if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
-          array_pop ( $ltr ) ;
-         $t[$k] = $z ;
-       }
-/*      else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
-        { 
-        $z = trim ( substr ( $x , 2 ) ) ;
-        $t[$k] = "<caption>{$z}</caption>\n" ;
-        }*/
-      else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
-       {
-          $x = substr ( $x , 1 ) ;
-          while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
-          $z = "" ;
-          $l = array_pop ( $ltd ) ;
-          if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
-         if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
-          array_pop ( $ltr ) ;
-         $t[$k] = $z ;
-          array_push ( $tr , false ) ;
-         array_push ( $td , false ) ;
-          array_push ( $ltd , "" ) ;
-          array_push ( $ltr , $this->fixTagAttributes ( $x ) ) ;
+               if ( trim ( $t ) == "" ) return "" ; # Saves runtime ;-)
+               $htmlattrs = $this->getHTMLattrs() ;
+         
+               # Strip non-approved attributes from the tag
+               $t = preg_replace(
+                       "/(\\w+)(\\s*=\\s*([^\\s\">]+|\"[^\">]*\"))?/e",
+                       "(in_array(strtolower(\"\$1\"),\$htmlattrs)?(\"\$1\".((\"x\$3\" != \"x\")?\"=\$3\":'')):'')",
+                       $t);
+               # Strip javascript "expression" from stylesheets. Brute force approach:
+               # If anythin offensive is found, all attributes of the HTML tag are dropped
+
+               if( preg_match( 
+                       "/style\\s*=.*(expression|tps*:\/\/|url\\s*\().*/is",
+                       wfMungeToUtf8( $t ) ) )
+               {
+                       $t="";
+               }
+
+               return trim ( $t ) ;
        }
-      else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
+
+       function doTableStuff ( $t )
        {
-          if ( "|+" == substr ( $x , 0 , 2 ) )
-              {
-              $fc = "+" ;
-              $x = substr ( $x , 1 ) ;
-              }
-          $after = substr ( $x , 1 ) ;
-          if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
-          $after = explode ( "||" , $after ) ;
-          $t[$k] = "" ;
-          foreach ( $after AS $theline )
-             {
-         $z = "" ;
-         if ( $fc != "+" )
-         {  
-            $tra = array_pop ( $ltr ) ;
-            if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
-            array_push ( $tr , true ) ;
-            array_push ( $ltr , "" ) ;
-         }
-
-          $l = array_pop ( $ltd ) ;
-         if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
-          if ( $fc == "|" ) $l = "TD" ;
-          else if ( $fc == "!" ) $l = "TH" ;
-          else if ( $fc == "+" ) $l = "CAPTION" ;
-          else $l = "" ;
-          array_push ( $ltd , $l ) ;
-         $y = explode ( "|" , $theline , 2 ) ;
-          if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
-          else $y = $y = "{$z}<{$l} ".$this->fixTagAttributes($y[0]).">{$y[1]}" ;
-          $t[$k] .= $y ;
-         array_push ( $td , true ) ;
-             }
-       }
-    }
+               $t = explode ( "\n" , $t ) ;
+               $td = array () ; # Is currently a td tag open?
+                       $ltd = array () ; # Was it TD or TH?
+                       $tr = array () ; # Is currently a tr tag open?
+                       $ltr = array () ; # tr attributes
+                       foreach ( $t AS $k => $x )
+                       {
+                               $x = rtrim ( $x ) ;
+                               $fc = substr ( $x , 0 , 1 ) ;
+                               if ( "{|" == substr ( $x , 0 , 2 ) )
+                               {
+                                       $t[$k] = "\n<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
+                                       array_push ( $td , false ) ;
+                                       array_push ( $ltd , "" ) ;
+                                       array_push ( $tr , false ) ;
+                                       array_push ( $ltr , "" ) ;
+                               }
+                               else if ( count ( $td ) == 0 ) { } # Don't do any of the following
+                               else if ( "|}" == substr ( $x , 0 , 2 ) )
+                               {
+                                       $z = "</table>\n" ;
+                                       $l = array_pop ( $ltd ) ;
+                                       if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
+                                       if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
+                                       array_pop ( $ltr ) ;
+                                       $t[$k] = $z ;
+                               }
+                               /*      else if ( "|_" == substr ( $x , 0 , 2 ) ) # Caption
+                                               { 
+                                               $z = trim ( substr ( $x , 2 ) ) ;
+                                               $t[$k] = "<caption>{$z}</caption>\n" ;
+                                               }*/
+                               else if ( "|-" == substr ( $x , 0 , 2 ) ) # Allows for |---------------
+                               {
+                                       $x = substr ( $x , 1 ) ;
+                                       while ( $x != "" && substr ( $x , 0 , 1 ) == '-' ) $x = substr ( $x , 1 ) ;
+                                       $z = "" ;
+                                       $l = array_pop ( $ltd ) ;
+                                       if ( array_pop ( $tr ) ) $z = "</tr>" . $z ;
+                                       if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
+                                       array_pop ( $ltr ) ;
+                                       $t[$k] = $z ;
+                                       array_push ( $tr , false ) ;
+                                       array_push ( $td , false ) ;
+                                       array_push ( $ltd , "" ) ;
+                                       array_push ( $ltr , $this->fixTagAttributes ( $x ) ) ;
+                               }
+                               else if ( "|" == $fc || "!" == $fc || "|+" == substr ( $x , 0 , 2 ) ) # Caption
+                               {
+                                       if ( "|+" == substr ( $x , 0 , 2 ) )
+                                       {
+                                               $fc = "+" ;
+                                               $x = substr ( $x , 1 ) ;
+                                       }
+                                       $after = substr ( $x , 1 ) ;
+                                       if ( $fc == "!" ) $after = str_replace ( "!!" , "||" , $after ) ;
+                                       $after = explode ( "||" , $after ) ;
+                                       $t[$k] = "" ;
+                                       foreach ( $after AS $theline )
+                                       {
+                                               $z = "" ;
+                                               if ( $fc != "+" )
+                                               {  
+                                                       $tra = array_pop ( $ltr ) ;
+                                                       if ( !array_pop ( $tr ) ) $z = "<tr {$tra}>\n" ;
+                                                       array_push ( $tr , true ) ;
+                                                       array_push ( $ltr , "" ) ;
+                                               }
 
-# Closing open td, tr && table
-while ( count ( $td ) > 0 )
-{
-if ( array_pop ( $td ) ) $t[] = "</td>" ;
-if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
-$t[] = "</table>" ;
-}
+                                               $l = array_pop ( $ltd ) ;
+                                               if ( array_pop ( $td ) ) $z = "</{$l}>" . $z ;
+                                               if ( $fc == "|" ) $l = "td" ;
+                                               else if ( $fc == "!" ) $l = "th" ;
+                                               else if ( $fc == "+" ) $l = "caption" ;
+                                               else $l = "" ;
+                                               array_push ( $ltd , $l ) ;
+                                               $y = explode ( "|" , $theline , 2 ) ;
+                                               if ( count ( $y ) == 1 ) $y = "{$z}<{$l}>{$y[0]}" ;
+                                               else $y = $y = "{$z}<{$l} ".$this->fixTagAttributes($y[0]).">{$y[1]}" ;
+                                               $t[$k] .= $y ;
+                                               array_push ( $td , true ) ;
+                                       }
+                               }
+                       }
 
-  $t = implode ( "\n" , $t ) ;
-#              $t = $this->removeHTMLtags( $t );
-  return $t ;
-}
+               # Closing open td, tr && table
+               while ( count ( $td ) > 0 )
+               {
+                       if ( array_pop ( $td ) ) $t[] = "</td>" ;
+                       if ( array_pop ( $tr ) ) $t[] = "</tr>" ;
+                       $t[] = "</table>" ;
+               }
+
+               $t = implode ( "\n" , $t ) ;
+               #               $t = $this->removeHTMLtags( $t );
+               return $t ;
+       }
 
        # Well, OK, it's actually about 14 passes.  But since all the
        # hard lifting is done inside PHP's regex code, it probably
@@ -359,104 +414,52 @@ $t[] = "</table>" ;
        #
        function doWikiPass2( $text, $linestart )
        {
-               global $wgUser, $wgLang, $wgUseDynamicDates;
-               $fname = "OutputPage::doWikiPass2";
+               $fname = "Parser::doWikiPass2";
                wfProfileIn( $fname );
                
                $text = $this->removeHTMLtags( $text );
                $text = $this->replaceVariables( $text );
 
-               $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
-               $text = str_replace ( "<HR>", "<hr>", $text );
+               # $text = preg_replace( "/(^|\n)-----*/", "\\1<hr>", $text );
 
-               $text = $this->doAllQuotes( $text );
                $text = $this->doHeadings( $text );
-               $text = $this->doBlockLevels( $text, $linestart );
                
-               if($wgUseDynamicDates) {
+               if($this->mOptions->getUseDynamicDates()) {
                        global $wgDateFormatter;
-                       $text = $wgDateFormatter->reformat( $wgUser->getOption("date"), $text );
+                       $text = $wgDateFormatter->reformat( $this->mOptions->getDateFormat(), $text );
                }
 
                $text = $this->replaceExternalLinks( $text );
-               $text = $this->replaceInternalLinks ( $text );
+               $text = $this->doTokenizedParser ( $text );
+
                $text = $this->doTableStuff ( $text ) ;
 
-               $text = $this->magicISBN( $text );
-               $text = $this->magicRFC( $text );
                $text = $this->formatHeadings( $text );
 
-               $sk = $wgUser->getSkin();
+               $sk =& $this->mOptions->getSkin();
                $text = $sk->transformContent( $text );
+               $fixtags = array(
+                       "/<hr *>/i" => '<hr/>',
+                       "/<br *>/i" => '<br/>', 
+                       "/<center *>/i"=>'<span style="text-align:center;">',
+                       "/<\\/center *>/i" => '</span>'
+               );
+               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+
+               # needs to be called last
+               $text = $this->doBlockLevels( $text, $linestart );              
                $text .= $this->categoryMagic () ;
 
                wfProfileOut( $fname );
                return $text;
        }
 
-       /* private */ function doAllQuotes( $text )
-       {
-               $outtext = "";
-               $lines = explode( "\r\n", $text );
-               foreach ( $lines as $line ) {
-                       $outtext .= $this->doQuotes ( "", $line, "" ) . "\r\n";
-               }
-               return $outtext;
-       }
-
-       /* private */ function doQuotes( $pre, $text, $mode )
-       {
-               if ( preg_match( "/^(.*)''(.*)$/sU", $text, $m ) ) {
-                       $m1_strong = ($m[1] == "") ? "" : "<strong>{$m[1]}</strong>";
-                       $m1_em = ($m[1] == "") ? "" : "<em>{$m[1]}</em>";
-                       if ( substr ($m[2], 0, 1) == "'" ) {
-                               $m[2] = substr ($m[2], 1);
-                               if ($mode == "em") {
-                                       return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "emstrong" );
-                               } else if ($mode == "strong") {
-                                       return $m1_strong . $this->doQuotes ( "", $m[2], "" );
-                               } else if (($mode == "emstrong") || ($mode == "both")) {
-                                       return $this->doQuotes ( "", $pre.$m1_strong.$m[2], "em" );
-                               } else if ($mode == "strongem") {
-                                       return "<strong>{$pre}{$m1_em}</strong>" . $this->doQuotes ( "", $m[2], "em" );
-                               } else {
-                                       return $m[1] . $this->doQuotes ( "", $m[2], "strong" );
-                               }
-                       } else {
-                               if ($mode == "strong") {
-                                       return $this->doQuotes ( $m[1], $m[2], ($m[1] == "") ? "both" : "strongem" );
-                               } else if ($mode == "em") {
-                                       return $m1_em . $this->doQuotes ( "", $m[2], "" );
-                               } else if ($mode == "emstrong") {
-                                       return "<em>{$pre}{$m1_strong}</em>" . $this->doQuotes ( "", $m[2], "strong" );
-                               } else if (($mode == "strongem") || ($mode == "both")) {
-                                       return $this->doQuotes ( "", $pre.$m1_em.$m[2], "strong" );
-                               } else {
-                                       return $m[1] . $this->doQuotes ( "", $m[2], "em" );
-                               }
-                       }
-               } else {
-                       $text_strong = ($text == "") ? "" : "<strong>{$text}</strong>";
-                       $text_em = ($text == "") ? "" : "<em>{$text}</em>";
-                       if ($mode == "") {
-                               return $pre . $text;
-                       } else if ($mode == "em") {
-                               return $pre . $text_em;
-                       } else if ($mode == "strong") {
-                               return $pre . $text_strong;
-                       } else if ($mode == "strongem") {
-                               return (($pre == "") && ($text == "")) ? "" : "<strong>{$pre}{$text_em}</strong>";
-                       } else {
-                               return (($pre == "") && ($text == "")) ? "" : "<em>{$pre}{$text_strong}</em>";
-                       }
-               }
-       }
 
        /* private */ function doHeadings( $text )
        {
                for ( $i = 6; $i >= 1; --$i ) {
                        $h = substr( "======", 0, $i );
-                       $text = preg_replace( "/^{$h}([^=]+){$h}(\\s|$)/m",
+                       $text = preg_replace( "/^{$h}(.+){$h}(\\s|$)/m",
                          "<h{$i}>\\1</h{$i}>\\2", $text );
                }
                return $text;
@@ -468,7 +471,7 @@ $t[] = "</table>" ;
 
        /* private */ function replaceExternalLinks( $text )
        {
-               $fname = "OutputPage::replaceExternalLinks";
+               $fname = "Parser::replaceExternalLinks";
                wfProfileIn( $fname );
                $text = $this->subReplaceExternalLinks( $text, "http", true );
                $text = $this->subReplaceExternalLinks( $text, "https", true );
@@ -483,10 +486,6 @@ $t[] = "</table>" ;
        
        /* private */ function subReplaceExternalLinks( $s, $protocol, $autonumber )
        {
-               global $wgUser, $printable;
-               global $wgAllowExternalImages;
-
-
                $unique = "4jzAfzB8hNvf4sqyO9Edd8pSmk9rE2in0Tgw3";
                $uc = "A-Za-z0-9_\\/~%\\-+&*#?!=()@\\x80-\\xFF";
                
@@ -506,9 +505,9 @@ $t[] = "</table>" ;
                  "((?i){$images})([^{$uc}]|$)/";
                  
                $e2 = "/(^|[^\\[])({$protocol}:)(([".$uc."]|[".$sep."][".$uc."])+)([^". $uc . $sep. "]|[".$sep."]|$)/";
-               $sk = $wgUser->getSkin();
+               $sk =& $this->mOptions->getSkin();
 
-               if ( $autonumber and $wgAllowExternalImages) { # Use img tags only for HTTP urls
+               if ( $autonumber and $this->mOptions->getAllowExternalImages() ) { # Use img tags only for HTTP urls
                        $s = preg_replace( $e1, "\\1" . $sk->makeImage( "{$unique}:\\3" .
                          "/\\4.\\5", "\\4.\\5" ) . "\\6", $s );
                }
@@ -539,8 +538,12 @@ $t[] = "</table>" ;
                                $s .= "[{$protocol}:" . $line;
                                continue;
                        }
-                       if ( $printable == "yes") $paren = " (<i>" . htmlspecialchars ( $link ) . "</i>)";
-                       else $paren = "";
+                       if( $link == $text || preg_match( "!$protocol://" . preg_quote( $text, "/" ) . "/?$!", $link ) ) {
+                               $paren = "";
+                       } else {
+                               # Expand the URL for printable version
+                               $paren = "<span class='urlexpansion'> (<i>" . htmlspecialchars ( $link ) . "</i>)</span>";
+                       }
                        $la = $sk->getExternalLinkAttributes( $link, $text );
                        $s .= "<a href='{$link}'{$la}>{$text}</a>{$paren}{$trail}";
 
@@ -548,140 +551,345 @@ $t[] = "</table>" ;
                return $s;
        }
 
-       /* private */ function replaceInternalLinks( $s )
+       /* private */ function handle3Quotes( &$state, $token )
+       {
+               if ( $state["strong"] !== false ) {
+                       if ( $state["em"] !== false && $state["em"] > $state["strong"] )
+                       {
+                               # ''' lala ''lala '''
+                               $s = "</em></strong><em>";
+                       } else {
+                               $s = "</strong>";
+                       }
+                       $state["strong"] = FALSE;
+               } else {
+                       $s = "<strong>";
+                       $state["strong"] = $token["pos"];
+               }
+               return $s;
+       }
+
+       /* private */ function handle2Quotes( &$state, $token )
+       {
+               if ( $state["em"] !== false ) {
+                       if ( $state["strong"] !== false && $state["strong"] > $state["em"] )
+                       {
+                               # ''lala'''lala'' ....'''
+                               $s = "</strong></em><strong>";
+                       } else {
+                               $s = "</em>";
+                       }
+                       $state["em"] = FALSE;
+               } else {
+                       $s = "<em>";
+                       $state["em"] = $token["pos"];
+               }
+               return $s;
+       }
+       
+       /* private */ function handle5Quotes( &$state, $token )
+       {
+               $s = "";
+               if ( $state["em"] !== false && $state["strong"] ) {
+                       if ( $state["em"] < $state["strong"] ) {
+                               $s .= "</strong></em>";
+                       } else {
+                               $s .= "</em></strong>";
+                       }
+                       $state["strong"] = $state["em"] = FALSE;
+               } elseif ( $state["em"] !== false ) {
+                       $s .= "</em><strong>";
+                       $state["em"] = FALSE;
+                       $state["strong"] = $token["pos"];
+               } elseif ( $state["strong"] !== false ) {
+                       $s .= "</strong><em>";
+                       $state["strong"] = FALSE;
+                       $state["em"] = $token["pos"];
+               } else { # not $em and not $strong
+                       $s .= "<strong><em>";
+                       $state["strong"] = $state["em"] = $token["pos"];
+               }
+               return $s;
+       }
+
+       /* private */ function doTokenizedParser( $str )
+       {
+               global $wgLang; # for language specific parser hook
+
+               $tokenizer=Tokenizer::newFromString( $str );
+               $tokenStack = array();
+               
+               $s="";
+               $state["em"]      = FALSE;
+               $state["strong"]  = FALSE;
+               $tagIsOpen = FALSE;
+               $threeopen = false;
+               
+               # The tokenizer splits the text into tokens and returns them one by one.
+               # Every call to the tokenizer returns a new token.
+               while ( $token = $tokenizer->nextToken() )
+               {
+                       switch ( $token["type"] )
+                       {
+                               case "text":
+                                       # simple text with no further markup
+                                       $txt = $token["text"];
+                                       break;
+                               case "[[[":
+                                       # remember the tag opened with 3 [
+                                       $threeopen = true;
+                               case "[[":
+                                       # link opening tag.
+                                       # FIXME : Treat orphaned open tags (stack not empty when text is over)
+                                       $tagIsOpen = TRUE;
+                                       array_push( $tokenStack, $token );
+                                       $txt="";
+                                       break;
+                                       
+                               case "]]]":
+                               case "]]":
+                                       # link close tag.
+                                       # get text from stack, glue it together, and call the code to handle a
+                                       # link
+                                       
+                                       if ( count( $tokenStack ) == 0 )
+                                       {
+                                               # stack empty. Found a ]] without an opening [[
+                                               $txt = "]]";
+                                       } else {
+                                               $linkText = "";
+                                               $lastToken = array_pop( $tokenStack );
+                                               while ( !(($lastToken["type"] == "[[[") or ($lastToken["type"] == "[[")) )
+                                               {
+                                                       if( !empty( $lastToken["text"] ) ) {
+                                                               $linkText = $lastToken["text"] . $linkText;
+                                                       }
+                                                       $lastToken = array_pop( $tokenStack );
+                                               }
+                                               
+                                               $txt = $linkText ."]]";
+                                               
+                                               if( isset( $lastToken["text"] ) ) {
+                                                       $prefix = $lastToken["text"];
+                                               } else {
+                                                       $prefix = "";
+                                               }
+                                               $nextToken = $tokenizer->previewToken();
+                                               if ( $nextToken["type"] == "text" ) 
+                                               {
+                                                       # Preview just looks at it. Now we have to fetch it.
+                                                       $nextToken = $tokenizer->nextToken();
+                                                       $txt .= $nextToken["text"];
+                                               }
+                                               $fakestate = $this->mStripState;
+                                               $txt = $this->handleInternalLink( $this->unstrip($txt,$fakestate), $prefix );
+
+                                               # did the tag start with 3 [ ?                                          
+                                               if($threeopen) {
+                                                       # show the first as text
+                                                       $txt = "[".$txt;
+                                                       $threeopen=false;
+                                               }
+                               
+                                       }
+                                       $tagIsOpen = (count( $tokenStack ) != 0);
+                                       break;
+                               case "----":
+                                       $txt = "\n<hr />\n";
+                                       break;
+                               case "'''":
+                                       # This and the three next ones handle quotes
+                                       $txt = $this->handle3Quotes( $state, $token );
+                                       break;
+                               case "''":
+                                       $txt = $this->handle2Quotes( $state, $token );
+                                       break;
+                               case "'''''":
+                                       $txt = $this->handle5Quotes( $state, $token );
+                                       break;
+                               case "":
+                                       # empty token
+                                       $txt="";
+                                       break;
+                               case "RFC ":
+                                       if ( $tagIsOpen ) {
+                                               $txt = "RFC ";
+                                       } else {
+                                               $txt = $this->doMagicRFC( $tokenizer );
+                                       }
+                                       break;
+                               case "ISBN ":
+                                       if ( $tagIsOpen ) {
+                                               $txt = "ISBN ";
+                                       } else {
+                                               $txt = $this->doMagicISBN( $tokenizer );
+                                       }
+                                       break;
+                               default:
+                                       # Call language specific Hook.
+                                       $txt = $wgLang->processToken( $token, $tokenStack );
+                                       if ( NULL == $txt ) {
+                                               # An unkown token. Highlight.
+                                               $txt = "<font color=\"#FF0000\"><b>".$token["type"]."</b></font>";
+                                               $txt .= "<font color=\"#FFFF00\"><b>".$token["text"]."</b></font>";
+                                       }
+                                       break;
+                       }
+                       # If we're parsing the interior of a link, don't append the interior to $s,
+                       # but push it to the stack so it can be processed when a ]] token is found.
+                       if ( $tagIsOpen  && $txt != "" ) {
+                               $token["type"] = "text";
+                               $token["text"] = $txt;
+                               array_push( $tokenStack, $token );
+                       } else {
+                               $s .= $txt;
+                       }
+               } #end while
+               if ( count( $tokenStack ) != 0 )
+               {
+                       # still objects on stack. opened [[ tag without closing ]] tag.
+                       $txt = "";
+                       while ( $lastToken = array_pop( $tokenStack ) )
+                       {
+                               if ( $lastToken["type"] == "text" )
+                               {
+                                       $txt = $lastToken["text"] . $txt;
+                               } else {
+                                       $txt = $lastToken["type"] . $txt;
+                               }       
+                       }
+                       $s .= $txt;
+               }
+               return $s;
+       }
+
+       /* private */ function handleInternalLink( $line, $prefix )
        {
-               global $wgTitle, $wgUser, $wgLang;
-               global $wgLinkCache, $wgInterwikiMagic, $wgUseCategoryMagic;
+               global $wgLang, $wgLinkCache;
                global $wgNamespacesWithSubpages, $wgLanguageCode;
-               global $wgUseLinkPrefixCombination;
-               wfProfileIn( $fname = "OutputPage::replaceInternalLinks" );
+               static $fname = "Parser::handleInternalLink" ;
+               wfProfileIn( $fname );
 
                wfProfileIn( "$fname-setup" );
-               $tc = Title::legalChars() . "#";
-               $sk = $wgUser->getSkin();
-
-               $a = explode( "[[", " " . $s );
-               $s = array_shift( $a );
-               $s = substr( $s, 1 );
+               static $tc = FALSE;
+               if ( !$tc ) { $tc = Title::legalChars() . "#"; }
+               $sk =& $this->mOptions->getSkin();
 
                # Match a link having the form [[namespace:link|alternate]]trail
-               $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD";
+               static $e1 = FALSE;
+               if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|([^]]+))?]](.*)\$/sD"; }
                # Match the end of a line for a word that's not followed by whitespace,
                # e.g. in the case of 'The Arab al[[Razi]]', 'al' will be matched
                #$e2 = "/^(.*)\\b(\\w+)\$/suD";
                #$e2 = "/^(.*\\s)(\\S+)\$/suD";
-               $e2 = '/^(.*\s)([a-zA-Z\x80-\xff]+)$/sD';
+               static $e2 = '/^(.*\s)([a-zA-Z\x80-\xff]+)$/sD';
                
 
                # Special and Media are pseudo-namespaces; no pages actually exist in them
-               $image = Namespace::getImage();
-               $special = Namespace::getSpecial();
-               $media = Namespace::getMedia();
-               $category = wfMsg ( "category" ) ;
-               $nottalk = !Namespace::isTalk( $wgTitle->getNamespace() );
-
-               if ( $wgLang->linkPrefixExtension() && preg_match( $e2, $s, $m ) ) {
-                       $new_prefix = $m[2];
-                       $s = $m[1];
-               } else {
-                       $new_prefix="";
-               }
+               static $image = FALSE;
+               static $special = FALSE;
+               static $media = FALSE;
+               static $category = FALSE;
+               if ( !$image ) { $image = Namespace::getImage(); }
+               if ( !$special ) { $special = Namespace::getSpecial(); }
+               if ( !$media ) { $media = Namespace::getMedia(); }
+               if ( !$category ) { $category = wfMsg ( "category" ) ; }
+               
+               $nottalk = !Namespace::isTalk( $this->mTitle->getNamespace() );
 
                wfProfileOut( "$fname-setup" );
-
-               foreach ( $a as $line ) {
-                       $prefix = $new_prefix;
-                       if ( $wgUseLinkPrefixCombination && preg_match( $e2, $line, $m ) ) {
-                               $new_prefix = $m[2];
-                               $line = $m[1];
+               $s = "";
+               
+               if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
+                       $text = $m[2];
+                       $trail = $m[3];                         
+               } else { # Invalid form; output directly
+                       $s .= $prefix . "[[" . $line ;
+                       return $s;
+               }
+               
+               /* Valid link forms:
+               Foobar -- normal
+               :Foobar -- override special treatment of prefix (images, language links)
+               /Foobar -- convert to CurrentPage/Foobar
+               /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
+               */
+               $c = substr($m[1],0,1);
+               $noforce = ($c != ":");
+               if( $c == "/" ) { # subpage
+                       if(substr($m[1],-1,1)=="/") {                 # / at end means we don't want the slash to be shown
+                               $m[1]=substr($m[1],1,strlen($m[1])-2); 
+                               $noslash=$m[1];
                        } else {
-                               $new_prefix = "";
-                       }
-                       if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
-                               $text = $m[2];
-                               $trail = $m[3];                         
-                       } else { # Invalid form; output directly
-                               $s .= $prefix . "[[" . $line ;
-                               continue;
+                               $noslash=substr($m[1],1);
                        }
-                       
-                       /* Valid link forms:
-                       Foobar -- normal
-                       :Foobar -- override special treatment of prefix (images, language links)
-                       /Foobar -- convert to CurrentPage/Foobar
-                       /Foobar/ -- convert to CurrentPage/Foobar, strip the initial / from text
-                       */
-                       $c = substr($m[1],0,1);
-                       $noforce = ($c != ":");
-                       if( $c == "/" ) { # subpage
-                               if(substr($m[1],-1,1)=="/") {                 # / at end means we don't want the slash to be shown
-                                       $m[1]=substr($m[1],1,strlen($m[1])-2); 
-                                       $noslash=$m[1];
-                               } else {
-                                       $noslash=substr($m[1],1);
-                               }
-                               if($wgNamespacesWithSubpages[$wgTitle->getNamespace()]) { # subpages allowed here
-                                       $link = $wgTitle->getPrefixedText(). "/" . trim($noslash);
-                                       if( "" == $text ) {
-                                               $text= $m[1]; 
-                                       } # this might be changed for ugliness reasons
-                               } else {
-                                       $link = $noslash; # no subpage allowed, use standard link
-                               }
-                       } elseif( $noforce ) { # no subpage
-                               $link = $m[1];
+                       if($wgNamespacesWithSubpages[$this->mTitle->getNamespace()]) { # subpages allowed here
+                               $link = $this->mTitle->getPrefixedText(). "/" . trim($noslash);
+                               if( "" == $text ) {
+                                       $text= $m[1]; 
+                               } # this might be changed for ugliness reasons
                        } else {
-                               $link = substr( $m[1], 1 );
+                               $link = $noslash; # no subpage allowed, use standard link
                        }
-                       if( "" == $text )
-                               $text = $link;
+               } elseif( $noforce ) { # no subpage
+                       $link = $m[1];
+               } else {
+                       $link = substr( $m[1], 1 );
+               }
+               if( "" == $text )
+                       $text = $link;
 
-                       $nt = Title::newFromText( $link );
-                       if( !$nt ) {
-                               $s .= $prefix . "[[" . $line;
-                               continue;
-                       }
-                       $ns = $nt->getNamespace();
-                       $iw = $nt->getInterWiki();
-                       if( $noforce ) {
-                               if( $iw && $wgInterwikiMagic && $nottalk && $wgLang->getLanguageName( $iw ) ) {
-                                       array_push( $this->mOutput->mLanguageLinks, $nt->getPrefixedText() );
-                                       $s .= $prefix . $trail;
-                                       continue;
-                               }
-                               if( $ns == $image ) {
-                                       $s .= $prefix . $sk->makeImageLinkObj( $nt, $text ) . $trail;
-                                       $wgLinkCache->addImageLinkObj( $nt );
-                                       continue;
-                               }
-                       }
-                       if( ( $nt->getPrefixedText() == $wgTitle->getPrefixedText() ) &&
-                           ( strpos( $link, "#" ) == FALSE ) ) {
-                               $s .= $prefix . "<strong>" . $text . "</strong>" . $trail;
-                               continue;
-                       }
-                       if ( $ns == $category && $wgUseCategoryMagic ) {
-                         $t = explode ( ":" , $nt->getText() ) ;
-                               array_shift ( $t ) ;
-                               $t = implode ( ":" , $t ) ;
-                               $t = $wgLang->ucFirst ( $t ) ;
-#                              $t = $sk->makeKnownLink( $category.":".$t, $t, "", $trail , $prefix );
-                               $nnt = Title::newFromText ( $category.":".$t ) ;
-                               $t = $sk->makeLinkObj( $nnt, $t, "", $trail , $prefix );
-                               $this->mCategoryLinks[] = $t ;
-                               $s .= $prefix . $trail ;
-                               continue ;          
+               $nt = Title::newFromText( $link );
+               if( !$nt ) {
+                       $s .= $prefix . "[[" . $line;
+                       return $s;
+               }
+               $ns = $nt->getNamespace();
+               $iw = $nt->getInterWiki();
+               if( $noforce ) {
+                       if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgLang->getLanguageName( $iw ) ) {
+                               array_push( $this->mOutput->mLanguageLinks, $nt->getPrefixedText() );
+                               return (trim($s) == '')? '': $s;
                        }
-                       if( $ns == $media ) {
-                               $s .= $prefix . $sk->makeMediaLinkObj( $nt, $text ) . $trail;
+                       if( $ns == $image ) {
+                               $s .= $prefix . $sk->makeImageLinkObj( $nt, $text ) . $trail;
                                $wgLinkCache->addImageLinkObj( $nt );
-                               continue;
-                       } elseif( $ns == $special ) {
-                               $s .= $prefix . $sk->makeKnownLinkObj( $nt, $text, "", $trail );
-                               continue;
+                               return $s;
                        }
-                       $s .= $sk->makeLinkObj( $nt, $text, "", $trail , $prefix );
                }
+               if( ( $nt->getPrefixedText() == $this->mTitle->getPrefixedText() ) &&
+                   ( strpos( $link, "#" ) == FALSE ) ) {
+                       $s .= $prefix . "<strong>" . $text . "</strong>" . $trail;
+                       return $s;
+               }
+
+               # Category feature
+               $catns = strtoupper ( $nt->getDBkey () ) ;
+               $catns = explode ( ":" , $catns ) ;
+               if ( count ( $catns ) > 1 ) $catns = array_shift ( $catns ) ;
+               else $catns = "" ;
+               if ( $catns == strtoupper($category) && $this->mOptions->getUseCategoryMagic() ) {
+                       $t = explode ( ":" , $nt->getText() ) ;
+                       array_shift ( $t ) ;
+                       $t = implode ( ":" , $t ) ;
+                       $t = $wgLang->ucFirst ( $t ) ;
+                       $nnt = Title::newFromText ( $category.":".$t ) ;
+                       $t = $sk->makeLinkObj( $nnt, $t, "", $trail , $prefix );
+                       $this->mOutput->mCategoryLinks[] = $t ;
+                       $s .= $prefix . $trail ;
+                       return $s ;
+               }
+
+               if( $ns == $media ) {
+                       $s .= $prefix . $sk->makeMediaLinkObj( $nt, $text ) . $trail;
+                       $wgLinkCache->addImageLinkObj( $nt );
+                       return $s;
+               } elseif( $ns == $special ) {
+                       $s .= $prefix . $sk->makeKnownLinkObj( $nt, $text, "", $trail );
+                       return $s;
+               }
+               $s .= $sk->makeLinkObj( $nt, $text, "", $trail , $prefix );
+
                wfProfileOut( $fname );
                return $s;
        }
@@ -691,8 +899,7 @@ $t[] = "</table>" ;
        /* private */ function closeParagraph()
        {
                $result = "";
-               if ( 0 != strcmp( "p", $this->mLastSection ) &&
-                 0 != strcmp( "", $this->mLastSection ) ) {
+               if ( '' != $this->mLastSection ) {
                        $result = "</" . $this->mLastSection  . ">";
                }
                $this->mLastSection = "";
@@ -766,14 +973,14 @@ $t[] = "</table>" ;
 
        /* private */ function doBlockLevels( $text, $linestart )
        {
-               $fname = "OutputPage::doBlockLevels";
+               $fname = "Parser::doBlockLevels";
                wfProfileIn( $fname );
                # Parsing through the text line by line.  The main thing
                # happening here is handling of block-level elements p, pre,
                # and making lists from lines starting with * # : etc.
                #
                $a = explode( "\n", $text );
-               $text = $lastPref = "";
+               $lastPref = $text = '';
                $this->mDTopen = $inBlockElem = false;
 
                if ( ! $linestart ) { $text .= array_shift( $a ); }
@@ -825,32 +1032,32 @@ $t[] = "</table>" ;
                                $lastPref = $pref2;
                        }
                        if ( 0 == $npl ) { # No prefix--go to paragraph mode
+                               $uniq_prefix = UNIQ_PREFIX;
+                               $inBlockElem=false;
                                if ( preg_match(
-                                 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6)/i", $t ) ) {
+                                 "/(<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<div)/i", $t ) ) {
                                        $text .= $this->closeParagraph();
                                        $inBlockElem = true;
+                               } else if ( preg_match("/(<hr|<\\/td|".$uniq_prefix."-pre)/i", $t ) ) {
+                                       $text .= $this->closeParagraph();
+                                       $inBlockElem = false;
                                }
-                               if ( ! $inBlockElem ) {
+                               if ( !$inBlockElem ) {
                                        if ( " " == $t{0} ) {
                                                $newSection = "pre";
+                                               $text .= $this->closeParagraph();
                                                # $t = wfEscapeHTML( $t );
                                        }
                                        else { $newSection = "p"; }
 
-                                       if ( 0 == strcmp( "", trim( $oLine ) ) ) {
+                                       if ( ( '' == trim( $oLine ) ) ||  ( $this->mLastSection == $newSection and $newSection != 'p' )) {
                                                $text .= $this->closeParagraph();
                                                $text .= "<" . $newSection . ">";
-                                       } else if ( 0 != strcmp( $this->mLastSection,
-                                         $newSection ) ) {
-                                               $text .= $this->closeParagraph();
-                                               if ( 0 != strcmp( "p", $newSection ) ) {
-                                                       $text .= "<" . $newSection . ">";
-                                               }
-                                       }
-                                       $this->mLastSection = $newSection;
+                                               $this->mLastSection = $newSection;
+                                       } 
                                }
                                if ( $inBlockElem &&
-                                 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6)/i", $t ) ) {
+                                 preg_match( "/(<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|<\\/p<\\/div)/i", $t ) ) {
                                        $inBlockElem = false;
                                }
                        }
@@ -861,74 +1068,270 @@ $t[] = "</table>" ;
                        --$npl;
                }
                if ( "" != $this->mLastSection ) {
-                       if ( "p" != $this->mLastSection ) {
-                               $text .= "</" . $this->mLastSection . ">";
-                       }
+                       $text .= "</" . $this->mLastSection . ">";
                        $this->mLastSection = "";
                }
                wfProfileOut( $fname );
                return $text;
        }
 
+       function getVariableValue( $index ) {
+               global $wgLang, $wgSitename, $wgServer;
+
+               switch ( $index ) {
+                       case MAG_CURRENTMONTH:
+                               return date( "m" );
+                       case MAG_CURRENTMONTHNAME:
+                               return $wgLang->getMonthName( date("n") );
+                       case MAG_CURRENTMONTHNAMEGEN:
+                               return $wgLang->getMonthNameGen( date("n") );
+                       case MAG_CURRENTDAY:
+                               return date("j");
+                       case MAG_CURRENTDAYNAME:
+                               return $wgLang->getWeekdayName( date("w")+1 );
+                       case MAG_CURRENTYEAR:
+                               return date( "Y" );
+                       case MAG_CURRENTTIME:
+                               return $wgLang->time( wfTimestampNow(), false );
+                       case MAG_NUMBEROFARTICLES:
+                               return wfNumberOfArticles();
+                       case MAG_SITENAME:
+                               return $wgSitename;
+                       case MAG_SERVER:
+                               return $wgServer;
+                       default:
+                               return NULL;
+               }
+       }
+
+       function initialiseVariables()
+       {
+               global $wgVariableIDs;
+               $this->mVariables = array();
+               foreach ( $wgVariableIDs as $id ) {
+                       $mw =& MagicWord::get( $id );
+                       $mw->addToArray( $this->mVariables, $this->getVariableValue( $id ) );
+               }
+       }
+
        /* private */ function replaceVariables( $text )
        {
-               global $wgLang, $wgCurOut;
-               $fname = "OutputPage::replaceVariables";
+               global $wgLang, $wgCurParser;
+               global $wgScript, $wgArticlePath;
+
+               $fname = "Parser::replaceVariables";
                wfProfileIn( $fname );
+               
+               $bail = false;
+               if ( !$this->mVariables ) {
+                       $this->initialiseVariables();
+               }
+               $titleChars = Title::legalChars();
+               $regex = "/{{([$titleChars\\|]*?)}}/s";
 
-               $magic = array();
+               # "Recursive" variable expansion: run it through a couple of passes
+               for ( $i=0; $i<MAX_INCLUDE_REPEAT && !$bail; $i++ ) {
+                       $oldText = $text;
+                       
+                       # It's impossible to rebind a global in PHP
+                       # Instead, we run the substitution on a copy, then merge the changed fields back in
+                       $wgCurParser = $this->fork();
 
-               # Basic variables
-               # See Language.php for the definition of each magic word
-               # As with sigs, this uses the server's local time -- ensure 
-               # this is appropriate for your audience!
+                       $text = preg_replace_callback( $regex, "wfBraceSubstitution", $text );
+                       if ( $oldText == $text ) {
+                               $bail = true;
+                       }
+                       $this->merge( $wgCurParser );
+               }
 
-               $magic[MAG_CURRENTMONTH] = date( "m" );
-               $magic[MAG_CURRENTMONTHNAME] = $wgLang->getMonthName( date("n") );
-               $magic[MAG_CURRENTMONTHNAMEGEN] = $wgLang->getMonthNameGen( date("n") );
-               $magic[MAG_CURRENTDAY] = date("j");
-               $magic[MAG_CURRENTDAYNAME] = $wgLang->getWeekdayName( date("w")+1 );
-               $magic[MAG_CURRENTYEAR] = date( "Y" );
-               $magic[MAG_CURRENTTIME] = $wgLang->time( wfTimestampNow(), false );
+               return $text;
+       }
+
+       # Returns a copy of this object except with various variables cleared
+       # This copy can be re-merged with the parent after operations on the copy
+       function fork()
+       {
+               $copy = $this;
+               $copy->mOutput = new ParserOutput;
+               return $copy;
+       }
+
+       # Merges a copy split off with fork()
+       function merge( &$copy )
+       {
+               $this->mOutput->merge( $copy->mOutput );
                
-               $this->mContainsOldMagic += MagicWord::replaceMultiple($magic, $text, $text);
+               # Merge include throttling arrays
+               foreach( $copy->mIncludeCount as $dbk => $count ) {
+                       if ( array_key_exists( $dbk, $this->mIncludeCount ) ) {
+                               $this->mIncludeCount[$dbk] += $count;
+                       } else {
+                               $this->mIncludeCount[$dbk] = $count;
+                       }
+               }
+       }
 
-               $mw =& MagicWord::get( MAG_NUMBEROFARTICLES );
-               if ( $mw->match( $text ) ) {
-                       $v = wfNumberOfArticles();
-                       $text = $mw->replace( $v, $text );
-                       if( $mw->getWasModified() ) { $this->mContainsOldMagic++; }
+       function braceSubstitution( $matches )
+       {
+               global $wgLinkCache, $wgLang;
+               $fname = "Parser::braceSubstitution";
+               $found = false;
+               $nowiki = false;
+       
+               $text = $matches[1];
+
+               # SUBST
+               $mwSubst =& MagicWord::get( MAG_SUBST );
+               if ( $mwSubst->matchStartAndRemove( $text ) ) {
+                       if ( $this->mOutputType != OT_WIKI ) {
+                               # Invalid SUBST not replaced at PST time
+                               # Return without further processing
+                               $text = $matches[0];
+                               $found = true;
+                       }
+               } elseif ( $this->mOutputType == OT_WIKI ) {
+                       # SUBST not found in PST pass, do nothing
+                       $text = $matches[0];
+                       $found = true;
                }
+               
+               # MSG, MSGNW and INT
+               if ( !$found ) {
+                       # Check for MSGNW:
+                       $mwMsgnw =& MagicWord::get( MAG_MSGNW );
+                       if ( $mwMsgnw->matchStartAndRemove( $text ) ) {
+                               $nowiki = true;
+                       } else {
+                               # Remove obsolete MSG:
+                               $mwMsg =& MagicWord::get( MAG_MSG );
+                               $mwMsg->matchStartAndRemove( $text );
+                       }
+                       
+                       # Check if it is an internal message
+                       $mwInt =& MagicWord::get( MAG_INT );
+                       if ( $mwInt->matchStartAndRemove( $text ) ) {
+                               $text = wfMsg( $text );
+                               $found = true;
+                       }
+               }
+       
+               # NS
+               if ( !$found ) {
+                       # Check for NS: (namespace expansion)
+                       $mwNs = MagicWord::get( MAG_NS );
+                       if ( $mwNs->matchStartAndRemove( $text ) ) {
+                               if ( intval( $text ) ) {
+                                       $text = $wgLang->getNsText( intval( $text ) );
+                                       $found = true;
+                               } else {
+                                       $index = Namespace::getCanonicalIndex( strtolower( $text ) );
+                                       if ( !is_null( $index ) ) {
+                                               $text = $wgLang->getNsText( $index );
+                                               $found = true;
+                                       }
+                               }
+                       }
+               }
+               
+               # LOCALURL and LOCALURLE
+               if ( !$found ) {
+                       $mwLocal = MagicWord::get( MAG_LOCALURL );
+                       $mwLocalE = MagicWord::get( MAG_LOCALURLE );
+
+                       if ( $mwLocal->matchStartAndRemove( $text ) ) {
+                               $func = 'getLocalURL';
+                       } elseif ( $mwLocalE->matchStartAndRemove( $text ) ) {
+                               $func = 'escapeLocalURL';
+                       } else {
+                               $func = '';
+                       }
+                       
+                       if ( $func !== '' ) {
+                               $args = explode( "|", $text );
+                               $n = count( $args );
+                               if ( $n > 0 ) {
+                                       $title = Title::newFromText( $args[0] );
+                                       if ( !is_null( $title ) ) {
+                                               if ( $n > 1 ) {
+                                                       $text = $title->$func( $args[1] );
+                                               } else {
+                                                       $text = $title->$func();
+                                               }
+                                               $found = true;
+                                       }
+                               }
+                       }       
+               }
+               
+               # Check for a match against internal variables
+               if ( !$found && array_key_exists( $text, $this->mVariables ) ) {
+                       $text = $this->mVariables[$text];
+                       $found = true;
+                       $this->mOutput->mContainsOldMagic = true;
+               } 
+               
+               # Load from database
+               if ( !$found ) {
+                       $title = Title::newFromText( $text, NS_TEMPLATE );
+                       if ( is_object( $title ) && !$title->isExternal() ) {
+                               # Check for excessive inclusion
+                               $dbk = $title->getPrefixedDBkey();
+                               if ( !array_key_exists( $dbk, $this->mIncludeCount ) ) {
+                                       $this->mIncludeCount[$dbk] = 0;
+                               }
+                               if ( ++$this->mIncludeCount[$dbk] <= MAX_INCLUDE_REPEAT ) {
+                                       $article = new Article( $title );
+                                       $articleContent = $article->getContentWithoutUsingSoManyDamnGlobals();
+                                       if ( $articleContent !== false ) {
+                                               $found = true;
+                                               $text = $articleContent;
+                                               
+                                               # Escaping and link table handling
+                                               # Not required for preSaveTransform()
+                                               if ( $this->mOutputType == OT_HTML ) {
+                                                       if ( $nowiki ) {
+                                                               $text = wfEscapeWikiText( $text );
+                                                       } else {
+                                                               $text = $this->removeHTMLtags( $text );
+                                                       }
+                                                       $wgLinkCache->suspend();
+                                                       $text = $this->doTokenizedParser( $text );
+                                                       $wgLinkCache->resume();
+                                                       $wgLinkCache->addLinkObj( $title );
 
-               # "Variables" with an additional parameter e.g. {{MSG:wikipedia}}
-               # The callbacks are at the bottom of this file
-               $wgCurOut = $this;
-               $mw =& MagicWord::get( MAG_MSG );
-               $text = $mw->substituteCallback( $text, "wfReplaceMsgVar" );
-               if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
+                                               }
+                                       } 
+                               } 
 
-               $mw =& MagicWord::get( MAG_MSGNW );
-               $text = $mw->substituteCallback( $text, "wfReplaceMsgnwVar" );
-               if( $mw->getWasModified() ) { $this->mContainsNewMagic++; }
+                               # If the title is valid but undisplayable, make a link to it
+                               if ( $this->mOutputType == OT_HTML && !$found ) {
+                                       $text = "[[" . $title->getPrefixedText() . "]]";
+                                       $found = true;
+                               }
+                       }
+               }
 
-               wfProfileOut( $fname );
-               return $text;
+               if ( !$found ) {
+                       return $matches[0];
+               } else {
+                       return $text;
+               }
        }
 
        # Cleans up HTML, removes dangerous tags and attributes
        /* private */ function removeHTMLtags( $text )
        {
-               $fname = "OutputPage::removeHTMLtags";
+               $fname = "Parser::removeHTMLtags";
                wfProfileIn( $fname );
                $htmlpairs = array( # Tags that must be closed
                        "b", "i", "u", "font", "big", "small", "sub", "sup", "h1",
                        "h2", "h3", "h4", "h5", "h6", "cite", "code", "em", "s",
                        "strike", "strong", "tt", "var", "div", "center",
                        "blockquote", "ol", "ul", "dl", "table", "caption", "pre",
-                       "ruby", "rt" , "rb" , "rp"
+                       "ruby", "rt" , "rb" , "rp", "p"
                );
                $htmlsingle = array(
-                       "br", "p", "hr", "li", "dt", "dd"
+                       "br", "hr", "li", "dt", "dd"
                );
                $htmlnest = array( # Tags that can be nested--??
                        "table", "tr", "td", "th", "div", "blockquote", "ol", "ul",
@@ -1018,198 +1421,398 @@ $t[] = "</table>" ;
  *
  * It loops through all headlines, collects the necessary data, then splits up the
  * string and re-inserts the newly formatted headlines.
- *
- * */
+ * 
+ */
+
        /* private */ function formatHeadings( $text )
        {
-               global $wgUser,$wgArticle,$wgTitle,$wpPreview;
-               $nh=$wgUser->getOption( "numberheadings" );
-               $st=$wgUser->getOption( "showtoc" );
-               if(!$wgTitle->userCanEdit()) {
-                       $es=0;
-                       $esr=0;
+               return $text;
+               $doNumberHeadings = $this->mOptions->getNumberHeadings();
+               $doShowToc = $this->mOptions->getShowToc();
+               if( !$this->mTitle->userCanEdit() ) {
+                       $showEditLink = 0;
+                       $rightClickHack = 0;
                } else {
-                       $es=$wgUser->getID() && $wgUser->getOption( "editsection" );
-                       $esr=$wgUser->getID() && $wgUser->getOption( "editsectiononrightclick" );
+                       $showEditLink = $this->mOptions->getEditSection();
+                       $rightClickHack = $this->mOptions->getEditSectionOnRightClick();
                }
 
                # Inhibit editsection links if requested in the page
                $esw =& MagicWord::get( MAG_NOEDITSECTION );
-               if ($esw->matchAndRemove( $text )) {
-                       $es=0;
+               if( $esw->matchAndRemove( $text ) ) {
+                       $showEditLink = 0;
                }
                # if the string __NOTOC__ (not case-sensitive) occurs in the HTML, 
                # do not add TOC
                $mw =& MagicWord::get( MAG_NOTOC );
-               if ($mw->matchAndRemove( $text ))
-               {
-                       $st = 0;
+               if( $mw->matchAndRemove( $text ) ) {
+                       $doShowToc = 0;
                }
 
                # never add the TOC to the Main Page. This is an entry page that should not
                # be more than 1-2 screens large anyway
-               if($wgTitle->getPrefixedText()==wfMsg("mainpage")) {$st=0;}
+               if( $this->mTitle->getPrefixedText() == wfMsg("mainpage") ) {
+                       $doShowToc = 0;
+               }
+
+               # Get all headlines for numbering them and adding funky stuff like [edit]
+               # links - this is for later, but we need the number of headlines right now
+               $numMatches = preg_match_all( "/<H([1-6])(.*?" . ">)(.*?)<\/H[1-6]>/i", $text, $matches );
+
+               # if there are fewer than 4 headlines in the article, do not show TOC
+               if( $numMatches < 4 ) {
+                       $doShowToc = 0;
+               }
+
+               # if the string __FORCETOC__ (not case-sensitive) occurs in the HTML,
+               # override above conditions and always show TOC
+               $mw =& MagicWord::get( MAG_FORCETOC );
+               if ($mw->matchAndRemove( $text ) ) {
+                       $doShowToc = 1;
+               }
+
 
                # We need this to perform operations on the HTML
-               $sk=$wgUser->getSkin();
+               $sk =& $this->mOptions->getSkin();
 
-               # Get all headlines for numbering them and adding funky stuff like [edit]
-               # links
-               preg_match_all("/<H([1-6])(.*?>)(.*?)<\/H[1-6]>/i",$text,$matches);
-               
                # headline counter
-               $c=0;
+               $headlineCount = 0;
 
                # Ugh .. the TOC should have neat indentation levels which can be
                # passed to the skin functions. These are determined here
-               foreach($matches[3] as $headline) {
-                       if($level) { $prevlevel=$level;}
-                       $level=$matches[1][$c];
-                       if(($nh||$st) && $prevlevel && $level>$prevlevel) { 
-                                                       
-                               $h[$level]=0; // reset when we enter a new level                                
-                               $toc.=$sk->tocIndent($level-$prevlevel);
-                               $toclevel+=$level-$prevlevel;
-                       
+               $toclevel = 0;
+               $toc = "";
+               $full = "";
+               $head = array();
+               $sublevelCount = array();
+               $level = 0;
+               $prevlevel = 0;
+               foreach( $matches[3] as $headline ) {
+                       $numbering = "";
+                       if( $level ) {
+                               $prevlevel = $level;
+                       }
+                       $level = $matches[1][$headlineCount];
+                       if( ( $doNumberHeadings || $doShowToc ) && $prevlevel && $level > $prevlevel ) { 
+                               # reset when we enter a new level
+                               $sublevelCount[$level] = 0;
+                               $toc .= $sk->tocIndent( $level - $prevlevel );
+                               $toclevel += $level - $prevlevel;
                        } 
-                       if(($nh||$st) && $level<$prevlevel) {
-                               $h[$level+1]=0; // reset when we step back a level
-                               $toc.=$sk->tocUnindent($prevlevel-$level);
-                               $toclevel-=$prevlevel-$level;
-
+                       if( ( $doNumberHeadings || $doShowToc ) && $level < $prevlevel ) {
+                               # reset when we step back a level
+                               $sublevelCount[$level+1]=0;
+                               $toc .= $sk->tocUnindent( $prevlevel - $level );
+                               $toclevel -= $prevlevel - $level;
                        }
-                       $h[$level]++; // count number of headlines for each level
-                       
-                       if($nh||$st) {
-                               for($i=1;$i<=$level;$i++) {
-                                       if($h[$i]) {
-                                               if($dot) {$numbering.=".";}
-                                               $numbering.=$h[$i];
-                                               $dot=1;                                 
+                       # count number of headlines for each level
+                       @$sublevelCount[$level]++;
+                       if( $doNumberHeadings || $doShowToc ) {
+                               $dot = 0;
+                               for( $i = 1; $i <= $level; $i++ ) {
+                                       if( !empty( $sublevelCount[$i] ) ) {
+                                               if( $dot ) {
+                                                       $numbering .= ".";
+                                               }
+                                               $numbering .= $sublevelCount[$i];
+                                               $dot = 1;                                       
                                        }
                                }
                        }
 
-                       // The canonized header is a version of the header text safe to use for links
+                       # The canonized header is a version of the header text safe to use for links
+                       # Avoid insertion of weird stuff like <math> by expanding the relevant sections
+                       $canonized_headline = Parser::unstrip( $headline, $this->mStripState );
+                       
+                       # strip out HTML
+                       $canonized_headline = preg_replace( "/<.*?" . ">/","",$canonized_headline );
+                       $tocline = trim( $canonized_headline ); 
+                       $canonized_headline = preg_replace("/[ &\\/<>\\(\\)\\[\\]=,+']+/", '_', html_entity_decode( $tocline));
+                       $refer[$headlineCount] = $canonized_headline;
                        
-                       $canonized_headline=preg_replace("/<.*?>/","",$headline); // strip out HTML
-                       $tocline = trim( $canonized_headline );
-                       $canonized_headline=str_replace('"',"",$canonized_headline);
-                       $canonized_headline=str_replace(" ","_",trim($canonized_headline));                     
-                       $refer[$c]=$canonized_headline;
-                       $refers[$canonized_headline]++;  // count how many in assoc. array so we can track dupes in anchors
-                       $refcount[$c]=$refers[$canonized_headline];
-
-            // Prepend the number to the heading text
+                       # count how many in assoc. array so we can track dupes in anchors
+                       @$refers[$canonized_headline]++;
+                       $refcount[$headlineCount]=$refers[$canonized_headline];
+
+                       # Prepend the number to the heading text
                        
-                       if($nh||$st) {
-                               $tocline=$numbering ." ". $tocline;
+                       if( $doNumberHeadings || $doShowToc ) {
+                               $tocline = $numbering . " " . $tocline;
                                
-                               // Don't number the heading if it is the only one (looks silly)
-                               if($nh && count($matches[3]) > 1) {
-                                       $headline=$numbering . " " . $headline; // the two are different if the line contains a link
+                               # Don't number the heading if it is the only one (looks silly)
+                               if( $doNumberHeadings && count( $matches[3] ) > 1) {
+                                       # the two are different if the line contains a link
+                                       $headline=$numbering . " " . $headline;
                                }
                        }
                        
-                       // Create the anchor for linking from the TOC to the section
-                       
-                       $anchor=$canonized_headline;
-                       if($refcount[$c]>1) {$anchor.="_".$refcount[$c];}
-                       if($st) {
-                               $toc.=$sk->tocLine($anchor,$tocline,$toclevel);
+                       # Create the anchor for linking from the TOC to the section
+                       $anchor = $canonized_headline;
+                       if($refcount[$headlineCount] > 1 ) {
+                               $anchor .= "_" . $refcount[$headlineCount];
                        }
-                       if($es && !isset($wpPreview)) {
-                               $head[$c].=$sk->editSectionLink($c+1);
+                       if( $doShowToc ) {
+                               $toc .= $sk->tocLine($anchor,$tocline,$toclevel);
                        }
-                       
-                       // Put it all together
-                       
-                       $head[$c].="<h".$level.$matches[2][$c]
-                        ."<a name=\"".$anchor."\">"
-                        .$headline
-                        ."</a>"
-                        ."</h".$level.">";
-                       
-                       // Add the edit section link
-                       
-                       if($esr && !isset($wpPreview)) {
-                               $head[$c]=$sk->editSectionScript($c+1,$head[$c]);       
+                       if( $showEditLink ) {
+                               if ( empty( $head[$headlineCount] ) ) {
+                                       $head[$headlineCount] = "";
+                               }
+                               $head[$headlineCount] .= $sk->editSectionLink($headlineCount+1);
                        }
+                               
+                       # Add the edit section span
+                       if( $rightClickHack ) {
+                               $headline = $sk->editSectionScript($headlineCount+1,$headline); 
+                       }
+
+                       # give headline the correct <h#> tag
+                       @$head[$headlineCount] .= "<a name=\"$anchor\"></a><h".$level.$matches[2][$headlineCount] .$headline."</h".$level.">";
                        
-                       $numbering="";
-                       $c++;
-                       $dot=0;
+                       $headlineCount++;
                }               
 
-               if($st) {
-                       $toclines=$c;
-                       $toc.=$sk->tocUnindent($toclevel);
-                       $toc=$sk->tocTable($toc);
+               if( $doShowToc ) {
+                       $toclines = $headlineCount;
+                       $toc .= $sk->tocUnindent( $toclevel );
+                       $toc = $sk->tocTable( $toc );
                }
 
-               // split up and insert constructed headlines
+               # split up and insert constructed headlines
                
-               $blocks=preg_split("/<H[1-6].*?>.*?<\/H[1-6]>/i",$text);
-               $i=0;
+               $blocks = preg_split( "/<H[1-6].*?" . ">.*?<\/H[1-6]>/i", $text );
+               $i = 0;
 
-               foreach($blocks as $block) {
-                       if(($es) && !isset($wpPreview) && $c>0 && $i==0) {
+               foreach( $blocks as $block ) {
+                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
                            # This is the [edit] link that appears for the top block of text when 
                                # section editing is enabled
-                               $full.=$sk->editSectionLink(0);
+                               $full .= $sk->editSectionLink(0);
                        }
-                       $full.=$block;
-                       if($st && $toclines>3 && !$i) {
-                               # Let's add a top anchor just in case we want to link to the top of the page
-                               $full="<a name=\"top\"></a>".$full.$toc;
+                       $full .= $block;
+                       if( $doShowToc && !$i) {
+                       # Top anchor now in skin
+                               $full = $full.$toc;
                        }
 
-                       $full.=$head[$i];
+                       if( !empty( $head[$i] ) ) {
+                               $full .= $head[$i];
+                       }
                        $i++;
                }
                
                return $full;
        }
 
-       /* private */ function magicISBN( $text )
+       /* private */ function doMagicISBN( &$tokenizer )
        {
                global $wgLang;
 
-               $a = split( "ISBN ", " $text" );
-               if ( count ( $a ) < 2 ) return $text;
-               $text = substr( array_shift( $a ), 1);
-               $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+               # Check whether next token is a text token
+               # If yes, fetch it and convert the text into a
+               # Special::BookSources link
+               $token = $tokenizer->previewToken();
+               while ( $token["type"] == "" )
+               {
+                       $tokenizer->nextToken();
+                       $token = $tokenizer->previewToken();
+               }
+               if ( $token["type"] == "text" )
+               {
+                       $token = $tokenizer->nextToken();
+                       $x = $token["text"];
+                       $valid = "0123456789-ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 
-               foreach ( $a as $x ) {
                        $isbn = $blank = "" ;
                        while ( " " == $x{0} ) {
                                $blank .= " ";
                                $x = substr( $x, 1 );
                        }
-                       while ( strstr( $valid, $x{0} ) != false ) {
+                       while ( strstr( $valid, $x{0} ) != false ) {
                                $isbn .= $x{0};
                                $x = substr( $x, 1 );
                        }
                        $num = str_replace( "-", "", $isbn );
                        $num = str_replace( " ", "", $num );
-
+               
                        if ( "" == $num ) {
-                               $text .= "ISBN $blank$x";
+                               $text = "ISBN $blank$x";
                        } else {
-                               $text .= "<a href=\"" . wfLocalUrlE( $wgLang->specialPage(
-                                 "Booksources"), "isbn={$num}" ) . "\" class=\"internal\">ISBN $isbn</a>";
+                               $titleObj = Title::makeTitle( NS_SPECIAL, "Booksources" );
+                               $text = "<a href=\"" .
+                               $titleObj->escapeLocalUrl( "isbn={$num}" ) .
+                                       "\" class=\"internal\">ISBN $isbn</a>";
                                $text .= $x;
                        }
+               } else {
+                       $text = "ISBN ";
+               }
+               return $text;
+       }
+       /* private */ function doMagicRFC( &$tokenizer )
+       {
+               global $wgLang;
+
+               # Check whether next token is a text token
+               # If yes, fetch it and convert the text into a
+               # link to an RFC source
+               $token = $tokenizer->previewToken();
+               while ( $token["type"] == "" )
+               {
+                       $tokenizer->nextToken();
+                       $token = $tokenizer->previewToken();
+               }
+               if ( $token["type"] == "text" )
+               {
+                       $token = $tokenizer->nextToken();
+                       $x = $token["text"];
+                       $valid = "0123456789";
+
+                       $rfc = $blank = "" ;
+                       while ( " " == $x{0} ) {
+                               $blank .= " ";
+                               $x = substr( $x, 1 );
+                       }
+                       while ( strstr( $valid, $x{0} ) != false ) {
+                               $rfc .= $x{0};
+                               $x = substr( $x, 1 );
+                       }
+               
+                       if ( "" == $rfc ) {
+                               $text .= "RFC $blank$x";
+                       } else {
+                               $url = wfmsg( "rfcurl" );
+                               $url = str_replace( "$1", $rfc, $url);
+                               $sk =& $this->mOptions->getSkin();
+                               $la = $sk->getExternalLinkAttributes( $url, "RFC {$rfc}" );
+                               $text = "<a href='{$url}'{$la}>RFC {$rfc}</a>{$x}";
+                       }
+               } else {
+                       $text = "RFC ";
+               }
+               return $text;
+       }
+
+       function preSaveTransform( $text, &$title, &$user, $options, $clearState = true )
+       {
+               $this->mOptions = $options;
+               $this->mTitle =& $title;
+               $this->mOutputType = OT_WIKI;
+               
+               if ( $clearState ) {
+                       $this->clearState();
                }
+               
+               $stripState = false;
+               $text = str_replace("\r\n", "\n", $text);
+               $text = $this->strip( $text, $stripState, false );
+               $text = $this->pstPass2( $text, $user );
+               $text = $this->unstrip( $text, $stripState );
                return $text;
        }
 
-       /* private */ function magicRFC( $text )
+       /* private */ function pstPass2( $text, &$user )
        {
+               global $wgLang, $wgLocaltimezone, $wgCurParser;
+
+               # Variable replacement
+               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+               $text = $this->replaceVariables( $text );
+
+               # Signatures
+               #
+               $n = $user->getName();
+               $k = $user->getOption( "nickname" );
+               if ( "" == $k ) { $k = $n; }
+               if(isset($wgLocaltimezone)) {
+                       $oldtz = getenv("TZ"); putenv("TZ=$wgLocaltimezone");
+               }
+               /* Note: this is an ugly timezone hack for the European wikis */
+               $d = $wgLang->timeanddate( date( "YmdHis" ), false ) .
+                 " (" . date( "T" ) . ")";
+               if(isset($wgLocaltimezone)) putenv("TZ=$oldtz");
+
+               $text = preg_replace( "/~~~~~/", $d, $text );
+               $text = preg_replace( "/~~~~/", "[[" . $wgLang->getNsText(
+                 Namespace::getUser() ) . ":$n|$k]] $d", $text );
+               $text = preg_replace( "/~~~/", "[[" . $wgLang->getNsText(
+                 Namespace::getUser() ) . ":$n|$k]]", $text );
+
+               # Context links: [[|name]] and [[name (context)|]]
+               #
+               $tc = "[&;%\\-,.\\(\\)' _0-9A-Za-z\\/:\\x80-\\xff]";
+               $np = "[&;%\\-,.' _0-9A-Za-z\\/:\\x80-\\xff]"; # No parens
+               $namespacechar = '[ _0-9A-Za-z\x80-\xff]'; # Namespaces can use non-ascii!
+               $conpat = "/^({$np}+) \\(({$tc}+)\\)$/";
+
+               $p1 = "/\[\[({$np}+) \\(({$np}+)\\)\\|]]/";             # [[page (context)|]]
+               $p2 = "/\[\[\\|({$tc}+)]]/";                                    # [[|page]]
+               $p3 = "/\[\[($namespacechar+):({$np}+)\\|]]/";          # [[namespace:page|]]
+               $p4 = "/\[\[($namespacechar+):({$np}+) \\(({$np}+)\\)\\|]]/";
+                                                                                                               # [[ns:page (cont)|]]
+               $context = "";
+               $t = $this->mTitle->getText();
+               if ( preg_match( $conpat, $t, $m ) ) {
+                       $context = $m[2];
+               }
+               $text = preg_replace( $p4, "[[\\1:\\2 (\\3)|\\2]]", $text );
+               $text = preg_replace( $p1, "[[\\1 (\\2)|\\1]]", $text );
+               $text = preg_replace( $p3, "[[\\1:\\2|\\2]]", $text );
+
+               if ( "" == $context ) {
+                       $text = preg_replace( $p2, "[[\\1]]", $text );
+               } else {
+                       $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
+               }
+               
+               /*
+               $mw =& MagicWord::get( MAG_SUBST );
+               $wgCurParser = $this->fork();
+               $text = $mw->substituteCallback( $text, "wfBraceSubstitution" );
+               $this->merge( $wgCurParser );
+               */
+               
+               # Trim trailing whitespace
+               # MAG_END (__END__) tag allows for trailing 
+               # whitespace to be deliberately included
+               $text = rtrim( $text );
+               $mw =& MagicWord::get( MAG_END );
+               $mw->matchAndRemove( $text );
+
                return $text;
        }
 
+       # Set up some variables which are usually set up in parse()
+       # so that an external function can call some class members with confidence
+       function startExternalParse( &$title, $options, $outputType, $clearState = true ) 
+       {
+               $this->mTitle =& $title;
+               $this->mOptions = $options;
+               $this->mOutputType = $outputType;
+               if ( $clearState ) {
+                       $this->clearState();
+               }
+       }
 
+       function transformMsg( $text, $options ) {
+               global $wgTitle;
+               static $executing = false;
+               
+               # Guard against infinite recursion
+               if ( $executing ) {
+                       return $text;
+               }
+               $executing = true;
+
+               $this->mTitle = $wgTitle;
+               $this->mOptions = $options;
+               $this->mOutputType = OT_MSG;
+               $this->clearState();
+               $text = $this->replaceVariables( $text );
+               
+               $executing = false;
+               return $text;
+       }
 }
 
 class ParserOutput
@@ -1233,32 +1836,93 @@ class ParserOutput
        function setLanguageLinks( $ll ) { return wfSetVar( $this->mLanguageLinks, $ll ); }
        function setCategoryLinks( $cl ) { return wfSetVar( $this->mCategoryLinks, $cl ); }
        function setContainsOldMagic( $com ) { return wfSetVar( $this->mContainsOldMagic, $com ); }
-}
 
-# Regex callbacks, used in OutputPage::replaceVariables
-
-# Just get rid of the dangerous stuff
-# Necessary because replaceVariables is called after removeHTMLtags, 
-# and message text can come from any user
-function wfReplaceMsgVar( $matches ) {
-       global $wgCurOut, $wgLinkCache;
-       $text = $wgCurOut->removeHTMLtags( wfMsg( $matches[1] ) );
-       $wgLinkCache->suspend();
-       $text = $wgCurOut->replaceInternalLinks( $text );
-       $wgLinkCache->resume();
-       $wgLinkCache->addLinkObj( Title::makeTitle( NS_MEDIAWIKI, $matches[1] ) );
-       return $text;
-}
+       function merge( $other ) {
+               $this->mLanguageLinks = array_merge( $this->mLanguageLinks, $other->mLanguageLinks );
+               $this->mCategoryLinks = array_merge( $this->mCategoryLinks, $this->mLanguageLinks );
+               $this->mContainsOldMagic = $this->mContainsOldMagic || $other->mContainsOldMagic;
+       }
 
-# Effective <nowiki></nowiki>
-# Not real <nowiki> because this is called after nowiki sections are processed
-function wfReplaceMsgnwVar( $matches ) {
-       global $wgCurOut, $wgLinkCache;
-       $text = wfEscapeWikiText( wfMsg( $matches[1] ) );
-       $wgLinkCache->addLinkObj( Title::makeTitle( NS_MEDIAWIKI, $matches[1] ) );
-       return $text;
 }
 
+class ParserOptions
+{
+       # All variables are private
+       var $mUseTeX;                    # Use texvc to expand <math> tags
+       var $mUseCategoryMagic;          # Treat [[Category:xxxx]] tags specially
+       var $mUseDynamicDates;           # Use $wgDateFormatter to format dates
+       var $mInterwikiMagic;            # Interlanguage links are removed and returned in an array
+       var $mAllowExternalImages;       # Allow external images inline
+       var $mSkin;                      # Reference to the preferred skin
+       var $mDateFormat;                # Date format index
+       var $mEditSection;               # Create "edit section" links
+       var $mEditSectionOnRightClick;   # Generate JavaScript to edit section on right click
+       var $mNumberHeadings;            # Automatically number headings
+       var $mShowToc;                   # Show table of contents
+
+       function getUseTeX() { return $this->mUseTeX; }
+       function getUseCategoryMagic() { return $this->mUseCategoryMagic; }
+       function getUseDynamicDates() { return $this->mUseDynamicDates; }
+       function getInterwikiMagic() { return $this->mInterwikiMagic; }
+       function getAllowExternalImages() { return $this->mAllowExternalImages; }
+       function getSkin() { return $this->mSkin; }
+       function getDateFormat() { return $this->mDateFormat; }
+       function getEditSection() { return $this->mEditSection; }
+       function getEditSectionOnRightClick() { return $this->mEditSectionOnRightClick; }
+       function getNumberHeadings() { return $this->mNumberHeadings; }
+       function getShowToc() { return $this->mShowToc; }
+
+       function setUseTeX( $x ) { return wfSetVar( $this->mUseTeX, $x ); }
+       function setUseCategoryMagic( $x ) { return wfSetVar( $this->mUseCategoryMagic, $x ); }
+       function setUseDynamicDates( $x ) { return wfSetVar( $this->mUseDynamicDates, $x ); }
+       function setInterwikiMagic( $x ) { return wfSetVar( $this->mInterwikiMagic, $x ); }
+       function setAllowExternalImages( $x ) { return wfSetVar( $this->mAllowExternalImages, $x ); }
+       function setSkin( $x ) { return wfSetRef( $this->mSkin, $x ); }
+       function setDateFormat( $x ) { return wfSetVar( $this->mDateFormat, $x ); }
+       function setEditSection( $x ) { return wfSetVar( $this->mEditSection, $x ); }
+       function setEditSectionOnRightClick( $x ) { return wfSetVar( $this->mEditSectionOnRightClick, $x ); }
+       function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
+       function setShowToc( $x ) { return wfSetVar( $this->mShowToc, $x ); }
+
+       /* static */ function newFromUser( &$user ) 
+       {
+               $popts = new ParserOptions;
+               $popts->initialiseFromUser( &$user );
+               return $popts;
+       }
+
+       function initialiseFromUser( &$userInput ) 
+       {
+               global $wgUseTeX, $wgUseCategoryMagic, $wgUseDynamicDates, $wgInterwikiMagic, $wgAllowExternalImages;
+               
+               if ( !$userInput ) {
+                       $user = new User;
+                       $user->setLoaded( true );
+               } else {
+                       $user =& $userInput;
+               }
+
+               $this->mUseTeX = $wgUseTeX;
+               $this->mUseCategoryMagic = $wgUseCategoryMagic;
+               $this->mUseDynamicDates = $wgUseDynamicDates;
+               $this->mInterwikiMagic = $wgInterwikiMagic;
+               $this->mAllowExternalImages = $wgAllowExternalImages;
+               $this->mSkin =& $user->getSkin();
+               $this->mDateFormat = $user->getOption( "date" );
+               $this->mEditSection = $user->getOption( "editsection" );
+               $this->mEditSectionOnRightClick = $user->getOption( "editsectiononrightclick" );
+               $this->mNumberHeadings = $user->getOption( "numberheadings" );
+               $this->mShowToc = $user->getOption( "showtoc" );
+       }
+
+
+}
 
+# Regex callbacks, used in Parser::replaceVariables
+function wfBraceSubstitution( $matches )
+{
+       global $wgCurParser;
+       return $wgCurParser->braceSubstitution( $matches );
+}
 
 ?>