make doBlockLevels last parser stage again, and fix missing paragraphs from
[lhc/web/wiklou.git] / includes / Parser.php
index 3764a61..198c84d 100644 (file)
@@ -2,12 +2,19 @@
 
 include_once('Tokenizer.php');
 
+if( $GLOBALS['wgUseWikiHiero'] ){
+       include_once('wikihiero.php');
+}
+
 # PHP Parser 
 # 
-# Converts wikitext to HTML. 
+# 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: 
-#    objects:   $wgLang, $wgDateFormatter, $wgLinkCache, $wgCurOut
+#    objects:   $wgLang, $wgDateFormatter, $wgLinkCache, $wgCurParser
 #
 # NOT $wgArticle, $wgUser or $wgTitle. Keep them away!
 #
@@ -16,14 +23,37 @@ include_once('Tokenizer.php');
 #               $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
 {
        # Cleared with clearState():
-       var $mOutput, $mAutonumber, $mLastSection, $mDTopen, $mStripState;
+       var $mOutput, $mAutonumber, $mLastSection, $mDTopen, $mStripState = array();
+       var $mVariables, $mIncludeCount;
 
        # Temporary:
-       var $mOptions, $mTitle;
+       var $mOptions, $mTitle, $mOutputType;
 
        function Parser()
        {
@@ -36,7 +66,9 @@ class Parser
                $this->mAutonumber = 0;
                $this->mLastSection = "";
                $this->mDTopen = false;
-               $this->mStripState = false;
+               $this->mVariables = false;
+               $this->mIncludeCount = array();
+               $this->mStripState = array();
        }
        
        # First pass--just handle <nowiki> sections, pass the rest off
@@ -55,9 +87,10 @@ class Parser
                
                $this->mOptions = $options;
                $this->mTitle =& $title;
+               $this->mOutputType = OT_HTML;
                
                $stripState = NULL;
-               $text = $this->strip( $text, $this->mStripState, true );
+               $text = $this->strip( $text, $this->mStripState );
                $text = $this->doWikiPass2( $text, $linestart );
                $text = $this->unstrip( $text, $this->mStripState );
                
@@ -70,111 +103,101 @@ class Parser
        {
                return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
        }
-       
-       # Strips <nowiki>, <pre> and <math>
-       # Returns the text, and fills an array with data needed in unstrip()
-       #
-       function strip( $text, &$state, $render = true )
-       {
-               $state = array(
-                       'nwlist' => array(),
-                       'nwsecs' => 0,
-                       'nwunq' => Parser::getRandomString(),
-                       'mathlist' => array(),
-                       'mathsecs' => 0,
-                       'mathunq' => Parser::getRandomString(),
-                       'prelist' => array(),
-                       'presecs' => 0,
-                       'preunq' => Parser::getRandomString()
-               );
 
+       # 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 = "";
-               $stripped2 = "";
-               $stripped3 = "";
-               
-               # Replace any instances of the placeholders
-               $text = str_replace( $state['nwunq'], wfHtmlEscapeFirst( $state['nwunq'] ), $text );
-               $text = str_replace( $state['mathunq'], wfHtmlEscapeFirst( $state['mathunq'] ), $text );
-               $text = str_replace( $state['preunq'], wfHtmlEscapeFirst( $state['preunq'] ), $text );
-               
+
                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 );
-                               ++$state['nwsecs'];
+                               $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 ( $render ) {
-                                       $state['nwlist'][$state['nwsecs']] = wfEscapeHTMLTagsOnly($q[0]);
+       # 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>";
+                       }
+               }
+
+               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 {
-                                       $state['nwlist'][$state['nwsecs']] = "<nowiki>{$q[0]}</nowiki>";
+                                       $hiero_content[$marker] = "<hiero>$content</hiero>";
                                }
-                               
-                               $stripped .= $state['nwunq'] . sprintf("%08X", $state['nwsecs']);
-                               $text = $q[1];
                        }
                }
 
-               if( $this->mOptions->getUseTeX() ) {
-                       while ( "" != $stripped ) {
-                               $p = preg_split( "/<\\s*math\\s*>/i", $stripped, 2 );
-                               $stripped2 .= $p[0];
-                               if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { 
-                                       $stripped = ""; 
+               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 {
-                                       $q = preg_split( "/<\\/\\s*math\\s*>/i", $p[1], 2 );
-                                       ++$state['mathsecs'];
-
-                                       if ( $render ) {
-                                               $state['mathlist'][$state['mathsecs']] = renderMath($q[0]);
-                                       } else {
-                                               $state['mathlist'][$state['mathsecs']] = "<math>{$q[0]}</math>";
-                                       }
-                                       
-                                       $stripped2 .= $state['mathunq'] . sprintf("%08X", $state['mathsecs']);
-                                       $stripped = $q[1];
+                                       $math_content[$marker] = "<math>$content</math>";
                                }
                        }
-               } else {
-                       $stripped2 = $stripped;
                }
 
-               while ( "" != $stripped2 ) {
-                       $p = preg_split( "/<\\s*pre\\s*>/i", $stripped2, 2 );
-                       $stripped3 .= $p[0];
-                       if ( ( count( $p ) < 2 ) || ( "" == $p[1] ) ) { 
-                               $stripped2 = ""; 
+               $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 {
-                               $q = preg_split( "/<\\/\\s*pre\\s*>/i", $p[1], 2 );
-                               ++$state['presecs'];
-
-                               if ( $render ) {
-                                       $state['prelist'][$state['presecs']] = "<pre>". wfEscapeHTMLTagsOnly($q[0]). "</pre>\n";
-                               } else {
-                                       $state['prelist'][$state['presecs']] = "<pre>{$q[0]}</pre>";
-                               }
-                               
-                               $stripped3 .= $state['preunq'] . sprintf("%08X", $state['presecs']);
-                               $stripped2 = $q[1];
+                               $pre_content[$marker] = "<pre>$content</pre>";
                        }
                }
-               return $stripped3;
+               
+               # 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 )
        {
-               for ( $i = 1; $i <= $state['presecs']; ++$i ) {
-                       $text = str_replace( $state['preunq'] . sprintf("%08X", $i), $state['prelist'][$i], $text );
-               }
-
-               for ( $i = 1; $i <= $state['mathsecs']; ++$i ) {
-                       $text = str_replace( $state['mathunq'] . sprintf("%08X", $i), $state['mathlist'][$i], $text );          
-               }
-
-               for ( $i = 1; $i <= $state['nwsecs']; ++$i ) {
-                       $text = str_replace( $state['nwunq'] . sprintf("%08X", $i), $state['nwlist'][$i], $text );
+               foreach( $state as $content_dict ){
+                       foreach( $content_dict as $marker => $content ){
+                               $text = str_replace( $marker, $content, $text );
+                       }
                }
                return $text;
        }
@@ -184,11 +207,11 @@ class Parser
                global $wgLang , $wgUser ;
                if ( !$this->mOptions->getUseCategoryMagic() ) return ;
                $id = $this->mTitle->getArticleID() ;
-               $cat = ucfirst ( wfMsg ( "category" ) ) ;
+               $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 () ;
@@ -198,19 +221,19 @@ class Parser
 #              $sk =& $this->mGetSkin();
                $sk =& $wgUser->getSkin() ;
 
-               $doesexist = false ;
-               if ( $doesexist ) {
-                       $sql = "SELECT cur_title,cur_namespace FROM cur,links WHERE l_to={$id} AND l_from=cur_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 ( $sql2, DB_READ ) ;
+               while ( $x = wfFetchObject ( $res ) ) $data[] = $x ;
+
 
-               $res = wfQuery ( $sql, DB_READ ) ;
-               while ( $x = wfFetchObject ( $res ) )
+               foreach ( $data AS $x )
                {
-               #  $t = new Title ; 
-               #  $t->newFromDBkey ( $x->l_from ) ;
-               #  $t = $t->getText() ;
                        $t = $wgLang->getNsText ( $x->cur_namespace ) ;
                        if ( $t != "" ) $t .= ":" ;
                        $t .= $x->cur_title ;
@@ -298,7 +321,7 @@ class Parser
                                $fc = substr ( $x , 0 , 1 ) ;
                                if ( "{|" == substr ( $x , 0 , 2 ) )
                                {
-                                       $t[$k] = "<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
+                                       $t[$k] = "\n<table " . $this->fixTagAttributes ( substr ( $x , 3 ) ) . ">" ;
                                        array_push ( $td , false ) ;
                                        array_push ( $ltd , "" ) ;
                                        array_push ( $tr , false ) ;
@@ -358,9 +381,9 @@ class Parser
 
                                                $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" ;
+                                               if ( $fc == "|" ) $l = "td" ;
+                                               else if ( $fc == "!" ) $l = "th" ;
+                                               else if ( $fc == "+" ) $l = "caption" ;
                                                else $l = "" ;
                                                array_push ( $ltd , $l ) ;
                                                $y = explode ( "|" , $theline , 2 ) ;
@@ -391,17 +414,15 @@ class Parser
        #
        function doWikiPass2( $text, $linestart )
        {
-               $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 = $this->doHeadings( $text );
-               $text = $this->doBlockLevels( $text, $linestart );
                
                if($this->mOptions->getUseDynamicDates()) {
                        global $wgDateFormatter;
@@ -409,13 +430,24 @@ class Parser
                }
 
                $text = $this->replaceExternalLinks( $text );
-               $text = $this->replaceInternalLinks ( $text );
+               $text = $this->doTokenizedParser ( $text );
+
                $text = $this->doTableStuff ( $text ) ;
 
                $text = $this->formatHeadings( $text );
 
                $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 );
@@ -427,7 +459,7 @@ class Parser
        {
                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;
@@ -439,7 +471,7 @@ class Parser
 
        /* private */ function replaceExternalLinks( $text )
        {
-               $fname = "OutputPage::replaceExternalLinks";
+               $fname = "Parser::replaceExternalLinks";
                wfProfileIn( $fname );
                $text = $this->subReplaceExternalLinks( $text, "http", true );
                $text = $this->subReplaceExternalLinks( $text, "https", true );
@@ -506,8 +538,12 @@ class Parser
                                $s .= "[{$protocol}:" . $line;
                                continue;
                        }
-                       if ( $this->mOptions->getPrintable() ) $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}";
 
@@ -517,8 +553,8 @@ class Parser
 
        /* private */ function handle3Quotes( &$state, $token )
        {
-               if ( $state["strong"] ) {
-                       if ( $state["em"] && $state["em"] > $state["strong"] )
+               if ( $state["strong"] !== false ) {
+                       if ( $state["em"] !== false && $state["em"] > $state["strong"] )
                        {
                                # ''' lala ''lala '''
                                $s = "</em></strong><em>";
@@ -535,8 +571,8 @@ class Parser
 
        /* private */ function handle2Quotes( &$state, $token )
        {
-               if ( $state["em"] ) {
-                       if ( $state["strong"] && $state["strong"] > $state["em"] )
+               if ( $state["em"] !== false ) {
+                       if ( $state["strong"] !== false && $state["strong"] > $state["em"] )
                        {
                                # ''lala'''lala'' ....'''
                                $s = "</strong></em><strong>";
@@ -553,18 +589,19 @@ class Parser
        
        /* private */ function handle5Quotes( &$state, $token )
        {
-               if ( $state["em"] && $state["strong"] ) {
+               $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"] ) {
+               } elseif ( $state["em"] !== false ) {
                        $s .= "</em><strong>";
                        $state["em"] = FALSE;
                        $state["strong"] = $token["pos"];
-               } elseif ( $state["strong"] ) {
+               } elseif ( $state["strong"] !== false ) {
                        $s .= "</strong><em>";
                        $state["strong"] = FALSE;
                        $state["em"] = $token["pos"];
@@ -575,7 +612,7 @@ class Parser
                return $s;
        }
 
-       /* private */ function replaceInternalLinks( $str )
+       /* private */ function doTokenizedParser( $str )
        {
                global $wgLang; # for language specific parser hook
 
@@ -586,12 +623,12 @@ class Parser
                $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() )
                {
-echo $token["type"]."<br>";
                        switch ( $token["type"] )
                        {
                                case "text":
@@ -644,7 +681,8 @@ echo $token["type"]."<br>";
                                                        $nextToken = $tokenizer->nextToken();
                                                        $txt .= $nextToken["text"];
                                                }
-                                               $txt = $this->handleInternalLink( $txt, $prefix );
+                                               $fakestate = $this->mStripState;
+                                               $txt = $this->handleInternalLink( $this->unstrip($txt,$fakestate), $prefix );
 
                                                # did the tag start with 3 [ ?                                          
                                                if($threeopen) {
@@ -657,7 +695,7 @@ echo $token["type"]."<br>";
                                        $tagIsOpen = (count( $tokenStack ) != 0);
                                        break;
                                case "----":
-                                       $txt = "\n<hr>\n";
+                                       $txt = "\n<hr />\n";
                                        break;
                                case "'''":
                                        # This and the three next ones handle quotes
@@ -718,7 +756,7 @@ echo $token["type"]."<br>";
                                        $txt = $lastToken["text"] . $txt;
                                } else {
                                        $txt = $lastToken["type"] . $txt;
-                               }
+                               }       
                        }
                        $s .= $txt;
                }
@@ -729,7 +767,7 @@ echo $token["type"]."<br>";
        {
                global $wgLang, $wgLinkCache;
                global $wgNamespacesWithSubpages, $wgLanguageCode;
-               static $fname = "OutputPage::replaceInternalLinks" ;
+               static $fname = "Parser::handleInternalLink" ;
                wfProfileIn( $fname );
 
                wfProfileIn( "$fname-setup" );
@@ -811,8 +849,7 @@ echo $token["type"]."<br>";
                if( $noforce ) {
                        if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgLang->getLanguageName( $iw ) ) {
                                array_push( $this->mOutput->mLanguageLinks, $nt->getPrefixedText() );
-                               $s .= $prefix . $trail;
-                               return $s;
+                               return (trim($s) == '')? '': $s;
                        }
                        if( $ns == $image ) {
                                $s .= $prefix . $sk->makeImageLinkObj( $nt, $text ) . $trail;
@@ -825,18 +862,24 @@ echo $token["type"]."<br>";
                        $s .= $prefix . "<strong>" . $text . "</strong>" . $trail;
                        return $s;
                }
-               if ( $ns == $category && $this->mOptions->getUseCategoryMagic() ) {
+
+               # 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 ) ;
-#                      $t = $sk->makeKnownLink( $category.":".$t, $t, "", $trail , $prefix );
                        $nnt = Title::newFromText ( $category.":".$t ) ;
                        $t = $sk->makeLinkObj( $nnt, $t, "", $trail , $prefix );
-                       $this->mCategoryLinks[] = $t ;
+                       $this->mOutput->mCategoryLinks[] = $t ;
                        $s .= $prefix . $trail ;
-                       return $s ;         
+                       return $s ;
                }
+
                if( $ns == $media ) {
                        $s .= $prefix . $sk->makeMediaLinkObj( $nt, $text ) . $trail;
                        $wgLinkCache->addImageLinkObj( $nt );
@@ -856,8 +899,7 @@ echo $token["type"]."<br>";
        /* private */ function closeParagraph()
        {
                $result = "";
-               if ( 0 != strcmp( "p", $this->mLastSection ) &&
-                 0 != strcmp( "", $this->mLastSection ) ) {
+               if ( '' != $this->mLastSection ) {
                        $result = "</" . $this->mLastSection  . ">";
                }
                $this->mLastSection = "";
@@ -931,14 +973,14 @@ echo $token["type"]."<br>";
 
        /* 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 ); }
@@ -990,32 +1032,32 @@ echo $token["type"]."<br>";
                                $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;
                                }
                        }
@@ -1026,74 +1068,270 @@ echo $token["type"]."<br>";
                        --$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 );
+               }
+
+               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;
+       }
 
-               $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 );
+       # Merges a copy split off with fork()
+       function merge( &$copy )
+       {
+               $this->mOutput->merge( $copy->mOutput );
                
-               $this->mOutput->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->mOutput->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",
@@ -1183,46 +1421,62 @@ echo $token["type"]."<br>";
  *
  * 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 )
        {
-               $nh=$this->mOptions->getNumberHeadings();
-               $st=$this->mOptions->getShowToc();
-               if(!$this->mTitle->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=$this->mOptions->getEditSection();
-                       $esr=$this->mOptions->getEditSectionOnRightClick();
+                       $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($this->mTitle->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 =& $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
@@ -1230,106 +1484,115 @@ echo $token["type"]."<br>";
                $toc = "";
                $full = "";
                $head = array();
-               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;
-                       
+               $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
-                       // Avoid insertion of weird stuff like <math> by expanding the relevant sections
-                       $canonized_headline=Parser::unstrip( $headline, $this->mStripState );
-                       $canonized_headline=preg_replace("/<.*?>/","",$canonized_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
+                       # 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 );
                        
-                       if($nh||$st) {
-                               $tocline=$numbering ." ". $tocline;
+                       # 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;
+                       
+                       # 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( $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) {
-                               $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) {
-                               $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) && $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;
                        }
 
                        if( !empty( $head[$i] ) ) {
@@ -1433,7 +1696,9 @@ echo $token["type"]."<br>";
        function preSaveTransform( $text, &$title, &$user, $options, $clearState = true )
        {
                $this->mOptions = $options;
-               $this->mTitle = $title;
+               $this->mTitle =& $title;
+               $this->mOutputType = OT_WIKI;
+               
                if ( $clearState ) {
                        $this->clearState();
                }
@@ -1448,7 +1713,11 @@ echo $token["type"]."<br>";
 
        /* private */ function pstPass2( $text, &$user )
        {
-               global $wgLang, $wgLocaltimezone;
+               global $wgLang, $wgLocaltimezone, $wgCurParser;
+
+               # Variable replacement
+               # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
+               $text = $this->replaceVariables( $text );
 
                # Signatures
                #
@@ -1463,6 +1732,7 @@ echo $token["type"]."<br>";
                  " (" . 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(
@@ -1495,11 +1765,13 @@ echo $token["type"]."<br>";
                        $text = preg_replace( $p2, "[[\\1 ({$context})|\\1]]", $text );
                }
                
-               # {{SUBST:xxx}} variables
-               #
+               /*
                $mw =& MagicWord::get( MAG_SUBST );
-               $text = $mw->substituteCallback( $text, "wfReplaceSubstVar" );
-
+               $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
@@ -1510,7 +1782,37 @@ echo $token["type"]."<br>";
                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
@@ -1534,6 +1836,13 @@ 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 ); }
+
+       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;
+       }
+
 }
 
 class ParserOptions
@@ -1548,7 +1857,6 @@ class ParserOptions
        var $mDateFormat;                # Date format index
        var $mEditSection;               # Create "edit section" links
        var $mEditSectionOnRightClick;   # Generate JavaScript to edit section on right click
-       var $mPrintable;                 # Generate printable output
        var $mNumberHeadings;            # Automatically number headings
        var $mShowToc;                   # Show table of contents
 
@@ -1561,7 +1869,6 @@ class ParserOptions
        function getDateFormat() { return $this->mDateFormat; }
        function getEditSection() { return $this->mEditSection; }
        function getEditSectionOnRightClick() { return $this->mEditSectionOnRightClick; }
-       function getPrintable() { return $this->mPrintable; }
        function getNumberHeadings() { return $this->mNumberHeadings; }
        function getShowToc() { return $this->mShowToc; }
 
@@ -1574,7 +1881,6 @@ class ParserOptions
        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 setPrintable( $x ) { return wfSetVar( $this->mPrintable, $x ); }
        function setNumberHeadings( $x ) { return wfSetVar( $this->mNumberHeadings, $x ); }
        function setShowToc( $x ) { return wfSetVar( $this->mShowToc, $x ); }
 
@@ -1591,6 +1897,7 @@ class ParserOptions
                
                if ( !$userInput ) {
                        $user = new User;
+                       $user->setLoaded( true );
                } else {
                        $user =& $userInput;
                }
@@ -1604,38 +1911,18 @@ class ParserOptions
                $this->mDateFormat = $user->getOption( "date" );
                $this->mEditSection = $user->getOption( "editsection" );
                $this->mEditSectionOnRightClick = $user->getOption( "editsectiononrightclick" );
-               $this->mPrintable = false;
                $this->mNumberHeadings = $user->getOption( "numberheadings" );
                $this->mShowToc = $user->getOption( "showtoc" );
        }
 
 
 }
-       
-# 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;
-}
 
-# 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;
+# Regex callbacks, used in Parser::replaceVariables
+function wfBraceSubstitution( $matches )
+{
+       global $wgCurParser;
+       return $wgCurParser->braceSubstitution( $matches );
 }
 
-
-
 ?>