]> scripts.mit.edu Git - autoinstallsdev/mediawiki.git/blobdiff - includes/parser/Parser.php
MediaWiki 1.17.3
[autoinstallsdev/mediawiki.git] / includes / parser / Parser.php
index e6a6878249daebc43776ecee2c09fa75c8f0d9ba..eddfd43cc4b1e077268d3c2307009fa1219833b9 100644 (file)
  * <pre>
  * There are five main entry points into the Parser class:
  * parse()
- *   produces HTML output
+ *     produces HTML output
  * preSaveTransform().
- *   produces altered wiki markup.
+ *     produces altered wiki markup.
  * preprocess()
- *   removes HTML comments and expands templates
- * cleanSig()
- *   Cleans a signature before saving it to preferences
+ *     removes HTML comments and expands templates
+ * cleanSig() / cleanSigInSig()
+ *     Cleans a signature before saving it to preferences
  * extractSections()
- *   Extracts sections from an article for section editing
+ *     Extracts sections from an article for section editing
+ * getPreloadText()
+ *     Removes <noinclude> sections, and <includeonly> tags.
  *
  * Globals used:
  *    objects:   $wgLang, $wgContLang
@@ -43,8 +45,7 @@
  *
  * @ingroup Parser
  */
-class Parser
-{
+class Parser {
        /**
         * Update this version number when the ParserOutput format
         * changes in an incompatible way, so the parser cache
@@ -63,7 +64,7 @@ class Parser
        const EXT_IMAGE_REGEX = '/^(http:\/\/|https:\/\/)([^][<>"\\x00-\\x20\\x7F]+)
                \\/([A-Za-z0-9_.,~%\\-+&;#*?!=()@\\x80-\\xFF]+)\\.((?i)gif|png|jpg|jpeg)$/Sx';
 
-       // State constants for the definition list colon extraction
+       # State constants for the definition list colon extraction
        const COLON_STATE_TEXT = 0;
        const COLON_STATE_TAG = 1;
        const COLON_STATE_TAGSTART = 2;
@@ -73,26 +74,25 @@ class Parser
        const COLON_STATE_COMMENTDASH = 6;
        const COLON_STATE_COMMENTDASHDASH = 7;
 
-       // Flags for preprocessToDom
+       # Flags for preprocessToDom
        const PTD_FOR_INCLUSION = 1;
 
-       // Allowed values for $this->mOutputType
-       // Parameter to startExternalParse().
-       const OT_HTML = 1;
-       const OT_WIKI = 2;
-       const OT_PREPROCESS = 3;
+       # Allowed values for $this->mOutputType
+       # Parameter to startExternalParse().
+       const OT_HTML = 1; # like parse()
+       const OT_WIKI = 2; # like preSaveTransform()
+       const OT_PREPROCESS = 3; # like preprocess()
        const OT_MSG = 3;
+       const OT_PLAIN = 4; # like extractSections() - portions of the original are returned unchanged.
 
-       // Marker Suffix needs to be accessible staticly.
+       # Marker Suffix needs to be accessible staticly.
        const MARKER_SUFFIX = "-QINU\x7f";
 
-       /**#@+
-        * @private
-        */
        # Persistent:
-       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
-               $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
-               $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
+       var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables;
+       var $mSubstWords, $mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex;
+       var $mPreprocessor, $mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList;
+       var $mVarCache, $mConf, $mFunctionTagHooks;
 
 
        # Cleared with clearState():
@@ -100,22 +100,19 @@ class Parser
        var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
        var $mLinkHolders, $mLinkID;
        var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
-       var $mTplExpandCache; // empty-frame expansion cache
+       var $mTplExpandCache; # empty-frame expansion cache
        var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
-       var $mExpensiveFunctionCount; // number of expensive parser function calls
-       var $mFileCache;
+       var $mExpensiveFunctionCount; # number of expensive parser function calls
 
        # Temporary
        # These are variables reset at least once per parse regardless of $clearState
-       var $mOptions,      // ParserOptions object
-               $mTitle,        // Title context, used for self-link rendering and similar things
-               $mOutputType,   // Output type, one of the OT_xxx constants
-               $ot,            // Shortcut alias, see setOutputType()
-               $mRevisionId,   // ID to display in {{REVISIONID}} tags
-               $mRevisionTimestamp, // The timestamp of the specified revision ID
-               $mRevIdForTs;   // The revision ID which was used to fetch the timestamp
-
-       /**#@-*/
+       var $mOptions;      # ParserOptions object
+       var $mTitle;        # Title context, used for self-link rendering and similar things
+       var $mOutputType;   # Output type, one of the OT_xxx constants
+       var $ot;            # Shortcut alias, see setOutputType()
+       var $mRevisionId;   # ID to display in {{REVISIONID}} tags
+       var $mRevisionTimestamp; # The timestamp of the specified revision ID
+       var $mRevIdForTs;   # The revision ID which was used to fetch the timestamp
 
        /**
         * Constructor
@@ -127,16 +124,17 @@ class Parser
                $this->mTagHooks = array();
                $this->mTransparentTagHooks = array();
                $this->mFunctionHooks = array();
+               $this->mFunctionTagHooks = array();
                $this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
-               $this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+               $this->mDefaultStripList = $this->mStripList = array();
                $this->mUrlProtocols = wfUrlProtocols();
                $this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
-                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
+                       '[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x00-\\x08\\x0a-\\x1F]*?)\]/S';
                $this->mVarCache = array();
                if ( isset( $conf['preprocessorClass'] ) ) {
                        $this->mPreprocessorClass = $conf['preprocessorClass'];
                } elseif ( extension_loaded( 'domxml' ) ) {
-                       // PECL extension that conflicts with the core DOM extension (bug 13770)
+                       # PECL extension that conflicts with the core DOM extension (bug 13770)
                        wfDebug( "Warning: you have the obsolete domxml extension for PHP. Please remove it!\n" );
                        $this->mPreprocessorClass = 'Preprocessor_Hash';
                } elseif ( extension_loaded( 'dom' ) ) {
@@ -171,8 +169,8 @@ class Parser
 
                wfProfileIn( __METHOD__ );
 
-               $this->setHook( 'pre', array( $this, 'renderPreTag' ) );
                CoreParserFunctions::register( $this );
+               CoreTagHooks::register( $this );
                $this->initialiseVariables();
 
                wfRunHooks( 'ParserFirstCallInit', array( &$this ) );
@@ -190,6 +188,7 @@ class Parser
                        $this->firstCallInit();
                }
                $this->mOutput = new ParserOutput;
+               $this->mOptions->registerWatcher( array( $this->mOutput, 'recordOption' ) );
                $this->mAutonumber = 0;
                $this->mLastSection = '';
                $this->mDTopen = false;
@@ -200,6 +199,7 @@ class Parser
                $this->mLinkHolders = new LinkHolderArray( $this );
                $this->mLinkID = 0;
                $this->mRevisionTimestamp = $this->mRevisionId = null;
+               $this->mVarCache = array();
 
                /**
                 * Prefix for temporary replacement strings for the multipass parser.
@@ -211,7 +211,7 @@ class Parser
                 * Must not consist of all title characters, or else it will change
                 * the behaviour of <nowiki> in a link.
                 */
-               #$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
+               # $this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
                # Changed to \x7f to allow XML double-parsing -- TS
                $this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
 
@@ -230,7 +230,6 @@ class Parser
                $this->mHeadings = array();
                $this->mDoubleUnderscores = array();
                $this->mExpensiveFunctionCount = 0;
-               $this->mFileCache = array();
 
                # Fix cloning
                if ( isset( $this->mPreprocessor ) && $this->mPreprocessor->parser !== $this ) {
@@ -241,50 +240,6 @@ class Parser
                wfProfileOut( __METHOD__ );
        }
 
-       function setOutputType( $ot ) {
-               $this->mOutputType = $ot;
-               // Shortcut alias
-               $this->ot = array(
-                       'html' => $ot == self::OT_HTML,
-                       'wiki' => $ot == self::OT_WIKI,
-                       'pre' => $ot == self::OT_PREPROCESS,
-               );
-       }
-
-       /**
-        * Set the context title
-        */
-       function setTitle( $t ) {
-               if ( !$t || $t instanceof FakeTitle ) {
-                       $t = Title::newFromText( 'NO TITLE' );
-               }
-               if ( strval( $t->getFragment() ) !== '' ) {
-                       # Strip the fragment to avoid various odd effects
-                       $this->mTitle = clone $t;
-                       $this->mTitle->setFragment( '' );
-               } else {
-                       $this->mTitle = $t;
-               }
-       }
-
-       /**
-        * Accessor for mUniqPrefix.
-        *
-        * @public
-        */
-       function uniqPrefix() {
-               if( !isset( $this->mUniqPrefix ) ) {
-                       // @fixme this is probably *horribly wrong*
-                       // LanguageConverter seems to want $wgParser's uniqPrefix, however
-                       // if this is called for a parser cache hit, the parser may not
-                       // have ever been initialized in the first place.
-                       // Not really sure what the heck is supposed to be going on here.
-                       return '';
-                       //throw new MWException( "Accessing uninitialized mUniqPrefix" );
-               }
-               return $this->mUniqPrefix;
-       }
-
        /**
         * Convert wikitext to HTML
         * Do not call this function recursively.
@@ -303,20 +258,21 @@ class Parser
                 * to internalParse() which does all the real work.
                 */
 
-               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
+               global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang, $wgDisableLangConversion, $wgDisableTitleConversion;
                $fname = __METHOD__.'-' . wfGetCaller();
                wfProfileIn( __METHOD__ );
                wfProfileIn( $fname );
 
+               $this->mOptions = $options;
                if ( $clearState ) {
                        $this->clearState();
                }
 
-               $this->mOptions = $options;
-               $this->setTitle( $title );
+               $this->setTitle( $title ); # Page title has to be set for the pre-processor
+
                $oldRevisionId = $this->mRevisionId;
                $oldRevisionTimestamp = $this->mRevisionTimestamp;
-               if( $revid !== null ) {
+               if ( $revid !== null ) {
                        $this->mRevisionId = $revid;
                        $this->mRevisionTimestamp = null;
                }
@@ -325,28 +281,62 @@ class Parser
                # No more strip!
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
                $text = $this->internalParse( $text );
+
                $text = $this->mStripState->unstripGeneral( $text );
 
                # Clean up special characters, only run once, next-to-last before doBlockLevels
                $fixtags = array(
                        # french spaces, last one Guillemet-left
                        # only if there is something before the space
-                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&nbsp;\\2',
+                       '/(.) (?=\\?|:|;|!|%|\\302\\273)/' => '\\1&#160;\\2',
                        # french spaces, Guillemet-right
-                       '/(\\302\\253) /' => '\\1&nbsp;',
-                       '/&nbsp;(!\s*important)/' => ' \\1', #Beware of CSS magic word !important, bug #11874.
+                       '/(\\302\\253) /' => '\\1&#160;',
+                       '/&#160;(!\s*important)/' => ' \\1', # Beware of CSS magic word !important, bug #11874.
                );
-               $text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
+               $text = preg_replace( array_keys( $fixtags ), array_values( $fixtags ), $text );
 
                $text = $this->doBlockLevels( $text, $linestart );
 
                $this->replaceLinkHolders( $text );
 
-               # the position of the parserConvert() call should not be changed. it
-               # assumes that the links are all replaced and the only thing left
-               # is the <nowiki> mark.
-               # Side-effects: this calls $this->mOutput->setTitleText()
-               $text = $wgContLang->parserConvert( $text, $this );
+               /**
+                * The page doesn't get language converted if
+                * a) It's disabled
+                * b) Content isn't converted
+                * c) It's a conversion table
+                */
+               if ( !( $wgDisableLangConversion
+                               || isset( $this->mDoubleUnderscores['nocontentconvert'] )
+                               || $this->mTitle->isConversionTable() ) ) {
+
+                       # The position of the convert() call should not be changed. it
+                       # assumes that the links are all replaced and the only thing left
+                       # is the <nowiki> mark.
+
+                       $text = $wgContLang->convert( $text );
+               }
+
+               /**
+                * A converted title will be provided in the output object if title and
+                * content conversion are enabled, the article text does not contain
+                * a conversion-suppressing double-underscore tag, and no
+                * {{DISPLAYTITLE:...}} is present. DISPLAYTITLE takes precedence over
+                * automatic link conversion.
+                */
+               if ( !( $wgDisableLangConversion
+                               || $wgDisableTitleConversion
+                               || isset( $this->mDoubleUnderscores['nocontentconvert'] )
+                               || isset( $this->mDoubleUnderscores['notitleconvert'] )
+                               || $this->mOutput->getDisplayTitle() !== false ) ) 
+               {
+                       $convruletitle = $wgContLang->getConvRuleTitle();
+                       if ( $convruletitle ) {
+                               $this->mOutput->setTitleText( $convruletitle );
+                       } else {
+                               $titleText = $wgContLang->convertTitle( $title );
+                               $this->mOutput->setTitleText( $titleText );
+                       }
+               }
 
                $text = $this->mStripState->unstripNoWiki( $text );
 
@@ -357,14 +347,13 @@ class Parser
                $uniq_prefix = $this->mUniqPrefix;
                $matches = array();
                $elements = array_keys( $this->mTransparentTagHooks );
-               $text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+               $text = $this->extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
 
-               foreach( $matches as $marker => $data ) {
+               foreach ( $matches as $marker => $data ) {
                        list( $element, $content, $params, $tag ) = $data;
                        $tagName = strtolower( $element );
-                       if( isset( $this->mTransparentTagHooks[$tagName] ) ) {
-                               $output = call_user_func_array( $this->mTransparentTagHooks[$tagName],
-                                       array( $content, $params, $this ) );
+                       if ( isset( $this->mTransparentTagHooks[$tagName] ) ) {
+                               $output = call_user_func_array( $this->mTransparentTagHooks[$tagName], array( $content, $params, $this ) );
                        } else {
                                $output = $tag;
                        }
@@ -374,7 +363,7 @@ class Parser
 
                $text = Sanitizer::normalizeCharReferences( $text );
 
-               if ( ( $wgUseTidy && $this->mOptions->mTidy ) || $wgAlwaysUseTidy ) {
+               if ( ( $wgUseTidy && $this->mOptions->getTidy() ) || $wgAlwaysUseTidy ) {
                        $text = MWTidy::tidy( $text );
                } else {
                        # attempt to sanitize at least some nesting problems
@@ -412,12 +401,11 @@ class Parser
 
                # Information on include size limits, for the benefit of users who try to skirt them
                if ( $this->mOptions->getEnableLimitReport() ) {
-                       global $wgExpensiveParserFunctionLimit;
                        $max = $this->mOptions->getMaxIncludeSize();
                        $PFreport = "Expensive parser function count: {$this->mExpensiveFunctionCount}/$wgExpensiveParserFunctionLimit\n";
                        $limitReport =
                                "NewPP limit report\n" .
-                               "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->mMaxPPNodeCount}\n" .
+                               "Preprocessor node count: {$this->mPPNodeCount}/{$this->mOptions->getMaxPPNodeCount()}\n" .
                                "Post-expand include size: {$this->mIncludeSizes['post-expand']}/$max bytes\n" .
                                "Template argument size: {$this->mIncludeSizes['arg']}/$max bytes\n".
                                $PFreport;
@@ -425,6 +413,7 @@ class Parser
                        $text .= "\n<!-- \n$limitReport-->\n";
                }
                $this->mOutput->setText( $text );
+
                $this->mRevisionId = $oldRevisionId;
                $this->mRevisionTimestamp = $oldRevisionTimestamp;
                wfProfileOut( $fname );
@@ -436,12 +425,17 @@ class Parser
        /**
         * Recursive parser entry point that can be called from an extension tag
         * hook.
+        *
+        * If $frame is not provided, then template variables (e.g., {{{1}}}) within $text are not expanded
+        *
+        * @param $text String: text extension wants to have parsed
+        * @param $frame PPFrame: The frame to use for expanding any template variables
         */
-       function recursiveTagParse( $text ) {
+       function recursiveTagParse( $text, $frame=false ) {
                wfProfileIn( __METHOD__ );
                wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
                wfRunHooks( 'ParserAfterStrip', array( &$this, &$text, &$this->mStripState ) );
-               $text = $this->internalParse( $text );
+               $text = $this->internalParse( $text, false, $frame );
                wfProfileOut( __METHOD__ );
                return $text;
        }
@@ -450,13 +444,13 @@ class Parser
         * Expand templates and variables in the text, producing valid, static wikitext.
         * Also removes comments.
         */
-       function preprocess( $text, $title, $options, $revid = null ) {
+       function preprocess( $text, Title $title, ParserOptions $options, $revid = null ) {
                wfProfileIn( __METHOD__ );
+               $this->mOptions = $options;
                $this->clearState();
                $this->setOutputType( self::OT_PREPROCESS );
-               $this->mOptions = $options;
                $this->setTitle( $title );
-               if( $revid !== null ) {
+               if ( $revid !== null ) {
                        $this->mRevisionId = $revid;
                }
                wfRunHooks( 'ParserBeforeStrip', array( &$this, &$text, &$this->mStripState ) );
@@ -467,21 +461,147 @@ class Parser
                return $text;
        }
 
+       /**
+        * Process the wikitext for the ?preload= feature. (bug 5210)
+        *
+        * <noinclude>, <includeonly> etc. are parsed as for template transclusion,
+        * comments, templates, arguments, tags hooks and parser functions are untouched.
+        */
+       public function getPreloadText( $text, Title $title, ParserOptions $options ) {
+               # Parser (re)initialisation
+               $this->mOptions = $options;
+               $this->clearState();
+               $this->setOutputType( self::OT_PLAIN );
+               $this->setTitle( $title );
+
+               $flags = PPFrame::NO_ARGS | PPFrame::NO_TEMPLATES;
+               $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
+               $text = $this->getPreprocessor()->newFrame()->expand( $dom, $flags );
+               $text = $this->mStripState->unstripBoth( $text );
+               return $text;
+       }
+
        /**
         * Get a random string
         *
         * @private
         * @static
         */
-       function getRandomString() {
-               return dechex(mt_rand(0, 0x7fffffff)) . dechex(mt_rand(0, 0x7fffffff));
+       static private function getRandomString() {
+               return dechex( mt_rand( 0, 0x7fffffff ) ) . dechex( mt_rand( 0, 0x7fffffff ) );
+       }
+
+       /**
+        * Accessor for mUniqPrefix.
+        *
+        * @return String
+        */
+       public function uniqPrefix() {
+               if ( !isset( $this->mUniqPrefix ) ) {
+                       # @todo Fixme: this is probably *horribly wrong*
+                       # LanguageConverter seems to want $wgParser's uniqPrefix, however
+                       # if this is called for a parser cache hit, the parser may not
+                       # have ever been initialized in the first place.
+                       # Not really sure what the heck is supposed to be going on here.
+                       return '';
+                       # throw new MWException( "Accessing uninitialized mUniqPrefix" );
+               }
+               return $this->mUniqPrefix;
        }
 
-       function &getTitle() { return $this->mTitle; }
-       function getOptions() { return $this->mOptions; }
-       function getRevisionId() { return $this->mRevisionId; }
-       function getOutput() { return $this->mOutput; }
-       function nextLinkID() { return $this->mLinkID++; }
+       /**
+        * Set the context title
+        */
+       function setTitle( $t ) {
+               if ( !$t || $t instanceof FakeTitle ) {
+                       $t = Title::newFromText( 'NO TITLE' );
+               }
+
+               if ( strval( $t->getFragment() ) !== '' ) {
+                       # Strip the fragment to avoid various odd effects
+                       $this->mTitle = clone $t;
+                       $this->mTitle->setFragment( '' );
+               } else {
+                       $this->mTitle = $t;
+               }
+       }
+
+       /**
+        * Accessor for the Title object
+        *
+        * @return Title object
+        */
+       function getTitle() {
+               return $this->mTitle;
+       }
+
+       /**
+        * Accessor/mutator for the Title object
+        *
+        * @param $x New Title object or null to just get the current one
+        * @return Title object
+        */
+       function Title( $x = null ) {
+               return wfSetVar( $this->mTitle, $x );
+       }
+
+       /**
+        * Set the output type
+        *
+        * @param $ot Integer: new value
+        */
+       function setOutputType( $ot ) {
+               $this->mOutputType = $ot;
+               # Shortcut alias
+               $this->ot = array(
+                       'html' => $ot == self::OT_HTML,
+                       'wiki' => $ot == self::OT_WIKI,
+                       'pre' => $ot == self::OT_PREPROCESS,
+                       'plain' => $ot == self::OT_PLAIN,
+               );
+       }
+
+       /**
+        * Accessor/mutator for the output type
+        *
+        * @param $x New value or null to just get the current one
+        * @return Integer
+        */
+       function OutputType( $x = null ) {
+               return wfSetVar( $this->mOutputType, $x );
+       }
+
+       /**
+        * Get the ParserOutput object
+        *
+        * @return ParserOutput object
+        */
+       function getOutput() {
+               return $this->mOutput;
+       }
+
+       /**
+        * Get the ParserOptions object
+        *
+        * @return ParserOptions object
+        */
+       function getOptions() {
+               return $this->mOptions;
+       }
+
+       /**
+        * Accessor/mutator for the ParserOptions object
+        *
+        * @param $x New value or null to just get the current one
+        * @return Current ParserOptions object
+        */
+       function Options( $x = null ) {
+               return wfSetVar( $this->mOptions, $x );
+       }
+
+       function nextLinkID() {
+               return $this->mLinkID++;
+       }
 
        function getFunctionLang() {
                global $wgLang, $wgContLang;
@@ -496,6 +616,8 @@ class Parser
 
        /**
         * Get a preprocessor object
+        *
+        * @return Preprocessor instance
         */
        function getPreprocessor() {
                if ( !isset( $this->mPreprocessor ) ) {
@@ -518,56 +640,57 @@ class Parser
         *
         * @param $elements list of element names. Comments are always extracted.
         * @param $text Source text string.
+        * @param $matches Out parameter, Array: extracted tags
         * @param $uniq_prefix
+        * @return String: stripped text
         *
-        * @public
         * @static
         */
-       function extractTagsAndParams($elements, $text, &$matches, $uniq_prefix = ''){
+       public function extractTagsAndParams( $elements, $text, &$matches, $uniq_prefix = '' ) {
                static $n = 1;
                $stripped = '';
                $matches = array();
 
                $taglist = implode( '|', $elements );
-               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?>)|<(!--)/i";
+               $start = "/<($taglist)(\\s+[^>]*?|\\s*?)(\/?" . ">)|<(!--)/i";
 
-               while ( '' != $text ) {
+               while ( $text != '' ) {
                        $p = preg_split( $start, $text, 2, PREG_SPLIT_DELIM_CAPTURE );
                        $stripped .= $p[0];
-                       if( count( $p ) < 5 ) {
+                       if ( count( $p ) < 5 ) {
                                break;
                        }
-                       if( count( $p ) > 5 ) {
-                               // comment
+                       if ( count( $p ) > 5 ) {
+                               # comment
                                $element    = $p[4];
                                $attributes = '';
                                $close      = '';
                                $inside     = $p[5];
                        } else {
-                               // tag
+                               # tag
                                $element    = $p[1];
                                $attributes = $p[2];
                                $close      = $p[3];
                                $inside     = $p[4];
                        }
 
-                       $marker = "$uniq_prefix-$element-" . sprintf('%08X', $n++) . self::MARKER_SUFFIX;
+                       $marker = "$uniq_prefix-$element-" . sprintf( '%08X', $n++ ) . self::MARKER_SUFFIX;
                        $stripped .= $marker;
 
                        if ( $close === '/>' ) {
-                               // Empty element tag, <tag />
+                               # Empty element tag, <tag />
                                $content = null;
                                $text = $inside;
                                $tail = null;
                        } else {
-                               if( $element === '!--' ) {
+                               if ( $element === '!--' ) {
                                        $end = '/(-->)/';
                                } else {
                                        $end = "/(<\\/$element\\s*>)/i";
                                }
                                $q = preg_split( $end, $inside, 2, PREG_SPLIT_DELIM_CAPTURE );
                                $content = $q[0];
-                               if( count( $q ) < 3 ) {
+                               if ( count( $q ) < 3 ) {
                                        # No end tag -- let it run out to the end of the text.
                                        $tail = '';
                                        $text = '';
@@ -589,21 +712,13 @@ class Parser
         * Get a list of strippable XML-like elements
         */
        function getStripList() {
-               global $wgRawHtml;
-               $elements = $this->mStripList;
-               if( $wgRawHtml ) {
-                       $elements[] = 'html';
-               }
-               if( $this->mOptions->getUseTeX() ) {
-                       $elements[] = 'math';
-               }
-               return $elements;
+               return $this->mStripList;
        }
 
        /**
         * @deprecated use replaceVariables
         */
-       function strip( $text, $state, $stripcomments = false , $dontstrip = array () ) {
+       function strip( $text, $state, $stripcomments = false , $dontstrip = array() ) {
                return $text;
        }
 
@@ -648,14 +763,14 @@ class Parser
                $this->mStripState->general->setPair( $rnd, $text );
                return $rnd;
        }
-       
+
        /**
         * Interface with html tidy
         * @deprecated Use MWTidy::tidy()
         */
        public static function tidy( $text ) {
                wfDeprecated( __METHOD__ );
-               return MWTidy::tidy( $text );   
+               return MWTidy::tidy( $text );
        }
 
        /**
@@ -663,191 +778,188 @@ class Parser
         *
         * @private
         */
-       function doTableStuff ( $text ) {
+       function doTableStuff( $text ) {
                wfProfileIn( __METHOD__ );
-
+               
                $lines = StringUtils::explode( "\n", $text );
                $out = '';
-               $td_history = array (); // Is currently a td tag open?
-               $last_tag_history = array (); // Save history of last lag activated (td, th or caption)
-               $tr_history = array (); // Is currently a tr tag open?
-               $tr_attributes = array (); // history of tr attributes
-               $has_opened_tr = array(); // Did this table open a <tr> element?
-               $indent_level = 0; // indent level of the table
+               $td_history = array(); # Is currently a td tag open?
+               $last_tag_history = array(); # Save history of last lag activated (td, th or caption)
+               $tr_history = array(); # Is currently a tr tag open?
+               $tr_attributes = array(); # history of tr attributes
+               $has_opened_tr = array(); # Did this table open a <tr> element?
+               $indent_level = 0; # indent level of the table
 
                foreach ( $lines as $outLine ) {
                        $line = trim( $outLine );
 
-                       if( $line == '' ) { // empty line, go to next line
+                       if ( $line === '' ) { # empty line, go to next line                     
                                $out .= $outLine."\n";
                                continue;
                        }
+
                        $first_character = $line[0];
                        $matches = array();
 
                        if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
-                               // First check if we are starting a new table
+                               # First check if we are starting a new table
                                $indent_level = strlen( $matches[1] );
 
                                $attributes = $this->mStripState->unstripBoth( $matches[2] );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'table' );
+                               $attributes = Sanitizer::fixTagAttributes( $attributes , 'table' );
 
                                $outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                               array_push ( $tr_history , false );
-                               array_push ( $tr_attributes , '' );
-                               array_push ( $has_opened_tr , false );
-                       } else if ( count ( $td_history ) == 0 ) {
-                               // Don't do any of the following
+                               array_push( $td_history , false );
+                               array_push( $last_tag_history , '' );
+                               array_push( $tr_history , false );
+                               array_push( $tr_attributes , '' );
+                               array_push( $has_opened_tr , false );
+                       } elseif ( count( $td_history ) == 0 ) {
+                               # Don't do any of the following
                                $out .= $outLine."\n";
                                continue;
-                       } else if ( substr ( $line , 0 , 2 ) === '|}' ) {
-                               // We are ending a table
-                               $line = '</table>' . substr ( $line , 2 );
-                               $last_tag = array_pop ( $last_tag_history );
+                       } elseif ( substr( $line , 0 , 2 ) === '|}' ) {
+                               # We are ending a table
+                               $line = '</table>' . substr( $line , 2 );
+                               $last_tag = array_pop( $last_tag_history );
 
-                               if ( !array_pop ( $has_opened_tr ) ) {
+                               if ( !array_pop( $has_opened_tr ) ) {
                                        $line = "<tr><td></td></tr>{$line}";
                                }
 
-                               if ( array_pop ( $tr_history ) ) {
+                               if ( array_pop( $tr_history ) ) {
                                        $line = "</tr>{$line}";
                                }
 
-                               if ( array_pop ( $td_history ) ) {
+                               if ( array_pop( $td_history ) ) {
                                        $line = "</{$last_tag}>{$line}";
                                }
-                               array_pop ( $tr_attributes );
+                               array_pop( $tr_attributes );
                                $outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
-                       } else if ( substr ( $line , 0 , 2 ) === '|-' ) {
-                               // Now we have a table row
+                       } elseif ( substr( $line , 0 , 2 ) === '|-' ) {
+                               # Now we have a table row
                                $line = preg_replace( '#^\|-+#', '', $line );
 
-                               // Whats after the tag is now only attributes
+                               # Whats after the tag is now only attributes
                                $attributes = $this->mStripState->unstripBoth( $line );
-                               $attributes = Sanitizer::fixTagAttributes ( $attributes , 'tr' );
-                               array_pop ( $tr_attributes );
-                               array_push ( $tr_attributes , $attributes );
+                               $attributes = Sanitizer::fixTagAttributes( $attributes, 'tr' );
+                               array_pop( $tr_attributes );
+                               array_push( $tr_attributes, $attributes );
 
                                $line = '';
-                               $last_tag = array_pop ( $last_tag_history );
-                               array_pop ( $has_opened_tr );
-                               array_push ( $has_opened_tr , true );
+                               $last_tag = array_pop( $last_tag_history );
+                               array_pop( $has_opened_tr );
+                               array_push( $has_opened_tr , true );
 
-                               if ( array_pop ( $tr_history ) ) {
+                               if ( array_pop( $tr_history ) ) {
                                        $line = '</tr>';
                                }
 
-                               if ( array_pop ( $td_history ) ) {
+                               if ( array_pop( $td_history ) ) {
                                        $line = "</{$last_tag}>{$line}";
                                }
 
                                $outLine = $line;
-                               array_push ( $tr_history , false );
-                               array_push ( $td_history , false );
-                               array_push ( $last_tag_history , '' );
-                       }
-                       else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 )  === '|+' ) {
-                               // This might be cell elements, td, th or captions
-                               if ( substr ( $line , 0 , 2 ) === '|+' ) {
+                               array_push( $tr_history , false );
+                               array_push( $td_history , false );
+                               array_push( $last_tag_history , '' );
+                       } elseif ( $first_character === '|' || $first_character === '!' || substr( $line , 0 , 2 )  === '|+' ) {
+                               # This might be cell elements, td, th or captions
+                               if ( substr( $line , 0 , 2 ) === '|+' ) {
                                        $first_character = '+';
-                                       $line = substr ( $line , 1 );
+                                       $line = substr( $line , 1 );
                                }
 
-                               $line = substr ( $line , 1 );
+                               $line = substr( $line , 1 );
 
                                if ( $first_character === '!' ) {
-                                       $line = str_replace ( '!!' , '||' , $line );
+                                       $line = str_replace( '!!' , '||' , $line );
                                }
 
-                               // Split up multiple cells on the same line.
-                               // FIXME : This can result in improper nesting of tags processed
-                               // by earlier parser steps, but should avoid splitting up eg
-                               // attribute values containing literal "||".
+                               # Split up multiple cells on the same line.
+                               # FIXME : This can result in improper nesting of tags processed
+                               # by earlier parser steps, but should avoid splitting up eg
+                               # attribute values containing literal "||".
                                $cells = StringUtils::explodeMarkup( '||' , $line );
 
                                $outLine = '';
 
-                               // Loop through each table cell
-                               foreach ( $cells as $cell )
-                               {
+                               # Loop through each table cell
+                               foreach ( $cells as $cell ) {
                                        $previous = '';
-                                       if ( $first_character !== '+' )
-                                       {
-                                               $tr_after = array_pop ( $tr_attributes );
-                                               if ( !array_pop ( $tr_history ) ) {
+                                       if ( $first_character !== '+' ) {
+                                               $tr_after = array_pop( $tr_attributes );
+                                               if ( !array_pop( $tr_history ) ) {
                                                        $previous = "<tr{$tr_after}>\n";
                                                }
-                                               array_push ( $tr_history , true );
-                                               array_push ( $tr_attributes , '' );
-                                               array_pop ( $has_opened_tr );
-                                               array_push ( $has_opened_tr , true );
+                                               array_push( $tr_history , true );
+                                               array_push( $tr_attributes , '' );
+                                               array_pop( $has_opened_tr );
+                                               array_push( $has_opened_tr , true );
                                        }
 
-                                       $last_tag = array_pop ( $last_tag_history );
+                                       $last_tag = array_pop( $last_tag_history );
 
-                                       if ( array_pop ( $td_history ) ) {
-                                               $previous = "</{$last_tag}>{$previous}";
+                                       if ( array_pop( $td_history ) ) {
+                                               $previous = "</{$last_tag}>\n{$previous}";
                                        }
 
                                        if ( $first_character === '|' ) {
                                                $last_tag = 'td';
-                                       } else if ( $first_character === '!' ) {
+                                       } elseif ( $first_character === '!' ) {
                                                $last_tag = 'th';
-                                       } else if ( $first_character === '+' ) {
+                                       } elseif ( $first_character === '+' ) {
                                                $last_tag = 'caption';
                                        } else {
                                                $last_tag = '';
                                        }
 
-                                       array_push ( $last_tag_history , $last_tag );
+                                       array_push( $last_tag_history , $last_tag );
 
-                                       // A cell could contain both parameters and data
-                                       $cell_data = explode ( '|' , $cell , 2 );
+                                       # A cell could contain both parameters and data
+                                       $cell_data = explode( '|' , $cell , 2 );
 
-                                       // Bug 553: Note that a '|' inside an invalid link should not
-                                       // be mistaken as delimiting cell parameters
+                                       # Bug 553: Note that a '|' inside an invalid link should not
+                                       # be mistaken as delimiting cell parameters
                                        if ( strpos( $cell_data[0], '[[' ) !== false ) {
                                                $cell = "{$previous}<{$last_tag}>{$cell}";
-                                       } else if ( count ( $cell_data ) == 1 )
+                                       } elseif ( count( $cell_data ) == 1 ) {
                                                $cell = "{$previous}<{$last_tag}>{$cell_data[0]}";
-                                       else {
+                                       else {
                                                $attributes = $this->mStripState->unstripBoth( $cell_data[0] );
                                                $attributes = Sanitizer::fixTagAttributes( $attributes , $last_tag );
                                                $cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
                                        }
 
                                        $outLine .= $cell;
-                                       array_push ( $td_history , true );
+                                       array_push( $td_history , true );
                                }
                        }
                        $out .= $outLine . "\n";
                }
 
-               // Closing open td, tr && table
-               while ( count ( $td_history ) > 0 )
-               {
-                       if ( array_pop ( $td_history ) ) {
+               # Closing open td, tr && table
+               while ( count( $td_history ) > 0 ) {
+                       if ( array_pop( $td_history ) ) {
                                $out .= "</td>\n";
                        }
-                       if ( array_pop ( $tr_history ) ) {
+                       if ( array_pop( $tr_history ) ) {
                                $out .= "</tr>\n";
                        }
-                       if ( !array_pop ( $has_opened_tr ) ) {
+                       if ( !array_pop( $has_opened_tr ) ) {
                                $out .= "<tr><td></td></tr>\n" ;
                        }
 
                        $out .= "</table>\n";
                }
 
-               // Remove trailing line-ending (b/c)
+               # Remove trailing line-ending (b/c)
                if ( substr( $out, -1 ) === "\n" ) {
                        $out = substr( $out, 0, -1 );
                }
 
-               // special case: don't return empty table
-               if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
+               # special case: don't return empty table
+               if ( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
                        $out = '';
                }
 
@@ -862,44 +974,61 @@ class Parser
         *
         * @private
         */
-       function internalParse( $text ) {
-               $isMain = true;
+       function internalParse( $text, $isMain = true, $frame=false ) {
                wfProfileIn( __METHOD__ );
 
+               $origText = $text;
+
                # Hook to suspend the parser in this state
                if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
                        wfProfileOut( __METHOD__ );
                        return $text ;
                }
 
-               $text = $this->replaceVariables( $text );
+               # if $frame is provided, then use $frame for replacing any variables
+               if ( $frame ) {
+                       # use frame depth to infer how include/noinclude tags should be handled
+                       # depth=0 means this is the top-level document; otherwise it's an included document
+                       if ( !$frame->depth ) {
+                               $flag = 0;
+                       } else {
+                               $flag = Parser::PTD_FOR_INCLUSION;
+                       }
+                       $dom = $this->preprocessToDom( $text, $flag );
+                       $text = $frame->expand( $dom );
+               } else {
+                       # if $frame is not provided, then use old-style replaceVariables
+                       $text = $this->replaceVariables( $text );
+               }
+
                $text = Sanitizer::removeHTMLtags( $text, array( &$this, 'attributeStripCallback' ), false, array_keys( $this->mTransparentTagHooks ) );
                wfRunHooks( 'InternalParseBeforeLinks', array( &$this, &$text, &$this->mStripState ) );
 
-               // Tables need to come after variable replacement for things to work
-               // properly; putting them before other transformations should keep
-               // exciting things like link expansions from showing up in surprising
-               // places.
+               # Tables need to come after variable replacement for things to work
+               # properly; putting them before other transformations should keep
+               # exciting things like link expansions from showing up in surprising
+               # places.
                $text = $this->doTableStuff( $text );
 
                $text = preg_replace( '/(^|\n)-----*/', '\\1<hr />', $text );
 
                $text = $this->doDoubleUnderscore( $text );
+
                $text = $this->doHeadings( $text );
-               if( $this->mOptions->getUseDynamicDates() ) {
+               if ( $this->mOptions->getUseDynamicDates() ) {
                        $df = DateFormatter::getInstance();
                        $text = $df->reformat( $this->mOptions->getDateFormat(), $text );
                }
-               $text = $this->doAllQuotes( $text );
                $text = $this->replaceInternalLinks( $text );
+               $text = $this->doAllQuotes( $text );
                $text = $this->replaceExternalLinks( $text );
 
                # replaceInternalLinks may sometimes leave behind
                # absolute URLs, which have to be masked to hide them from replaceExternalLinks
-               $text = str_replace($this->mUniqPrefix.'NOPARSE', '', $text);
+               $text = str_replace( $this->mUniqPrefix.'NOPARSE', '', $text );
 
                $text = $this->doMagicLinks( $text );
-               $text = $this->formatHeadings( $text, $isMain );
+               $text = $this->formatHeadings( $text, $origText, $isMain );
 
                wfProfileOut( __METHOD__ );
                return $text;
@@ -908,7 +1037,7 @@ class Parser
        /**
         * Replace special strings like "ISBN xxx" and "RFC xxx" with
         * magic external links.
-        * 
+        *
         * DML
         * @private
         */
@@ -918,7 +1047,7 @@ class Parser
                $urlChar = self::EXT_LINK_URL_CLASS;
                $text = preg_replace_callback(
                        '!(?:                           # Start cases
-                               (<a.*?</a>) |               # m[1]: Skip link text 
+                               (<a[ \t\r\n>].*?</a>) |     # m[1]: Skip link text
                                (<.*?>) |                   # m[2]: Skip stuff inside HTML elements' . "
                                (\\b(?:$prots)$urlChar+) |  # m[3]: Free external links" . '
                                (?:RFC|PMID)\s+([0-9]+) |   # m[4]: RFC or PMID, capture number
@@ -947,18 +1076,20 @@ class Parser
                        if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
                                $keyword = 'RFC';
                                $urlmsg = 'rfcurl';
+                               $CssClass = 'mw-magiclink-rfc';
                                $id = $m[4];
                        } elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
                                $keyword = 'PMID';
                                $urlmsg = 'pubmedurl';
+                               $CssClass = 'mw-magiclink-pmid';
                                $id = $m[4];
                        } else {
                                throw new MWException( __METHOD__.': unrecognised match type "' .
-                                       substr($m[0], 0, 20 ) . '"' );
+                                       substr( $m[0], 0, 20 ) . '"' );
                        }
-                       $url = wfMsg( $urlmsg, $id);
-                       $sk = $this->mOptions->getSkin();
-                       $la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
+                       $url = wfMsgForContent( $urlmsg, $id);
+                       $sk = $this->mOptions->getSkin( $this->mTitle );
+                       $la = $sk->getExternalLinkAttributes( "external $CssClass" );
                        return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
                } elseif ( isset( $m[5] ) && $m[5] !== '' ) {
                        # ISBN
@@ -971,7 +1102,7 @@ class Parser
                        $titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
                        return'<a href="' .
                                $titleObj->escapeLocalUrl() .
-                               "\" class=\"internal\">ISBN $isbn</a>";
+                               "\" class=\"internal mw-magiclink-isbn\">ISBN $isbn</a>";
                } else {
                        return $m[0];
                }
@@ -986,16 +1117,16 @@ class Parser
                global $wgContLang;
                wfProfileIn( __METHOD__ );
 
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
                $trail = '';
 
                # The characters '<' and '>' (which were escaped by
                # removeHTMLtags()) should not be included in
                # URLs, per RFC 2396.
                $m2 = array();
-               if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                       $trail = substr($url, $m2[0][1]) . $trail;
-                       $url = substr($url, 0, $m2[0][1]);
+               if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
+                       $trail = substr( $url, $m2[0][1] ) . $trail;
+                       $url = substr( $url, 0, $m2[0][1] );
                }
 
                # Move trailing punctuation to $trail
@@ -1017,7 +1148,7 @@ class Parser
                $text = $this->maybeMakeExternalImage( $url );
                if ( $text === false ) {
                        # Not an image, make a link
-                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', 
+                       $text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free',
                                $this->getExternalLinkAttribs( $url ) );
                        # Register it in the output object...
                        # Replace unnecessary URL escape codes with their equivalent characters
@@ -1057,7 +1188,7 @@ class Parser
                foreach ( $lines as $line ) {
                        $outtext .= $this->doQuotes( $line ) . "\n";
                }
-               $outtext = substr($outtext, 0,-1);
+               $outtext = substr( $outtext, 0,-1 );
                wfProfileOut( __METHOD__ );
                return $outtext;
        }
@@ -1067,89 +1198,84 @@ class Parser
         */
        public function doQuotes( $text ) {
                $arr = preg_split( "/(''+)/", $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-               if ( count( $arr ) == 1 )
+               if ( count( $arr ) == 1 ) {
                        return $text;
-               else
-               {
+               } else {
                        # First, do some preliminary work. This may shift some apostrophes from
                        # being mark-up to being text. It also counts the number of occurrences
                        # of bold and italics mark-ups.
-                       $i = 0;
                        $numbold = 0;
                        $numitalics = 0;
-                       foreach ( $arr as $r )
-                       {
-                               if ( ( $i % 2 ) == 1 )
-                               {
+                       for ( $i = 0; $i < count( $arr ); $i++ ) {
+                               if ( ( $i % 2 ) == 1 ) {
                                        # If there are ever four apostrophes, assume the first is supposed to
                                        # be text, and the remaining three constitute mark-up for bold text.
-                                       if ( strlen( $arr[$i] ) == 4 )
-                                       {
+                                       if ( strlen( $arr[$i] ) == 4 ) {
                                                $arr[$i-1] .= "'";
                                                $arr[$i] = "'''";
-                                       }
-                                       # If there are more than 5 apostrophes in a row, assume they're all
-                                       # text except for the last 5.
-                                       else if ( strlen( $arr[$i] ) > 5 )
-                                       {
+                                       } elseif ( strlen( $arr[$i] ) > 5 ) {
+                                               # If there are more than 5 apostrophes in a row, assume they're all
+                                               # text except for the last 5.
                                                $arr[$i-1] .= str_repeat( "'", strlen( $arr[$i] ) - 5 );
                                                $arr[$i] = "'''''";
                                        }
                                        # Count the number of occurrences of bold and italics mark-ups.
                                        # We are not counting sequences of five apostrophes.
-                                       if ( strlen( $arr[$i] ) == 2 )      { $numitalics++;             }
-                                       else if ( strlen( $arr[$i] ) == 3 ) { $numbold++;                }
-                                       else if ( strlen( $arr[$i] ) == 5 ) { $numitalics++; $numbold++; }
+                                       if ( strlen( $arr[$i] ) == 2 ) {
+                                               $numitalics++;
+                                       } elseif ( strlen( $arr[$i] ) == 3 ) {
+                                               $numbold++;
+                                       } elseif ( strlen( $arr[$i] ) == 5 ) {
+                                               $numitalics++;
+                                               $numbold++;
+                                       }
                                }
-                               $i++;
                        }
 
                        # If there is an odd number of both bold and italics, it is likely
                        # that one of the bold ones was meant to be an apostrophe followed
                        # by italics. Which one we cannot know for certain, but it is more
                        # likely to be one that has a single-letter word before it.
-                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) )
-                       {
+                       if ( ( $numbold % 2 == 1 ) && ( $numitalics % 2 == 1 ) ) {
                                $i = 0;
                                $firstsingleletterword = -1;
                                $firstmultiletterword = -1;
                                $firstspace = -1;
-                               foreach ( $arr as $r )
-                               {
-                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) )
-                                       {
-                                               $x1 = substr ($arr[$i-1], -1);
-                                               $x2 = substr ($arr[$i-1], -2, 1);
-                                               if ($x1 === ' ') {
-                                                       if ($firstspace == -1) $firstspace = $i;
-                                               } else if ($x2 === ' ') {
-                                                       if ($firstsingleletterword == -1) $firstsingleletterword = $i;
+                               foreach ( $arr as $r ) {
+                                       if ( ( $i % 2 == 1 ) and ( strlen( $r ) == 3 ) ) {
+                                               $x1 = substr( $arr[$i-1], -1 );
+                                               $x2 = substr( $arr[$i-1], -2, 1 );
+                                               if ( $x1 === ' ' ) {
+                                                       if ( $firstspace == -1 ) {
+                                                               $firstspace = $i;
+                                                       }
+                                               } elseif ( $x2 === ' ') {
+                                                       if ( $firstsingleletterword == -1 ) {
+                                                               $firstsingleletterword = $i;
+                                                       }
                                                } else {
-                                                       if ($firstmultiletterword == -1) $firstmultiletterword = $i;
+                                                       if ( $firstmultiletterword == -1 ) {
+                                                               $firstmultiletterword = $i;
+                                                       }
                                                }
                                        }
                                        $i++;
                                }
 
                                # If there is a single-letter word, use it!
-                               if ($firstsingleletterword > -1)
-                               {
-                                       $arr [ $firstsingleletterword ] = "''";
-                                       $arr [ $firstsingleletterword-1 ] .= "'";
-                               }
-                               # If not, but there's a multi-letter word, use that one.
-                               else if ($firstmultiletterword > -1)
-                               {
-                                       $arr [ $firstmultiletterword ] = "''";
-                                       $arr [ $firstmultiletterword-1 ] .= "'";
-                               }
-                               # ... otherwise use the first one that has neither.
-                               # (notice that it is possible for all three to be -1 if, for example,
-                               # there is only one pentuple-apostrophe in the line)
-                               else if ($firstspace > -1)
-                               {
-                                       $arr [ $firstspace ] = "''";
-                                       $arr [ $firstspace-1 ] .= "'";
+                               if ( $firstsingleletterword > -1 ) {
+                                       $arr[$firstsingleletterword] = "''";
+                                       $arr[$firstsingleletterword-1] .= "'";
+                               } elseif ( $firstmultiletterword > -1 ) {
+                                       # If not, but there's a multi-letter word, use that one.
+                                       $arr[$firstmultiletterword] = "''";
+                                       $arr[$firstmultiletterword-1] .= "'";
+                               } elseif ( $firstspace > -1 ) {
+                                       # ... otherwise use the first one that has neither.
+                                       # (notice that it is possible for all three to be -1 if, for example,
+                                       # there is only one pentuple-apostrophe in the line)
+                                       $arr[$firstspace] = "''";
+                                       $arr[$firstspace-1] .= "'";
                                }
                        }
 
@@ -1158,71 +1284,70 @@ class Parser
                        $buffer = '';
                        $state = '';
                        $i = 0;
-                       foreach ($arr as $r)
-                       {
-                               if (($i % 2) == 0)
-                               {
-                                       if ($state === 'both')
+                       foreach ( $arr as $r ) {
+                               if ( ( $i % 2 ) == 0 ) {
+                                       if ( $state === 'both' ) {
                                                $buffer .= $r;
-                                       else
+                                       } else {
                                                $output .= $r;
-                               }
-                               else
-                               {
-                                       if (strlen ($r) == 2)
-                                       {
-                                               if ($state === 'i')
-                                               { $output .= '</i>'; $state = ''; }
-                                               else if ($state === 'bi')
-                                               { $output .= '</i>'; $state = 'b'; }
-                                               else if ($state === 'ib')
-                                               { $output .= '</b></i><b>'; $state = 'b'; }
-                                               else if ($state === 'both')
-                                               { $output .= '<b><i>'.$buffer.'</i>'; $state = 'b'; }
-                                               else # $state can be 'b' or ''
-                                               { $output .= '<i>'; $state .= 'i'; }
                                        }
-                                       else if (strlen ($r) == 3)
-                                       {
-                                               if ($state === 'b')
-                                               { $output .= '</b>'; $state = ''; }
-                                               else if ($state === 'bi')
-                                               { $output .= '</i></b><i>'; $state = 'i'; }
-                                               else if ($state === 'ib')
-                                               { $output .= '</b>'; $state = 'i'; }
-                                               else if ($state === 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b>'; $state = 'i'; }
-                                               else # $state can be 'i' or ''
-                                               { $output .= '<b>'; $state .= 'b'; }
-                                       }
-                                       else if (strlen ($r) == 5)
-                                       {
-                                               if ($state === 'b')
-                                               { $output .= '</b><i>'; $state = 'i'; }
-                                               else if ($state === 'i')
-                                               { $output .= '</i><b>'; $state = 'b'; }
-                                               else if ($state === 'bi')
-                                               { $output .= '</i></b>'; $state = ''; }
-                                               else if ($state === 'ib')
-                                               { $output .= '</b></i>'; $state = ''; }
-                                               else if ($state === 'both')
-                                               { $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
-                                               else # ($state == '')
-                                               { $buffer = ''; $state = 'both'; }
+                               } else {
+                                       if ( strlen( $r ) == 2 ) {
+                                               if ( $state === 'i' ) {
+                                                       $output .= '</i>'; $state = '';
+                                               } elseif ( $state === 'bi' ) {
+                                                       $output .= '</i>'; $state = 'b';
+                                               } elseif ( $state === 'ib' ) {
+                                                       $output .= '</b></i><b>'; $state = 'b';
+                                               } elseif ( $state === 'both' ) {
+                                                       $output .= '<b><i>'.$buffer.'</i>'; $state = 'b';
+                                               } else { # $state can be 'b' or ''
+                                                       $output .= '<i>'; $state .= 'i';
+                                               }
+                                       } elseif ( strlen( $r ) == 3 ) {
+                                               if ( $state === 'b' ) {
+                                                       $output .= '</b>'; $state = '';
+                                               } elseif ( $state === 'bi' ) {
+                                                       $output .= '</i></b><i>'; $state = 'i';
+                                               } elseif ( $state === 'ib' ) {
+                                                       $output .= '</b>'; $state = 'i';
+                                               } elseif ( $state === 'both' ) {
+                                                       $output .= '<i><b>'.$buffer.'</b>'; $state = 'i';
+                                               } else { # $state can be 'i' or ''
+                                                       $output .= '<b>'; $state .= 'b';
+                                               }
+                                       } elseif ( strlen( $r ) == 5 ) {
+                                               if ( $state === 'b' ) {
+                                                       $output .= '</b><i>'; $state = 'i';
+                                               } elseif ( $state === 'i' ) {
+                                                       $output .= '</i><b>'; $state = 'b';
+                                               } elseif ( $state === 'bi' ) {
+                                                       $output .= '</i></b>'; $state = '';
+                                               } elseif ( $state === 'ib' ) {
+                                                       $output .= '</b></i>'; $state = '';
+                                               } elseif ( $state === 'both' ) {
+                                                       $output .= '<i><b>'.$buffer.'</b></i>'; $state = '';
+                                               } else { # ($state == '')
+                                                       $buffer = ''; $state = 'both';
+                                               }
                                        }
                                }
                                $i++;
                        }
                        # Now close all remaining tags.  Notice that the order is important.
-                       if ($state === 'b' || $state === 'ib')
+                       if ( $state === 'b' || $state === 'ib' ) {
                                $output .= '</b>';
-                       if ($state === 'i' || $state === 'bi' || $state === 'ib')
+                       }
+                       if ( $state === 'i' || $state === 'bi' || $state === 'ib' ) {
                                $output .= '</i>';
-                       if ($state === 'bi')
+                       }
+                       if ( $state === 'bi' ) {
                                $output .= '</b>';
+                       }
                        # There might be lonely ''''', so make sure we have a buffer
-                       if ($state === 'both' && $buffer)
+                       if ( $state === 'both' && $buffer ) {
                                $output .= '<b><i>'.$buffer.'</i></b>';
+                       }
                        return $output;
                }
        }
@@ -1239,7 +1364,7 @@ class Parser
                global $wgContLang;
                wfProfileIn( __METHOD__ );
 
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
 
                $bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
                $s = array_shift( $bits );
@@ -1255,9 +1380,9 @@ class Parser
                        # removeHTMLtags()) should not be included in
                        # URLs, per RFC 2396.
                        $m2 = array();
-                       if (preg_match('/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE)) {
-                               $text = substr($url, $m2[0][1]) . ' ' . $text;
-                               $url = substr($url, 0, $m2[0][1]);
+                       if ( preg_match( '/&(lt|gt);/', $url, $m2, PREG_OFFSET_CAPTURE ) ) {
+                               $text = substr( $url, $m2[0][1] ) . ' ' . $text;
+                               $url = substr( $url, 0, $m2[0][1] );
                        }
 
                        # If the link text is an image URL, replace it with an <img> tag
@@ -1270,12 +1395,12 @@ class Parser
                        $dtrail = '';
 
                        # Set linktype for CSS - if URL==text, link is essentially free
-                       $linktype = ($text === $url) ? 'free' : 'text';
+                       $linktype = ( $text === $url ) ? 'free' : 'text';
 
                        # No link text, e.g. [http://domain.tld/some.link]
                        if ( $text == '' ) {
                                # Autonumber if allowed. See bug #5918
-                               if ( strpos( wfUrlProtocols(), substr($protocol, 0, strpos($protocol, ':')) ) !== false ) {
+                               if ( strpos( wfUrlProtocols(), substr( $protocol, 0, strpos( $protocol, ':' ) ) ) !== false ) {
                                        $langObj = $this->getFunctionLang();
                                        $text = '[' . $langObj->formatNum( ++$this->mAutonumber ) . ']';
                                        $linktype = 'autonumber';
@@ -1290,13 +1415,13 @@ class Parser
                                list( $dtrail, $trail ) = Linker::splitTrail( $trail );
                        }
 
-                       $text = $wgContLang->markNoConversion($text);
+                       $text = $wgContLang->markNoConversion( $text );
 
                        $url = Sanitizer::cleanUrl( $url );
 
                        # Use the encoded URL
                        # This means that users can paste URLs directly into the text
-                       # Funny characters like &ouml; aren't valid in URLs anyway
+                       # Funny characters like Ã¶ aren't valid in URLs anyway
                        # This was changed in August 2004
                        $s .= $sk->makeExternalLink( $url, $text, false, $linktype,
                                $this->getExternalLinkAttribs( $url ) ) . $dtrail . $trail;
@@ -1318,15 +1443,15 @@ class Parser
         * (depending on configuration, namespace, and the URL's domain) and/or a
         * target attribute (depending on configuration).
         *
-        * @param string $url Optional URL, to extract the domain from for rel =>
+        * @param $url String: optional URL, to extract the domain from for rel =>
         *   nofollow if appropriate
-        * @return array Associative array of HTML attributes
+        * @return Array: associative array of HTML attributes
         */
        function getExternalLinkAttribs( $url = false ) {
                $attribs = array();
                global $wgNoFollowLinks, $wgNoFollowNsExceptions;
                $ns = $this->mTitle->getNamespace();
-               if( $wgNoFollowLinks && !in_array($ns, $wgNoFollowNsExceptions) ) {
+               if ( $wgNoFollowLinks && !in_array( $ns, $wgNoFollowNsExceptions ) ) {
                        $attribs['rel'] = 'nofollow';
 
                        global $wgNoFollowDomainExceptions;
@@ -1334,8 +1459,7 @@ class Parser
                                $bits = wfParseUrl( $url );
                                if ( is_array( $bits ) && isset( $bits['host'] ) ) {
                                        foreach ( $wgNoFollowDomainExceptions as $domain ) {
-                                               if( substr( $bits['host'], -strlen( $domain ) )
-                                               == $domain ) {
+                                               if ( substr( $bits['host'], -strlen( $domain ) ) == $domain ) {
                                                        unset( $attribs['rel'] );
                                                        break;
                                                }
@@ -1352,9 +1476,10 @@ class Parser
 
        /**
         * Replace unusual URL escape codes with their equivalent characters
-        * @param string
-        * @return string
-        * @static
+        *
+        * @param $url String
+        * @return String
+        *
         * @todo  This can merge genuinely required bits in the path or query string,
         *        breaking legit URLs. A proper fix would treat the various parts of
         *        the URL differently; as a workaround, just use the output for
@@ -1368,18 +1493,16 @@ class Parser
        /**
         * Callback function used in replaceUnusualEscapes().
         * Replaces unusual URL escape codes with their equivalent character
-        * @static
-        * @private
         */
        private static function replaceUnusualEscapesCallback( $matches ) {
                $char = urldecode( $matches[0] );
                $ord = ord( $char );
-               // Is it an unsafe or HTTP reserved character according to RFC 1738?
+               # Is it an unsafe or HTTP reserved character according to RFC 1738?
                if ( $ord > 32 && $ord < 127 && strpos( '<>"#{}|\^~[]`;/?', $char ) === false ) {
-                       // No, shouldn't be escaped
+                       # No, shouldn't be escaped
                        return $char;
                } else {
-                       // Yes, leave it escaped
+                       # Yes, leave it escaped
                        return $matches[0];
                }
        }
@@ -1390,21 +1513,21 @@ class Parser
         * @private
         */
        function maybeMakeExternalImage( $url ) {
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
                $imagesfrom = $this->mOptions->getAllowExternalImagesFrom();
-               $imagesexception = !empty($imagesfrom);
+               $imagesexception = !empty( $imagesfrom );
                $text = false;
                # $imagesfrom could be either a single string or an array of strings, parse out the latter
-               if( $imagesexception && is_array( $imagesfrom ) ) {
+               if ( $imagesexception && is_array( $imagesfrom ) ) {
                        $imagematch = false;
-                       foreach( $imagesfrom as $match ) {
-                               if( strpos( $url, $match ) === 0 ) {
+                       foreach ( $imagesfrom as $match ) {
+                               if ( strpos( $url, $match ) === 0 ) {
                                        $imagematch = true;
                                        break;
                                }
                        }
-               } elseif( $imagesexception ) {
-                       $imagematch = (strpos( $url, $imagesfrom ) === 0);
+               } elseif ( $imagesexception ) {
+                       $imagematch = ( strpos( $url, $imagesfrom ) === 0 );
                } else {
                        $imagematch = false;
                }
@@ -1415,14 +1538,15 @@ class Parser
                                $text = $sk->makeExternalImage( $url );
                        }
                }
-               if( !$text && $this->mOptions->getEnableImageWhitelist()
+               if ( !$text && $this->mOptions->getEnableImageWhitelist()
                         && preg_match( self::EXT_IMAGE_REGEX, $url ) ) {
                        $whitelist = explode( "\n", wfMsgForContent( 'external_image_whitelist' ) );
-                       foreach( $whitelist as $entry ) {
+                       foreach ( $whitelist as $entry ) {
                                # Sanitize the regex fragment, make it case-insensitive, ignore blank entries/comments
-                               if( strpos( $entry, '#' ) === 0 || $entry === '' )
+                               if ( strpos( $entry, '#' ) === 0 || $entry === '' ) {
                                        continue;
-                               if( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
+                               }
+                               if ( preg_match( '/' . str_replace( '/', '\\/', $entry ) . '/i', $url ) ) {
                                        # Image matches a whitelist entry
                                        $text = $sk->makeExternalImage( $url );
                                        break;
@@ -1434,7 +1558,7 @@ class Parser
 
        /**
         * Process [[ ]] wikilinks
-        * @return processed text
+        * @return String: processed text
         *
         * @private
         */
@@ -1457,7 +1581,7 @@ class Parser
                wfProfileIn( __METHOD__.'-setup' );
                static $tc = FALSE, $e1, $e1_img;
                # the % is needed to support urlencoded titles as well
-               if ( !$tc ) { 
+               if ( !$tc ) {
                        $tc = Title::legalChars() . '#%';
                        # Match a link having the form [[namespace:link|alternate]]trail
                        $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
@@ -1465,12 +1589,12 @@ class Parser
                        $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
                }
 
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
                $holders = new LinkHolderArray( $this );
 
-               #split the entire text string on occurences of [[
+               # split the entire text string on occurences of [[
                $a = StringUtils::explode( '[[', ' ' . $s );
-               #get the first element (all text up to first [[), and remove the space we added
+               # get the first element (all text up to first [[), and remove the space we added
                $s = $a->current();
                $a->next();
                $line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
@@ -1484,7 +1608,7 @@ class Parser
                        $e2 = wfMsgForContent( 'linkprefix' );
                }
 
-               if( is_null( $this->mTitle ) ) {
+               if ( is_null( $this->mTitle ) ) {
                        wfProfileOut( __METHOD__.'-setup' );
                        wfProfileOut( __METHOD__ );
                        throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
@@ -1502,10 +1626,10 @@ class Parser
                        $prefix = '';
                }
 
-               if($wgContLang->hasVariants()) {
-                       $selflink = $wgContLang->convertLinkToAllVariants($this->mTitle->getPrefixedText());
+               if ( $wgContLang->hasVariants() ) {
+                       $selflink = $wgContLang->autoConvertToAllVariants( $this->mTitle->getPrefixedText() );
                } else {
-                       $selflink = array($this->mTitle->getPrefixedText());
+                       $selflink = array( $this->mTitle->getPrefixedText() );
                }
                $useSubpages = $this->areSubpagesAllowed();
                wfProfileOut( __METHOD__.'-setup' );
@@ -1529,7 +1653,7 @@ class Parser
                                        $prefix='';
                                }
                                # first link
-                               if($first_prefix) {
+                               if ( $first_prefix ) {
                                        $prefix = $first_prefix;
                                        $first_prefix = false;
                                }
@@ -1549,25 +1673,25 @@ class Parser
                                # Still some problems for cases where the ] is meant to be outside punctuation,
                                # and no image is in sight. See bug 2095.
                                #
-                               if( $text !== '' &&
+                               if ( $text !== '' &&
                                        substr( $m[3], 0, 1 ) === ']' &&
-                                       strpos($text, '[') !== false
+                                       strpos( $text, '[' ) !== false
                                )
                                {
                                        $text .= ']'; # so that replaceExternalLinks($text) works later
                                        $m[3] = substr( $m[3], 1 );
                                }
                                # fix up urlencoded title texts
-                               if( strpos( $m[1], '%' ) !== false ) {
+                               if ( strpos( $m[1], '%' ) !== false ) {
                                        # Should anchors '#' also be rejected?
-                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode($m[1]) );
+                                       $m[1] = str_replace( array('<', '>'), array('&lt;', '&gt;'), urldecode( $m[1] ) );
                                }
                                $trail = $m[3];
-                       } elseif( preg_match($e1_img, $line, $m) ) { # Invalid, but might be an image with a link in its caption
+                       } elseif ( preg_match( $e1_img, $line, $m ) ) { # Invalid, but might be an image with a link in its caption
                                $might_be_img = true;
                                $text = $m[2];
                                if ( strpos( $m[1], '%' ) !== false ) {
-                                       $m[1] = urldecode($m[1]);
+                                       $m[1] = urldecode( $m[1] );
                                }
                                $trail = "";
                        } else { # Invalid form; output directly
@@ -1581,29 +1705,29 @@ class Parser
                        # Don't allow internal links to pages containing
                        # PROTO: where PROTO is a valid URL protocol; these
                        # should be external links.
-                       if (preg_match('/^\b(?:' . wfUrlProtocols() . ')/', $m[1])) {
+                       if ( preg_match( '/^\b(?:' . wfUrlProtocols() . ')/', $m[1] ) ) {
                                $s .= $prefix . '[[' . $line ;
                                wfProfileOut( __METHOD__."-misc" );
                                continue;
                        }
 
                        # Make subpage if necessary
-                       if( $useSubpages ) {
+                       if ( $useSubpages ) {
                                $link = $this->maybeDoSubpageLink( $m[1], $text );
                        } else {
                                $link = $m[1];
                        }
 
-                       $noforce = (substr($m[1], 0, 1) !== ':');
-                       if (!$noforce) {
+                       $noforce = ( substr( $m[1], 0, 1 ) !== ':' );
+                       if ( !$noforce ) {
                                # Strip off leading ':'
-                               $link = substr($link, 1);
+                               $link = substr( $link, 1 );
                        }
 
                        wfProfileOut( __METHOD__."-misc" );
                        wfProfileIn( __METHOD__."-title" );
-                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
-                       if( $nt === NULL ) {
+                       $nt = Title::newFromText( $this->mStripState->unstripNoWiki( $link ) );
+                       if ( $nt === null ) {
                                $s .= $prefix . '[[' . $line;
                                wfProfileOut( __METHOD__."-title" );
                                continue;
@@ -1613,12 +1737,12 @@ class Parser
                        $iw = $nt->getInterWiki();
                        wfProfileOut( __METHOD__."-title" );
 
-                       if ($might_be_img) { # if this is actually an invalid link
+                       if ( $might_be_img ) { # if this is actually an invalid link
                                wfProfileIn( __METHOD__."-might_be_img" );
-                               if ($ns == NS_FILE && $noforce) { #but might be an image
+                               if ( $ns == NS_FILE && $noforce ) { # but might be an image
                                        $found = false;
                                        while ( true ) {
-                                               #look at the next 'line' to see if we can close it there
+                                               # look at the next 'line' to see if we can close it there
                                                $a->next();
                                                $next_line = $a->current();
                                                if ( $next_line === false || $next_line === null ) {
@@ -1632,24 +1756,24 @@ class Parser
                                                        $trail = $m[2];
                                                        break;
                                                } elseif ( count( $m ) == 2 ) {
-                                                       #if there's exactly one ]] that's fine, we'll keep looking
+                                                       # if there's exactly one ]] that's fine, we'll keep looking
                                                        $text .= "[[{$m[0]}]]{$m[1]}";
                                                } else {
-                                                       #if $next_line is invalid too, we need look no further
+                                                       # if $next_line is invalid too, we need look no further
                                                        $text .= '[[' . $next_line;
                                                        break;
                                                }
                                        }
                                        if ( !$found ) {
                                                # we couldn't find the end of this imageLink, so output it raw
-                                               #but don't ignore what might be perfectly normal links in the text we've examined
+                                               # but don't ignore what might be perfectly normal links in the text we've examined
                                                $holders->merge( $this->replaceInternalLinks2( $text ) );
                                                $s .= "{$prefix}[[$link|$text";
                                                # note: no $trail, because without an end, there *is* no trail
                                                wfProfileOut( __METHOD__."-might_be_img" );
                                                continue;
                                        }
-                               } else { #it's not an image, so output it raw
+                               } else { # it's not an image, so output it raw
                                        $s .= "{$prefix}[[$link|$text";
                                        # note: no $trail, because without an end, there *is* no trail
                                        wfProfileOut( __METHOD__."-might_be_img" );
@@ -1658,18 +1782,25 @@ class Parser
                                wfProfileOut( __METHOD__."-might_be_img" );
                        }
 
-                       $wasblank = ( '' == $text );
-                       if( $wasblank ) $text = $link;
+                       $wasblank = ( $text  == '' );
+                       if ( $wasblank ) {
+                               $text = $link;
+                       } else {
+                               # Bug 4598 madness. Handle the quotes only if they come from the alternate part
+                               # [[Lista d''e paise d''o munno]] -> <a href="">Lista d''e paise d''o munno</a>
+                               # [[Criticism of Harry Potter|Criticism of ''Harry Potter'']] -> <a href="Criticism of Harry Potter">Criticism of <i>Harry Potter</i></a>
+                               $text = $this->doQuotes($text);
+                       }
 
                        # Link not escaped by : , create the various objects
-                       if( $noforce ) {
+                       if ( $noforce ) {
 
                                # Interwikis
                                wfProfileIn( __METHOD__."-interwiki" );
-                               if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
+                               if ( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
                                        $this->mOutput->addLanguageLink( $nt->getFullText() );
-                                       $s = rtrim($s . $prefix);
-                                       $s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
+                                       $s = rtrim( $s . $prefix );
+                                       $s .= trim( $trail, "\n" ) == '' ? '': $prefix . $trail;
                                        wfProfileOut( __METHOD__."-interwiki" );
                                        continue;
                                }
@@ -1678,14 +1809,23 @@ class Parser
                                if ( $ns == NS_FILE ) {
                                        wfProfileIn( __METHOD__."-image" );
                                        if ( !wfIsBadImage( $nt->getDBkey(), $this->mTitle ) ) {
-                                               # recursively parse links inside the image caption
-                                               # actually, this will parse them in any other parameters, too,
-                                               # but it might be hard to fix that, and it doesn't matter ATM
-                                               $text = $this->replaceExternalLinks($text);
-                                               $holders->merge( $this->replaceInternalLinks2( $text ) );
-
+                                               if ( $wasblank ) {
+                                                       # if no parameters were passed, $text
+                                                       # becomes something like "File:Foo.png",
+                                                       # which we don't want to pass on to the
+                                                       # image generator
+                                                       $text = '';
+                                               } else {
+                                                       # recursively parse links inside the image caption
+                                                       # actually, this will parse them in any other parameters, too,
+                                                       # but it might be hard to fix that, and it doesn't matter ATM
+                                                       $text = $this->replaceExternalLinks( $text );
+                                                       $holders->merge( $this->replaceInternalLinks2( $text ) );
+                                               }
                                                # cloak any absolute URLs inside the image markup, so replaceExternalLinks() won't touch them
                                                $s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
+                                       } else {
+                                               $s .= $prefix . $trail;
                                        }
                                        $this->mOutput->addImage( $nt->getDBkey() );
                                        wfProfileOut( __METHOD__."-image" );
@@ -1695,7 +1835,7 @@ class Parser
 
                                if ( $ns == NS_CATEGORY ) {
                                        wfProfileIn( __METHOD__."-category" );
-                                       $s = rtrim($s . "\n"); # bug 87
+                                       $s = rtrim( $s . "\n" ); # bug 87
 
                                        if ( $wasblank ) {
                                                $sortkey = $this->getDefaultSort();
@@ -1711,7 +1851,7 @@ class Parser
                                         * Strip the whitespace Category links produce, see bug 87
                                         * @todo We might want to use trim($tmp, "\n") here.
                                         */
-                                       $s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
+                                       $s .= trim( $prefix . $trail, "\n" ) == '' ? '': $prefix . $trail;
 
                                        wfProfileOut( __METHOD__."-category" );
                                        continue;
@@ -1719,8 +1859,8 @@ class Parser
                        }
 
                        # Self-link checking
-                       if( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
-                               if( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
+                       if ( $nt->getFragment() === '' && $ns != NS_SPECIAL ) {
+                               if ( in_array( $nt->getPrefixedText(), $selflink, true ) ) {
                                        $s .= $prefix . $sk->makeSelfLinkObj( $nt, $text, '', $trail );
                                        continue;
                                }
@@ -1728,7 +1868,7 @@ class Parser
 
                        # NS_MEDIA is a pseudo-namespace for linking directly to a file
                        # FIXME: Should do batch file existence checks, see comment below
-                       if( $ns == NS_MEDIA ) {
+                       if ( $ns == NS_MEDIA ) {
                                wfProfileIn( __METHOD__."-media" );
                                # Give extensions a chance to select the file revision for us
                                $skip = $time = false;
@@ -1751,7 +1891,7 @@ class Parser
                        #
                        # FIXME: isAlwaysKnown() can be expensive for file links; we should really do
                        # batch file existence checks for NS_FILE and NS_MEDIA
-                       if( $iw == '' && $nt->isAlwaysKnown() ) {
+                       if ( $iw == '' && $nt->isAlwaysKnown() ) {
                                $this->mOutput->addLink( $nt );
                                $s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
                        } else {
@@ -1783,16 +1923,17 @@ class Parser
         * breaking URLs in the following text without breaking trails on the
         * wiki links, it's been made into a horrible function.
         *
-        * @param Title $nt
-        * @param string $text
-        * @param string $query
-        * @param string $trail
-        * @param string $prefix
-        * @return string HTML-wikitext mix oh yuck
+        * @param $nt Title
+        * @param $text String
+        * @param $query String
+        * @param $trail String
+        * @param $prefix String
+        * @return String: HTML-wikitext mix oh yuck
         */
        function makeKnownLinkHolder( $nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
                list( $inside, $trail ) = Linker::splitTrail( $trail );
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
+               # FIXME: use link() instead of deprecated makeKnownLinkObj()
                $link = $sk->makeKnownLinkObj( $nt, $text, $query, $inside, $prefix );
                return $this->armorLinks( $link ) . $trail;
        }
@@ -1804,8 +1945,8 @@ class Parser
         * Not needed quite as much as it used to be since free links are a bit
         * more sensible these days. But bracketed links are still an issue.
         *
-        * @param string more-or-less HTML
-        * @return string less-or-more HTML with NOPARSE bits
+        * @param $text String: more-or-less HTML
+        * @return String: less-or-more HTML with NOPARSE bits
         */
        function armorLinks( $text ) {
                return preg_replace( '/\b(' . wfUrlProtocols() . ')/',
@@ -1814,7 +1955,7 @@ class Parser
 
        /**
         * Return true if subpage links should be expanded on this page.
-        * @return bool
+        * @return Boolean
         */
        function areSubpagesAllowed() {
                # Some namespaces don't allow subpages
@@ -1823,132 +1964,87 @@ class Parser
 
        /**
         * Handle link to subpage if necessary
-        * @param string $target the source of the link
-        * @param string &$text the link text, modified as necessary
+        *
+        * @param $target String: the source of the link
+        * @param &$text String: the link text, modified as necessary
         * @return string the full name of the link
         * @private
         */
-       function maybeDoSubpageLink($target, &$text) {
-               # 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
-               # ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
-               # ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
-
-               wfProfileIn( __METHOD__ );
-               $ret = $target; # default return value is no change
-
-               # Some namespaces don't allow subpages,
-               # so only perform processing if subpages are allowed
-               if( $this->areSubpagesAllowed() ) {
-                       $hash = strpos( $target, '#' );
-                       if( $hash !== false ) {
-                               $suffix = substr( $target, $hash );
-                               $target = substr( $target, 0, $hash );
-                       } else {
-                               $suffix = '';
-                       }
-                       # bug 7425
-                       $target = trim( $target );
-                       # Look at the first character
-                       if( $target != '' && $target{0} === '/' ) {
-                               # / at end means we don't want the slash to be shown
-                               $m = array();
-                               $trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
-                               if( $trailingSlashes ) {
-                                       $noslash = $target = substr( $target, 1, -strlen($m[0][0]) );
-                               } else {
-                                       $noslash = substr( $target, 1 );
-                               }
-
-                               $ret = $this->mTitle->getPrefixedText(). '/' . trim($noslash) . $suffix;
-                               if( '' === $text ) {
-                                       $text = $target . $suffix;
-                               } # this might be changed for ugliness reasons
-                       } else {
-                               # check for .. subpage backlinks
-                               $dotdotcount = 0;
-                               $nodotdot = $target;
-                               while( strncmp( $nodotdot, "../", 3 ) == 0 ) {
-                                       ++$dotdotcount;
-                                       $nodotdot = substr( $nodotdot, 3 );
-                               }
-                               if($dotdotcount > 0) {
-                                       $exploded = explode( '/', $this->mTitle->GetPrefixedText() );
-                                       if( count( $exploded ) > $dotdotcount ) { # not allowed to go below top level page
-                                               $ret = implode( '/', array_slice( $exploded, 0, -$dotdotcount ) );
-                                               # / at the end means don't show full path
-                                               if( substr( $nodotdot, -1, 1 ) === '/' ) {
-                                                       $nodotdot = substr( $nodotdot, 0, -1 );
-                                                       if( '' === $text ) {
-                                                               $text = $nodotdot . $suffix;
-                                                       }
-                                               }
-                                               $nodotdot = trim( $nodotdot );
-                                               if( $nodotdot != '' ) {
-                                                       $ret .= '/' . $nodotdot;
-                                               }
-                                               $ret .= $suffix;
-                                       }
-                               }
-                       }
-               }
-
-               wfProfileOut( __METHOD__ );
-               return $ret;
+       function maybeDoSubpageLink( $target, &$text ) {
+               return Linker::normalizeSubpageLink( $this->mTitle, $target, $text );
        }
 
        /**#@+
         * Used by doBlockLevels()
         * @private
         */
-       /* private */ function closeParagraph() {
+       function closeParagraph() {
                $result = '';
-               if ( '' != $this->mLastSection ) {
+               if ( $this->mLastSection != '' ) {
                        $result = '</' . $this->mLastSection  . ">\n";
                }
                $this->mInPre = false;
                $this->mLastSection = '';
                return $result;
        }
-       # getCommon() returns the length of the longest common substring
-       # of both arguments, starting at the beginning of both.
-       #
-       /* private */ function getCommon( $st1, $st2 ) {
+
+       /**
+        * getCommon() returns the length of the longest common substring
+        * of both arguments, starting at the beginning of both.
+        * @private
+        */
+       function getCommon( $st1, $st2 ) {
                $fl = strlen( $st1 );
                $shorter = strlen( $st2 );
-               if ( $fl < $shorter ) { $shorter = $fl; }
+               if ( $fl < $shorter ) {
+                       $shorter = $fl;
+               }
 
                for ( $i = 0; $i < $shorter; ++$i ) {
-                       if ( $st1{$i} != $st2{$i} ) { break; }
+                       if ( $st1{$i} != $st2{$i} ) {
+                               break;
+                       }
                }
                return $i;
        }
-       # These next three functions open, continue, and close the list
-       # element appropriate to the prefix character passed into them.
-       #
-       /* private */ function openList( $char ) {
+
+       /**
+        * These next three functions open, continue, and close the list
+        * element appropriate to the prefix character passed into them.
+        * @private
+        */
+       function openList( $char ) {
                $result = $this->closeParagraph();
 
-               if ( '*' === $char ) { $result .= '<ul><li>'; }
-               else if ( '#' === $char ) { $result .= '<ol><li>'; }
-               else if ( ':' === $char ) { $result .= '<dl><dd>'; }
-               else if ( ';' === $char ) {
+               if ( '*' === $char ) {
+                       $result .= '<ul><li>';
+               } elseif ( '#' === $char ) {
+                       $result .= '<ol><li>';
+               } elseif ( ':' === $char ) {
+                       $result .= '<dl><dd>';
+               } elseif ( ';' === $char ) {
                        $result .= '<dl><dt>';
                        $this->mDTopen = true;
+               } else {
+                       $result = '<!-- ERR 1 -->';
                }
-               else { $result = '<!-- ERR 1 -->'; }
 
                return $result;
        }
 
-       /* private */ function nextItem( $char ) {
-               if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
-               else if ( ':' === $char || ';' === $char ) {
+       /**
+        * TODO: document
+        * @param $char String
+        * @private
+        */
+       function nextItem( $char ) {
+               if ( '*' === $char || '#' === $char ) {
+                       return '</li><li>';
+               } elseif ( ':' === $char || ';' === $char ) {
                        $close = '</dd>';
-                       if ( $this->mDTopen ) { $close = '</dt>'; }
+                       if ( $this->mDTopen ) {
+                               $close = '</dt>';
+                       }
                        if ( ';' === $char ) {
                                $this->mDTopen = true;
                                return $close . '<dt>';
@@ -1960,18 +2056,26 @@ class Parser
                return '<!-- ERR 2 -->';
        }
 
-       /* private */ function closeList( $char ) {
-               if ( '*' === $char ) { $text = '</li></ul>'; }
-               else if ( '#' === $char ) { $text = '</li></ol>'; }
-               else if ( ':' === $char ) {
+       /**
+        * TODO: document
+        * @param $char String
+        * @private
+        */
+       function closeList( $char ) {
+               if ( '*' === $char ) {
+                       $text = '</li></ul>';
+               } elseif ( '#' === $char ) {
+                       $text = '</li></ol>';
+               } elseif ( ':' === $char ) {
                        if ( $this->mDTopen ) {
                                $this->mDTopen = false;
                                $text = '</dt></dl>';
                        } else {
                                $text = '</dd></dl>';
                        }
+               } else {
+                       return '<!-- ERR 3 -->';
                }
-               else {  return '<!-- ERR 3 -->'; }
                return $text."\n";
        }
        /**#@-*/
@@ -1979,6 +2083,8 @@ class Parser
        /**
         * Make lists from lines starting with ':', '*', '#', etc. (DBL)
         *
+        * @param $text String
+        * @param $linestart Boolean: whether or not this is at the start of a line.
         * @private
         * @return string the lists rendered as HTML
         */
@@ -2003,16 +2109,24 @@ class Parser
                                $linestart = true;
                                continue;
                        }
+                       # * = ul
+                       # # = ol
+                       # ; = dt
+                       # : = dd
 
                        $lastPrefixLength = strlen( $lastPrefix );
-                       $preCloseMatch = preg_match('/<\\/pre/i', $oLine );
-                       $preOpenMatch = preg_match('/<pre/i', $oLine );
+                       $preCloseMatch = preg_match( '/<\\/pre/i', $oLine );
+                       $preOpenMatch = preg_match( '/<pre/i', $oLine );
+                       # If not in a <pre> element, scan for and figure out what prefixes are there.
                        if ( !$this->mInPre ) {
                                # Multiple prefixes may abut each other for nested lists.
                                $prefixLength = strspn( $oLine, '*#:;' );
                                $prefix = substr( $oLine, 0, $prefixLength );
 
                                # eh?
+                               # ; and : are both from definition-lists, so they're equivalent
+                               #  for the purposes of determining whether or not we need to open/close
+                               #  elements.
                                $prefix2 = str_replace( ';', ':', $prefix );
                                $t = substr( $oLine, $prefixLength );
                                $this->mInPre = (bool)$preOpenMatch;
@@ -2024,7 +2138,7 @@ class Parser
                        }
 
                        # List generation
-                       if( $prefixLength && $lastPrefix === $prefix2 ) {
+                       if ( $prefixLength && $lastPrefix === $prefix2 ) {
                                # Same as the last item, so no need to deal with nesting or opening stuff
                                $output .= $this->nextItem( substr( $prefix, -1 ) );
                                $paragraphStack = false;
@@ -2035,30 +2149,37 @@ class Parser
                                        # So we check for : in the remainder text to split up the
                                        # title and definition, without b0rking links.
                                        $term = $t2 = '';
-                                       if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                       if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
                                                $t = $t2;
                                                $output .= $term . $this->nextItem( ':' );
                                        }
                                }
-                       } elseif( $prefixLength || $lastPrefixLength ) {
+                       } elseif ( $prefixLength || $lastPrefixLength ) {
+                               # We need to open or close prefixes, or both.
+
                                # Either open or close a level...
                                $commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
                                $paragraphStack = false;
 
-                               while( $commonPrefixLength < $lastPrefixLength ) {
+                               # Close all the prefixes which aren't shared.
+                               while ( $commonPrefixLength < $lastPrefixLength ) {
                                        $output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
                                        --$lastPrefixLength;
                                }
+
+                               # Continue the current prefix if appropriate.
                                if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
                                        $output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
                                }
+
+                               # Open prefixes where appropriate.
                                while ( $prefixLength > $commonPrefixLength ) {
                                        $char = substr( $prefix, $commonPrefixLength, 1 );
                                        $output .= $this->openList( $char );
 
                                        if ( ';' === $char ) {
                                                # FIXME: This is dupe of code above
-                                               if ($this->findColonNoLinks($t, $term, $t2) !== false) {
+                                               if ( $this->findColonNoLinks( $t, $term, $t2 ) !== false ) {
                                                        $t = $t2;
                                                        $output .= $term . $this->nextItem( ':' );
                                                }
@@ -2067,10 +2188,12 @@ class Parser
                                }
                                $lastPrefix = $prefix2;
                        }
-                       if( 0 == $prefixLength ) {
+
+                       # If we have no prefixes, go to paragraph mode.
+                       if ( 0 == $prefixLength ) {
                                wfProfileIn( __METHOD__."-paragraph" );
                                # No prefix (not in list)--go to paragraph mode
-                               // XXX: use a stack for nestable elements like span, table and div
+                               # XXX: use a stack for nestable elements like span, table and div
                                $openmatch = preg_match('/(?:<table|<blockquote|<h1|<h2|<h3|<h4|<h5|<h6|<pre|<tr|<p|<ul|<ol|<li|<\\/tr|<\\/td|<\\/th)/iS', $t );
                                $closematch = preg_match(
                                        '/(?:<\\/table|<\\/blockquote|<\\/h1|<\\/h2|<\\/h3|<\\/h4|<\\/h5|<\\/h6|'.
@@ -2082,29 +2205,25 @@ class Parser
                                        if ( $preOpenMatch and !$preCloseMatch ) {
                                                $this->mInPre = true;
                                        }
-                                       if ( $closematch ) {
-                                               $inBlockElem = false;
-                                       } else {
-                                               $inBlockElem = true;
-                                       }
-                               } else if ( !$inBlockElem && !$this->mInPre ) {
-                                       if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
-                                               // pre
-                                               if ($this->mLastSection !== 'pre') {
+                                       $inBlockElem = !$closematch;
+                               } elseif ( !$inBlockElem && !$this->mInPre ) {
+                                       if ( ' ' == substr( $t, 0, 1 ) and ( $this->mLastSection === 'pre' || trim( $t ) != '' ) ) {
+                                               # pre
+                                               if ( $this->mLastSection !== 'pre' ) {
                                                        $paragraphStack = false;
                                                        $output .= $this->closeParagraph().'<pre>';
                                                        $this->mLastSection = 'pre';
                                                }
                                                $t = substr( $t, 1 );
                                        } else {
-                                               // paragraph
-                                               if ( '' == trim($t) ) {
+                                               # paragraph
+                                               if ( trim( $t ) === '' ) {
                                                        if ( $paragraphStack ) {
                                                                $output .= $paragraphStack.'<br />';
                                                                $paragraphStack = false;
                                                                $this->mLastSection = 'p';
                                                        } else {
-                                                               if ($this->mLastSection !== 'p' ) {
+                                                               if ( $this->mLastSection !== 'p' ) {
                                                                        $output .= $this->closeParagraph();
                                                                        $this->mLastSection = '';
                                                                        $paragraphStack = '<p>';
@@ -2117,7 +2236,7 @@ class Parser
                                                                $output .= $paragraphStack;
                                                                $paragraphStack = false;
                                                                $this->mLastSection = 'p';
-                                                       } else if ($this->mLastSection !== 'p') {
+                                                       } elseif ( $this->mLastSection !== 'p' ) {
                                                                $output .= $this->closeParagraph().'<p>';
                                                                $this->mLastSection = 'p';
                                                        }
@@ -2126,11 +2245,11 @@ class Parser
                                }
                                wfProfileOut( __METHOD__."-paragraph" );
                        }
-                       // somewhere above we forget to get out of pre block (bug 785)
-                       if($preCloseMatch && $this->mInPre) {
+                       # somewhere above we forget to get out of pre block (bug 785)
+                       if ( $preCloseMatch && $this->mInPre ) {
                                $this->mInPre = false;
                        }
-                       if ($paragraphStack === false) {
+                       if ( $paragraphStack === false ) {
                                $output .= $t."\n";
                        }
                }
@@ -2138,7 +2257,7 @@ class Parser
                        $output .= $this->closeList( $prefix2[$prefixLength-1] );
                        --$prefixLength;
                }
-               if ( '' != $this->mLastSection ) {
+               if ( $this->mLastSection != '' ) {
                        $output .= '</' . $this->mLastSection . '>';
                        $this->mLastSection = '';
                }
@@ -2150,31 +2269,32 @@ class Parser
        /**
         * Split up a string on ':', ignoring any occurences inside tags
         * to prevent illegal overlapping.
-        * @param string $str the string to split
-        * @param string &$before set to everything before the ':'
-        * @param string &$after set to everything after the ':'
-        * return string the position of the ':', or false if none found
+        *
+        * @param $str String: the string to split
+        * @param &$before String: set to everything before the ':'
+        * @param &$after String: set to everything after the ':'
+        * return String: the position of the ':', or false if none found
         */
-       function findColonNoLinks($str, &$before, &$after) {
+       function findColonNoLinks( $str, &$before, &$after ) {
                wfProfileIn( __METHOD__ );
 
                $pos = strpos( $str, ':' );
-               if( $pos === false ) {
-                       // Nothing to find!
+               if ( $pos === false ) {
+                       # Nothing to find!
                        wfProfileOut( __METHOD__ );
                        return false;
                }
 
                $lt = strpos( $str, '<' );
-               if( $lt === false || $lt > $pos ) {
-                       // Easy; no tag nesting to worry about
+               if ( $lt === false || $lt > $pos ) {
+                       # Easy; no tag nesting to worry about
                        $before = substr( $str, 0, $pos );
                        $after = substr( $str, $pos+1 );
                        wfProfileOut( __METHOD__ );
                        return $pos;
                }
 
-               // Ugly state machine to walk through avoiding tags.
+               # Ugly state machine to walk through avoiding tags.
                $state = self::COLON_STATE_TEXT;
                $stack = 0;
                $len = strlen( $str );
@@ -2182,67 +2302,67 @@ class Parser
                        $c = $str{$i};
 
                        switch( $state ) {
-                       // (Using the number is a performance hack for common cases)
-                       case 0: // self::COLON_STATE_TEXT:
+                       # (Using the number is a performance hack for common cases)
+                       case 0: # self::COLON_STATE_TEXT:
                                switch( $c ) {
                                case "<":
-                                       // Could be either a <start> tag or an </end> tag
+                                       # Could be either a <start> tag or an </end> tag
                                        $state = self::COLON_STATE_TAGSTART;
                                        break;
                                case ":":
-                                       if( $stack == 0 ) {
-                                               // We found it!
+                                       if ( $stack == 0 ) {
+                                               # We found it!
                                                $before = substr( $str, 0, $i );
                                                $after = substr( $str, $i + 1 );
                                                wfProfileOut( __METHOD__ );
                                                return $i;
                                        }
-                                       // Embedded in a tag; don't break it.
+                                       # Embedded in a tag; don't break it.
                                        break;
                                default:
-                                       // Skip ahead looking for something interesting
+                                       # Skip ahead looking for something interesting
                                        $colon = strpos( $str, ':', $i );
-                                       if( $colon === false ) {
-                                               // Nothing else interesting
+                                       if ( $colon === false ) {
+                                               # Nothing else interesting
                                                wfProfileOut( __METHOD__ );
                                                return false;
                                        }
                                        $lt = strpos( $str, '<', $i );
-                                       if( $stack === 0 ) {
-                                               if( $lt === false || $colon < $lt ) {
-                                                       // We found it!
+                                       if ( $stack === 0 ) {
+                                               if ( $lt === false || $colon < $lt ) {
+                                                       # We found it!
                                                        $before = substr( $str, 0, $colon );
                                                        $after = substr( $str, $colon + 1 );
                                                        wfProfileOut( __METHOD__ );
                                                        return $i;
                                                }
                                        }
-                                       if( $lt === false ) {
-                                               // Nothing else interesting to find; abort!
-                                               // We're nested, but there's no close tags left. Abort!
+                                       if ( $lt === false ) {
+                                               # Nothing else interesting to find; abort!
+                                               # We're nested, but there's no close tags left. Abort!
                                                break 2;
                                        }
-                                       // Skip ahead to next tag start
+                                       # Skip ahead to next tag start
                                        $i = $lt;
                                        $state = self::COLON_STATE_TAGSTART;
                                }
                                break;
-                       case 1: // self::COLON_STATE_TAG:
-                               // In a <tag>
+                       case 1: # self::COLON_STATE_TAG:
+                               # In a <tag>
                                switch( $c ) {
                                case ">":
                                        $stack++;
                                        $state = self::COLON_STATE_TEXT;
                                        break;
                                case "/":
-                                       // Slash may be followed by >?
+                                       # Slash may be followed by >?
                                        $state = self::COLON_STATE_TAGSLASH;
                                        break;
                                default:
-                                       // ignore
+                                       # ignore
                                }
                                break;
-                       case 2: // self::COLON_STATE_TAGSTART:
+                       case 2: # self::COLON_STATE_TAGSTART:
                                switch( $c ) {
                                case "/":
                                        $state = self::COLON_STATE_CLOSETAG;
@@ -2251,18 +2371,18 @@ class Parser
                                        $state = self::COLON_STATE_COMMENT;
                                        break;
                                case ">":
-                                       // Illegal early close? This shouldn't happen D:
+                                       # Illegal early close? This shouldn't happen D:
                                        $state = self::COLON_STATE_TEXT;
                                        break;
                                default:
                                        $state = self::COLON_STATE_TAG;
                                }
                                break;
-                       case 3: // self::COLON_STATE_CLOSETAG:
-                               // In a </tag>
-                               if( $c === ">" ) {
+                       case 3: # self::COLON_STATE_CLOSETAG:
+                               # In a </tag>
+                               if ( $c === ">" ) {
                                        $stack--;
-                                       if( $stack < 0 ) {
+                                       if ( $stack < 0 ) {
                                                wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
                                                wfProfileOut( __METHOD__ );
                                                return false;
@@ -2271,28 +2391,28 @@ class Parser
                                }
                                break;
                        case self::COLON_STATE_TAGSLASH:
-                               if( $c === ">" ) {
-                                       // Yes, a self-closed tag <blah/>
+                               if ( $c === ">" ) {
+                                       # Yes, a self-closed tag <blah/>
                                        $state = self::COLON_STATE_TEXT;
                                } else {
-                                       // Probably we're jumping the gun, and this is an attribute
+                                       # Probably we're jumping the gun, and this is an attribute
                                        $state = self::COLON_STATE_TAG;
                                }
                                break;
-                       case 5: // self::COLON_STATE_COMMENT:
-                               if( $c === "-" ) {
+                       case 5: # self::COLON_STATE_COMMENT:
+                               if ( $c === "-" ) {
                                        $state = self::COLON_STATE_COMMENTDASH;
                                }
                                break;
                        case self::COLON_STATE_COMMENTDASH:
-                               if( $c === "-" ) {
+                               if ( $c === "-" ) {
                                        $state = self::COLON_STATE_COMMENTDASHDASH;
                                } else {
                                        $state = self::COLON_STATE_COMMENT;
                                }
                                break;
                        case self::COLON_STATE_COMMENTDASHDASH:
-                               if( $c === ">" ) {
+                               if ( $c === ">" ) {
                                        $state = self::COLON_STATE_TEXT;
                                } else {
                                        $state = self::COLON_STATE_COMMENT;
@@ -2302,8 +2422,9 @@ class Parser
                                throw new MWException( "State machine error in " . __METHOD__ );
                        }
                }
-               if( $stack > 0 ) {
+               if ( $stack > 0 ) {
                        wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
+                       wfProfileOut( __METHOD__ );
                        return false;
                }
                wfProfileOut( __METHOD__ );
@@ -2315,8 +2436,9 @@ class Parser
         *
         * @private
         */
-       function getVariableValue( $index ) {
-               global $wgContLang, $wgSitename, $wgServer, $wgServerName, $wgScriptPath;
+       function getVariableValue( $index, $frame=false ) {
+               global $wgContLang, $wgSitename, $wgServer;
+               global $wgArticlePath, $wgScriptPath, $wgStylePath;
 
                /**
                 * Some of these require message or data lookups and can be
@@ -2334,13 +2456,13 @@ class Parser
                # Use the time zone
                global $wgLocaltimezone;
                if ( isset( $wgLocaltimezone ) ) {
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
+                       $oldtz = date_default_timezone_get();
+                       date_default_timezone_set( $wgLocaltimezone );
                }
 
-               wfSuppressWarnings(); // E_STRICT system time bitching
                $localTimestamp = date( 'YmdHis', $ts );
                $localMonth = date( 'm', $ts );
+               $localMonth1 = date( 'n', $ts );
                $localMonthName = date( 'n', $ts );
                $localDay = date( 'j', $ts );
                $localDay2 = date( 'd', $ts );
@@ -2349,207 +2471,294 @@ class Parser
                $localYear = date( 'Y', $ts );
                $localHour = date( 'H', $ts );
                if ( isset( $wgLocaltimezone ) ) {
-                       putenv( 'TZ='.$oldtz );
+                       date_default_timezone_set( $oldtz );
                }
-               wfRestoreWarnings();
 
                switch ( $index ) {
                        case 'currentmonth':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                               $value = $wgContLang->formatNum( gmdate( 'm', $ts ) );
+                               break;
+                       case 'currentmonth1':
+                               $value = $wgContLang->formatNum( gmdate( 'n', $ts ) );
+                               break;
                        case 'currentmonthname':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+                               $value = $wgContLang->getMonthName( gmdate( 'n', $ts ) );
+                               break;
                        case 'currentmonthnamegen':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+                               $value = $wgContLang->getMonthNameGen( gmdate( 'n', $ts ) );
+                               break;
                        case 'currentmonthabbrev':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+                               $value = $wgContLang->getMonthAbbreviation( gmdate( 'n', $ts ) );
+                               break;
                        case 'currentday':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+                               $value = $wgContLang->formatNum( gmdate( 'j', $ts ) );
+                               break;
                        case 'currentday2':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+                               $value = $wgContLang->formatNum( gmdate( 'd', $ts ) );
+                               break;
                        case 'localmonth':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localMonth );
+                               $value = $wgContLang->formatNum( $localMonth );
+                               break;
+                       case 'localmonth1':
+                               $value = $wgContLang->formatNum( $localMonth1 );
+                               break;
                        case 'localmonthname':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthName( $localMonthName );
+                               $value = $wgContLang->getMonthName( $localMonthName );
+                               break;
                        case 'localmonthnamegen':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthNameGen( $localMonthName );
+                               $value = $wgContLang->getMonthNameGen( $localMonthName );
+                               break;
                        case 'localmonthabbrev':
-                               return $this->mVarCache[$index] = $wgContLang->getMonthAbbreviation( $localMonthName );
+                               $value = $wgContLang->getMonthAbbreviation( $localMonthName );
+                               break;
                        case 'localday':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay );
+                               $value = $wgContLang->formatNum( $localDay );
+                               break;
                        case 'localday2':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDay2 );
+                               $value = $wgContLang->formatNum( $localDay2 );
+                               break;
                        case 'pagename':
-                               return wfEscapeWikiText( $this->mTitle->getText() );
+                               $value = wfEscapeWikiText( $this->mTitle->getText() );
+                               break;
                        case 'pagenamee':
-                               return $this->mTitle->getPartialURL();
+                               $value = $this->mTitle->getPartialURL();
+                               break;
                        case 'fullpagename':
-                               return wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+                               $value = wfEscapeWikiText( $this->mTitle->getPrefixedText() );
+                               break;
                        case 'fullpagenamee':
-                               return $this->mTitle->getPrefixedURL();
+                               $value = $this->mTitle->getPrefixedURL();
+                               break;
                        case 'subpagename':
-                               return wfEscapeWikiText( $this->mTitle->getSubpageText() );
+                               $value = wfEscapeWikiText( $this->mTitle->getSubpageText() );
+                               break;
                        case 'subpagenamee':
-                               return $this->mTitle->getSubpageUrlForm();
+                               $value = $this->mTitle->getSubpageUrlForm();
+                               break;
                        case 'basepagename':
-                               return wfEscapeWikiText( $this->mTitle->getBaseText() );
+                               $value = wfEscapeWikiText( $this->mTitle->getBaseText() );
+                               break;
                        case 'basepagenamee':
-                               return wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+                               $value = wfUrlEncode( str_replace( ' ', '_', $this->mTitle->getBaseText() ) );
+                               break;
                        case 'talkpagename':
-                               if( $this->mTitle->canTalk() ) {
+                               if ( $this->mTitle->canTalk() ) {
                                        $talkPage = $this->mTitle->getTalkPage();
-                                       return wfEscapeWikiText( $talkPage->getPrefixedText() );
+                                       $value = wfEscapeWikiText( $talkPage->getPrefixedText() );
                                } else {
-                                       return '';
+                                       $value = '';
                                }
+                               break;
                        case 'talkpagenamee':
-                               if( $this->mTitle->canTalk() ) {
+                               if ( $this->mTitle->canTalk() ) {
                                        $talkPage = $this->mTitle->getTalkPage();
-                                       return $talkPage->getPrefixedUrl();
+                                       $value = $talkPage->getPrefixedUrl();
                                } else {
-                                       return '';
+                                       $value = '';
                                }
+                               break;
                        case 'subjectpagename':
                                $subjPage = $this->mTitle->getSubjectPage();
-                               return wfEscapeWikiText( $subjPage->getPrefixedText() );
+                               $value = wfEscapeWikiText( $subjPage->getPrefixedText() );
+                               break;
                        case 'subjectpagenamee':
                                $subjPage = $this->mTitle->getSubjectPage();
-                               return $subjPage->getPrefixedUrl();
+                               $value = $subjPage->getPrefixedUrl();
+                               break;
                        case 'revisionid':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONID}} used, setting vary-revision...\n" );
-                               return $this->mRevisionId;
+                               $value = $this->mRevisionId;
+                               break;
                        case 'revisionday':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONDAY}} used, setting vary-revision...\n" );
-                               return intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+                               $value = intval( substr( $this->getRevisionTimestamp(), 6, 2 ) );
+                               break;
                        case 'revisionday2':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONDAY2}} used, setting vary-revision...\n" );
-                               return substr( $this->getRevisionTimestamp(), 6, 2 );
+                               $value = substr( $this->getRevisionTimestamp(), 6, 2 );
+                               break;
                        case 'revisionmonth':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONMONTH}} used, setting vary-revision...\n" );
-                               return intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+                               $value = substr( $this->getRevisionTimestamp(), 4, 2 );
+                               break;
+                       case 'revisionmonth1':
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
+                               $this->mOutput->setFlag( 'vary-revision' );
+                               wfDebug( __METHOD__ . ": {{REVISIONMONTH1}} used, setting vary-revision...\n" );
+                               $value = intval( substr( $this->getRevisionTimestamp(), 4, 2 ) );
+                               break;
                        case 'revisionyear':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONYEAR}} used, setting vary-revision...\n" );
-                               return substr( $this->getRevisionTimestamp(), 0, 4 );
+                               $value = substr( $this->getRevisionTimestamp(), 0, 4 );
+                               break;
                        case 'revisiontimestamp':
-                               // Let the edit saving system know we should parse the page
-                               // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONTIMESTAMP}} used, setting vary-revision...\n" );
-                               return $this->getRevisionTimestamp();
+                               $value = $this->getRevisionTimestamp();
+                               break;
                        case 'revisionuser':
-                                // Let the edit saving system know we should parse the page
-                                // *after* a revision ID has been assigned. This is for null edits.
+                               # Let the edit saving system know we should parse the page
+                               # *after* a revision ID has been assigned. This is for null edits.
                                $this->mOutput->setFlag( 'vary-revision' );
                                wfDebug( __METHOD__ . ": {{REVISIONUSER}} used, setting vary-revision...\n" );
-                               return $this->getRevisionUser();
+                               $value = $this->getRevisionUser();
+                               break;
                        case 'namespace':
-                               return str_replace('_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                               $value = str_replace( '_',' ',$wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                               break;
                        case 'namespacee':
-                               return wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                               $value = wfUrlencode( $wgContLang->getNsText( $this->mTitle->getNamespace() ) );
+                               break;
                        case 'talkspace':
-                               return $this->mTitle->canTalk() ? str_replace('_',' ',$this->mTitle->getTalkNsText()) : '';
+                               $value = $this->mTitle->canTalk() ? str_replace( '_',' ',$this->mTitle->getTalkNsText() ) : '';
+                               break;
                        case 'talkspacee':
-                               return $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+                               $value = $this->mTitle->canTalk() ? wfUrlencode( $this->mTitle->getTalkNsText() ) : '';
+                               break;
                        case 'subjectspace':
-                               return $this->mTitle->getSubjectNsText();
+                               $value = $this->mTitle->getSubjectNsText();
+                               break;
                        case 'subjectspacee':
-                               return( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+                               $value = ( wfUrlencode( $this->mTitle->getSubjectNsText() ) );
+                               break;
                        case 'currentdayname':
-                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+                               $value = $wgContLang->getWeekdayName( gmdate( 'w', $ts ) + 1 );
+                               break;
                        case 'currentyear':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+                               $value = $wgContLang->formatNum( gmdate( 'Y', $ts ), true );
+                               break;
                        case 'currenttime':
-                               return $this->mVarCache[$index] = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+                               $value = $wgContLang->time( wfTimestamp( TS_MW, $ts ), false, false );
+                               break;
                        case 'currenthour':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+                               $value = $wgContLang->formatNum( gmdate( 'H', $ts ), true );
+                               break;
                        case 'currentweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+                               # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               # int to remove the padding
+                               $value = $wgContLang->formatNum( (int)gmdate( 'W', $ts ) );
+                               break;
                        case 'currentdow':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+                               $value = $wgContLang->formatNum( gmdate( 'w', $ts ) );
+                               break;
                        case 'localdayname':
-                               return $this->mVarCache[$index] = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+                               $value = $wgContLang->getWeekdayName( $localDayOfWeek + 1 );
+                               break;
                        case 'localyear':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localYear, true );
+                               $value = $wgContLang->formatNum( $localYear, true );
+                               break;
                        case 'localtime':
-                               return $this->mVarCache[$index] = $wgContLang->time( $localTimestamp, false, false );
+                               $value = $wgContLang->time( $localTimestamp, false, false );
+                               break;
                        case 'localhour':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localHour, true );
+                               $value = $wgContLang->formatNum( $localHour, true );
+                               break;
                        case 'localweek':
-                               // @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
-                               // int to remove the padding
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( (int)$localWeek );
+                               # @bug 4594 PHP5 has it zero padded, PHP4 does not, cast to
+                               # int to remove the padding
+                               $value = $wgContLang->formatNum( (int)$localWeek );
+                               break;
                        case 'localdow':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( $localDayOfWeek );
+                               $value = $wgContLang->formatNum( $localDayOfWeek );
+                               break;
                        case 'numberofarticles':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::articles() );
+                               $value = $wgContLang->formatNum( SiteStats::articles() );
+                               break;
                        case 'numberoffiles':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::images() );
+                               $value = $wgContLang->formatNum( SiteStats::images() );
+                               break;
                        case 'numberofusers':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::users() );
+                               $value = $wgContLang->formatNum( SiteStats::users() );
+                               break;
                        case 'numberofactiveusers':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::activeUsers() );
+                               $value = $wgContLang->formatNum( SiteStats::activeUsers() );
+                               break;
                        case 'numberofpages':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::pages() );
+                               $value = $wgContLang->formatNum( SiteStats::pages() );
+                               break;
                        case 'numberofadmins':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::numberingroup('sysop') );
+                               $value = $wgContLang->formatNum( SiteStats::numberingroup( 'sysop' ) );
+                               break;
                        case 'numberofedits':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::edits() );
+                               $value = $wgContLang->formatNum( SiteStats::edits() );
+                               break;
                        case 'numberofviews':
-                               return $this->mVarCache[$index] = $wgContLang->formatNum( SiteStats::views() );
+                               $value = $wgContLang->formatNum( SiteStats::views() );
+                               break;
                        case 'currenttimestamp':
-                               return $this->mVarCache[$index] = wfTimestamp( TS_MW, $ts );
+                               $value = wfTimestamp( TS_MW, $ts );
+                               break;
                        case 'localtimestamp':
-                               return $this->mVarCache[$index] = $localTimestamp;
+                               $value = $localTimestamp;
+                               break;
                        case 'currentversion':
-                               return $this->mVarCache[$index] = SpecialVersion::getVersion();
+                               $value = SpecialVersion::getVersion();
+                               break;
+                       case 'articlepath':
+                               return $wgArticlePath;
                        case 'sitename':
                                return $wgSitename;
                        case 'server':
                                return $wgServer;
                        case 'servername':
-                               return $wgServerName;
+                               wfSuppressWarnings(); # May give an E_WARNING in PHP < 5.3.3
+                               $serverName = parse_url( $wgServer, PHP_URL_HOST );
+                               wfRestoreWarnings();
+                               return $serverName ? $serverName : $wgServer;
                        case 'scriptpath':
                                return $wgScriptPath;
+                       case 'stylepath':
+                               return $wgStylePath;
                        case 'directionmark':
                                return $wgContLang->getDirMark();
                        case 'contentlanguage':
-                               global $wgContLanguageCode;
-                               return $wgContLanguageCode;
+                               global $wgLanguageCode;
+                               return $wgLanguageCode;
                        default:
                                $ret = null;
-                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret ) ) )
+                               if ( wfRunHooks( 'ParserGetVariableValueSwitch', array( &$this, &$this->mVarCache, &$index, &$ret, &$frame ) ) ) {
                                        return $ret;
-                               else
+                               } else {
                                        return null;
+                               }
                }
+
+               if ( $index )
+                       $this->mVarCache[$index] = $value;
+
+               return $value;
        }
 
        /**
-        * initialise the magic variables (like CURRENTMONTHNAME)
+        * initialise the magic variables (like CURRENTMONTHNAME) and substitution modifiers
         *
         * @private
         */
        function initialiseVariables() {
                wfProfileIn( __METHOD__ );
                $variableIDs = MagicWord::getVariableIDs();
+               $substIDs = MagicWord::getSubstIDs();
 
                $this->mVariables = new MagicWordArray( $variableIDs );
+               $this->mSubstWords = new MagicWordArray( $substIDs );
                wfProfileOut( __METHOD__ );
        }
 
@@ -2557,8 +2766,8 @@ class Parser
         * Preprocess some wikitext and return the document tree.
         * This is the ghost of replace_variables().
         *
-        * @param string $text The text to parse
-        * @param integer flags Bitwise combination of:
+        * @param $text String: The text to parse
+        * @param $flags Integer: bitwise combination of:
         *          self::PTD_FOR_INCLUSION    Handle <noinclude>/<includeonly> as if the text is being
         *                                     included. Default is to assume a direct page view.
         *
@@ -2575,12 +2784,12 @@ class Parser
         *
         * @private
         */
-       function preprocessToDom ( $text, $flags = 0 ) {
+       function preprocessToDom( $text, $flags = 0 ) {
                $dom = $this->getPreprocessor()->preprocessToObj( $text, $flags );
                return $dom;
        }
 
-       /*
+       /**
         * Return a three-element array: leading whitespace, string contents, trailing whitespace
         */
        public static function splitWhitespace( $s ) {
@@ -2606,11 +2815,11 @@ class Parser
         *  self::OT_PREPROCESS: templates but not extension tags
         *  self::OT_HTML: all templates and extension tags
         *
-        * @param string $tex The text to transform
-        * @param PPFrame $frame Object describing the arguments passed to the template. 
+        * @param $text String: the text to transform
+        * @param $frame PPFrame Object describing the arguments passed to the template.
         *        Arguments may also be provided as an associative array, as was the usual case before MW1.12.
         *        Providing arguments this way may be useful for extensions wishing to perform variable replacement explicitly.
-        * @param bool $argsOnly Only do argument (triple-brace) expansion, not double-brace expansion
+        * @param $argsOnly Boolean: only do argument (triple-brace) expansion, not double-brace expansion
         * @private
         */
        function replaceVariables( $text, $frame = false, $argsOnly = false ) {
@@ -2624,7 +2833,7 @@ class Parser
                        $frame = $this->getPreprocessor()->newFrame();
                } elseif ( !( $frame instanceof PPFrame ) ) {
                        wfDebug( __METHOD__." called using plain parameters instead of a PPFrame instance. Creating custom frame.\n" );
-                       $frame = $this->getPreprocessor()->newCustomFrame($frame);
+                       $frame = $this->getPreprocessor()->newCustomFrame( $frame );
                }
 
                $dom = $this->preprocessToDom( $text );
@@ -2635,11 +2844,11 @@ class Parser
                return $text;
        }
 
-       /// Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
+       # Clean up argument array - refactored in 1.9 so parserfunctions can use it, too.
        static function createAssocArgs( $args ) {
                $assocArgs = array();
                $index = 1;
-               foreach( $args as $arg ) {
+               foreach ( $args as $arg ) {
                        $eqpos = strpos( $arg, '=' );
                        if ( $eqpos === false ) {
                                $assocArgs[$index++] = $arg;
@@ -2662,34 +2871,37 @@ class Parser
         * Warn the user when a parser limitation is reached
         * Will warn at most once the user per limitation type
         *
-        * @param string $limitationType, should be one of:
-        *   'expensive-parserfunction' (corresponding messages: 'expensive-parserfunction-warning', 'expensive-parserfunction-category')
-        *   'post-expand-template-argument' (corresponding messages: 'post-expand-template-argument-warning', 'post-expand-template-argument-category')
-        *   'post-expand-template-inclusion' (corresponding messages: 'post-expand-template-inclusion-warning', 'post-expand-template-inclusion-category')
-        * @params int $current, $max When an explicit limit has been
+        * @param $limitationType String: should be one of:
+        *   'expensive-parserfunction' (corresponding messages:
+        *       'expensive-parserfunction-warning',
+        *       'expensive-parserfunction-category')
+        *   'post-expand-template-argument' (corresponding messages:
+        *       'post-expand-template-argument-warning',
+        *       'post-expand-template-argument-category')
+        *   'post-expand-template-inclusion' (corresponding messages:
+        *       'post-expand-template-inclusion-warning',
+        *       'post-expand-template-inclusion-category')
+        * @param $current Current value
+        * @param $max Maximum allowed, when an explicit limit has been
         *       exceeded, provide the values (optional)
         */
        function limitationWarn( $limitationType, $current=null, $max=null) {
-               $msgName = $limitationType . '-warning';
-               //does no harm if $current and $max are present but are unnecessary for the message
-               $warning = wfMsgExt( $msgName, array( 'parsemag', 'escape' ), $current, $max ); 
+               # does no harm if $current and $max are present but are unnecessary for the message
+               $warning = wfMsgExt( "$limitationType-warning", array( 'parsemag', 'escape' ), $current, $max );
                $this->mOutput->addWarning( $warning );
-               $cat = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( $limitationType . '-category' ) );
-               if ( $cat ) {
-                       $this->mOutput->addCategory( $cat->getDBkey(), $this->getDefaultSort() );
-               }
+               $this->addTrackingCategory( "$limitationType-category" );
        }
 
        /**
         * Return the text of a template, after recursively
         * replacing any variables or templates within the template.
         *
-        * @param array $piece The parts of the template
+        * @param $piece Array: the parts of the template
         *  $piece['title']: the title, i.e. the part before the |
         *  $piece['parts']: the parameter array
         *  $piece['lineStart']: whether the brace was at the start of a line
-        * @param PPFrame The current frame, contains template arguments
-        * @return string the text of the template
+        * @param $frame PPFrame The current frame, contains template arguments
+        * @return String: the text of the template
         * @private
         */
        function braceSubstitution( $piece, $frame ) {
@@ -2706,7 +2918,7 @@ class Parser
                $isLocalObj = false;        # $text is a DOM node needing expansion in the current frame
 
                # Title object, where $text came from
-               $title = NULL;
+               $title = null;
 
                # $part1 is the bit before the first |, and must contain only title characters.
                # Various prefixes will be stripped from it later.
@@ -2718,18 +2930,31 @@ class Parser
                $originalTitle = $part1;
 
                # $args is a list of argument nodes, starting from index 0, not including $part1
-               $args = (null == $piece['parts']) ? array() : $piece['parts'];
+               $args = ( null == $piece['parts'] ) ? array() : $piece['parts'];
                wfProfileOut( __METHOD__.'-setup' );
 
                # SUBST
                wfProfileIn( __METHOD__.'-modifiers' );
                if ( !$found ) {
-                       $mwSubst = MagicWord::get( 'subst' );
-                       if ( $mwSubst->matchStartAndRemove( $part1 ) xor $this->ot['wiki'] ) {
-                               # One of two possibilities is true:
-                               # 1) Found SUBST but not in the PST phase
-                               # 2) Didn't find SUBST and in the PST phase
-                               # In either case, return without further processing
+
+                       $substMatch = $this->mSubstWords->matchStartAndRemove( $part1 );
+
+                       # Possibilities for substMatch: "subst", "safesubst" or FALSE
+                       # Decide whether to expand template or keep wikitext as-is.
+                       if ( $this->ot['wiki'] ) {
+                               if ( $substMatch === false ) {
+                                       $literal = true;  # literal when in PST with no prefix
+                               } else {
+                                       $literal = false; # expand when in PST with subst: or safesubst:
+                               }
+                       } else {
+                               if ( $substMatch == 'subst' ) {
+                                       $literal = true;  # literal when not in PST with plain subst:
+                               } else {
+                                       $literal = false; # expand when not in PST with safesubst: or no prefix
+                               }
+                       }
+                       if ( $literal ) {
                                $text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
                                $isLocalObj = true;
                                $found = true;
@@ -2740,9 +2965,10 @@ class Parser
                if ( !$found && $args->getLength() == 0 ) {
                        $id = $this->mVariables->matchStartToEnd( $part1 );
                        if ( $id !== false ) {
-                               $text = $this->getVariableValue( $id );
-                               if (MagicWord::getCacheTTL($id)>-1)
-                                       $this->mOutput->mContainsOldMagic = true;
+                               $text = $this->getVariableValue( $id, $frame );
+                               if ( MagicWord::getCacheTTL( $id ) > -1 ) {
+                                       $this->mOutput->updateCacheExpiry( MagicWord::getCacheTTL( $id ) );
+                               }
                                $found = true;
                        }
                }
@@ -2779,7 +3005,7 @@ class Parser
                                        $function = $this->mFunctionSynonyms[1][$function];
                                } else {
                                        # Case insensitive functions
-                                       $function = strtolower( $function );
+                                       $function = $wgContLang->lc( $function );
                                        if ( isset( $this->mFunctionSynonyms[0][$function] ) ) {
                                                $function = $this->mFunctionSynonyms[0][$function];
                                        } else {
@@ -2808,21 +3034,23 @@ class Parser
 
                                        # Workaround for PHP bug 35229 and similar
                                        if ( !is_callable( $callback ) ) {
+                                               wfProfileOut( __METHOD__ . '-pfunc' );
+                                               wfProfileOut( __METHOD__ );
                                                throw new MWException( "Tag hook for $function is not callable\n" );
                                        }
                                        $result = call_user_func_array( $callback, $allArgs );
                                        $found = true;
                                        $noparse = true;
                                        $preprocessFlags = 0;
-                                       
+
                                        if ( is_array( $result ) ) {
                                                if ( isset( $result[0] ) ) {
                                                        $text = $result[0];
                                                        unset( $result[0] );
                                                }
 
-                                               // Extract flags into the local scope
-                                               // This allows callers to set flags such as nowiki, found, etc.
+                                               # Extract flags into the local scope
+                                               # This allows callers to set flags such as nowiki, found, etc.
                                                extract( $result );
                                        } else {
                                                $text = $result;
@@ -2843,21 +3071,23 @@ class Parser
                        # Split the title into page and subpage
                        $subpage = '';
                        $part1 = $this->maybeDoSubpageLink( $part1, $subpage );
-                       if ($subpage !== '') {
+                       if ( $subpage !== '' ) {
                                $ns = $this->mTitle->getNamespace();
                        }
                        $title = Title::newFromText( $part1, $ns );
                        if ( $title ) {
                                $titleText = $title->getPrefixedText();
                                # Check for language variants if the template is not found
-                               if($wgContLang->hasVariants() && $title->getArticleID() == 0){
+                               if ( $wgContLang->hasVariants() && $title->getArticleID() == 0 ) {
                                        $wgContLang->findVariantLink( $part1, $title, true );
                                }
                                # Do recursion depth check
                                $limit = $this->mOptions->getMaxTemplateDepth();
                                if ( $frame->depth >= $limit ) {
                                        $found = true;
-                                       $text = '<span class="error">' . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit ) . '</span>';
+                                       $text = '<span class="error">'
+                                               . wfMsgForContent( 'parser-template-recursion-depth-warning', $limit )
+                                               . '</span>';
                                }
                        }
                }
@@ -2866,15 +3096,18 @@ class Parser
                if ( !$found && $title ) {
                        wfProfileIn( __METHOD__ . '-loadtpl' );
                        if ( !$title->isExternal() ) {
-                               if ( $title->getNamespace() == NS_SPECIAL && $this->mOptions->getAllowSpecialInclusion() && $this->ot['html'] ) {
+                               if ( $title->getNamespace() == NS_SPECIAL
+                                       && $this->mOptions->getAllowSpecialInclusion()
+                                       && $this->ot['html'] )
+                               {
                                        $text = SpecialPage::capturePath( $title );
                                        if ( is_string( $text ) ) {
                                                $found = true;
                                                $isHTML = true;
                                                $this->disableCache();
                                        }
-                               } else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
-                                       $found = false; //access denied
+                               } elseif ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
+                                       $found = false; access denied
                                        wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
                                } else {
                                        list( $text, $title ) = $this->getTemplateDom( $title );
@@ -2890,13 +3123,13 @@ class Parser
                                        $found = true;
                                }
                        } elseif ( $title->isTrans() ) {
-                               // Interwiki transclusion
+                               # Interwiki transclusion
                                if ( $this->ot['html'] && !$forceRawInterwiki ) {
                                        $text = $this->interwikiTransclude( $title, 'render' );
                                        $isHTML = true;
                                } else {
                                        $text = $this->interwikiTransclude( $title, 'raw' );
-                                       // Preprocess it like a template
+                                       # Preprocess it like a template
                                        $text = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
                                        $isChildObj = true;
                                }
@@ -2951,22 +3184,31 @@ class Parser
                # immediately preceding headings
                if ( $isHTML ) {
                        $text = "\n\n" . $this->insertStripItem( $text );
-               }
-               # Escape nowiki-style return values
-               elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+               } elseif ( $nowiki && ( $this->ot['html'] || $this->ot['pre'] ) ) {
+                       # Escape nowiki-style return values
                        $text = wfEscapeWikiText( $text );
-               }
-               # Bug 529: if the template begins with a table or block-level
-               # element, it should be treated as beginning a new line.
-               # This behaviour is somewhat controversial.
-               elseif ( is_string( $text ) && !$piece['lineStart'] && preg_match('/^(?:{\\||:|;|#|\*)/', $text)) /*}*/{
+               } elseif ( is_string( $text )
+                       && !$piece['lineStart']
+                       && preg_match( '/^(?:{\\||:|;|#|\*)/', $text ) )
+               {
+                       # Bug 529: if the template begins with a table or block-level
+                       # element, it should be treated as beginning a new line.
+                       # This behaviour is somewhat controversial.
                        $text = "\n" . $text;
                }
 
                if ( is_string( $text ) && !$this->incrementIncludeSize( 'post-expand', strlen( $text ) ) ) {
                        # Error, oversize inclusion
-                       $text = "[[$originalTitle]]" .
-                               $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
+                       if ( $titleText !== false ) {
+                               # Make a working, properly escaped link if possible (bug 23588)
+                               $text = "[[:$titleText]]";
+                       } else {
+                               # This will probably not be a working link, but at least it may
+                               # provide some hint of where the problem is
+                               preg_replace( '/^:/', '', $originalTitle );
+                               $text = "[[:$originalTitle]]";
+                       }
+                       $text .= $this->insertStripItem( '<!-- WARNING: template omitted, post-expand include size too large -->' );
                        $this->limitationWarn( 'post-expand-template-inclusion' );
                }
 
@@ -2997,7 +3239,7 @@ class Parser
                        return array( $this->mTplDomCache[$titleText], $title );
                }
 
-               // Cache miss, go to the database
+               # Cache miss, go to the database
                list( $text, $title ) = $this->fetchTemplateAndTitle( $title );
 
                if ( $text === false ) {
@@ -3008,9 +3250,9 @@ class Parser
                $dom = $this->preprocessToDom( $text, self::PTD_FOR_INCLUSION );
                $this->mTplDomCache[ $titleText ] = $dom;
 
-               if (! $title->equals($cacheTitle)) {
+               if ( !$title->equals( $cacheTitle ) ) {
                        $this->mTplRedirCache[$cacheTitle->getPrefixedDBkey()] =
-                               array( $title->getNamespace(),$cdb = $title->getDBkey() );
+                               array( $title->getNamespace(), $cdb = $title->getDBkey() );
                }
 
                return array( $dom, $title );
@@ -3020,7 +3262,7 @@ class Parser
         * Fetch the unparsed text of a template and register a reference to it.
         */
        function fetchTemplateAndTitle( $title ) {
-               $templateCb = $this->mOptions->getTemplateCallback();
+               $templateCb = $this->mOptions->getTemplateCallback(); # Defaults to Parser::statelessFetchTemplate()
                $stuff = call_user_func( $templateCb, $title, $this );
                $text = $stuff['text'];
                $finalTitle = isset( $stuff['finalTitle'] ) ? $stuff['finalTitle'] : $title;
@@ -3029,11 +3271,11 @@ class Parser
                                $this->mOutput->addTemplate( $dep['title'], $dep['page_id'], $dep['rev_id'] );
                        }
                }
-               return array($text,$finalTitle);
+               return array( $text, $finalTitle );
        }
 
        function fetchTemplate( $title ) {
-               $rv = $this->fetchTemplateAndTitle($title);
+               $rv = $this->fetchTemplateAndTitle( $title );
                return $rv[0];
        }
 
@@ -3046,13 +3288,13 @@ class Parser
                $finalTitle = $title;
                $deps = array();
 
-               // Loop to fetch the article, with up to 1 redirect
+               # Loop to fetch the article, with up to 1 redirect
                for ( $i = 0; $i < 2 && is_object( $title ); $i++ ) {
                        # Give extensions a chance to select the revision instead
-                       $id = false; // Assume current
+                       $id = false; # Assume current
                        wfRunHooks( 'BeforeParserFetchTemplateAndtitle', array( $parser, &$title, &$skip, &$id ) );
 
-                       if( $skip ) {
+                       if ( $skip ) {
                                $text = false;
                                $deps[] = array(
                                        'title' => $title,
@@ -3062,8 +3304,8 @@ class Parser
                        }
                        $rev = $id ? Revision::newFromId( $id ) : Revision::newFromTitle( $title );
                        $rev_id = $rev ? $rev->getId() : 0;
-                       // If there is no current revision, there is no page
-                       if( $id === false && !$rev ) {
+                       # If there is no current revision, there is no page
+                       if ( $id === false && !$rev ) {
                                $linkCache = LinkCache::singleton();
                                $linkCache->addBadLinkObj( $title );
                        }
@@ -3073,13 +3315,13 @@ class Parser
                                'page_id' => $title->getArticleID(),
                                'rev_id' => $rev_id );
 
-                       if( $rev ) {
+                       if ( $rev ) {
                                $text = $rev->getText();
-                       } elseif( $title->getNamespace() == NS_MEDIAWIKI ) {
+                       } elseif ( $title->getNamespace() == NS_MEDIAWIKI ) {
                                global $wgContLang;
                                $message = $wgContLang->lcfirst( $title->getText() );
                                $text = wfMsgForContentNoTrans( $message );
-                               if( wfEmptyMsg( $message, $text ) ) {
+                               if ( wfEmptyMsg( $message, $text ) ) {
                                        $text = false;
                                        break;
                                }
@@ -3089,7 +3331,7 @@ class Parser
                        if ( $text === false ) {
                                break;
                        }
-                       // Redirect?
+                       # Redirect?
                        $finalTitle = $title;
                        $title = Title::newFromRedirect( $text );
                }
@@ -3105,38 +3347,39 @@ class Parser
        function interwikiTransclude( $title, $action ) {
                global $wgEnableScaryTranscluding;
 
-               if (!$wgEnableScaryTranscluding)
-                       return wfMsg('scarytranscludedisabled');
+               if ( !$wgEnableScaryTranscluding ) {
+                       return wfMsgForContent('scarytranscludedisabled');
+               }
 
                $url = $title->getFullUrl( "action=$action" );
 
-               if (strlen($url) > 255)
-                       return wfMsg('scarytranscludetoolong');
-               return $this->fetchScaryTemplateMaybeFromCache($url);
+               if ( strlen( $url ) > 255 ) {
+                       return wfMsgForContent( 'scarytranscludetoolong' );
+               }
+               return $this->fetchScaryTemplateMaybeFromCache( $url );
        }
 
-       function fetchScaryTemplateMaybeFromCache($url) {
+       function fetchScaryTemplateMaybeFromCache( $url ) {
                global $wgTranscludeCacheExpiry;
-               $dbr = wfGetDB(DB_SLAVE);
-               $obj = $dbr->selectRow('transcache', array('tc_time', 'tc_contents'),
-                               array('tc_url' => $url));
-               if ($obj) {
-                       $time = $obj->tc_time;
-                       $text = $obj->tc_contents;
-                       if ($time && time() < $time + $wgTranscludeCacheExpiry ) {
-                               return $text;
-                       }
+               $dbr = wfGetDB( DB_SLAVE );
+               $tsCond = $dbr->timestamp( time() - $wgTranscludeCacheExpiry );
+               $obj = $dbr->selectRow( 'transcache', array('tc_time', 'tc_contents' ),
+                               array( 'tc_url' => $url, "tc_time >= " . $dbr->addQuotes( $tsCond ) ) );
+               if ( $obj ) {
+                       return $obj->tc_contents;
                }
 
-               $text = Http::get($url);
-               if (!$text)
-                       return wfMsg('scarytranscludefailed', $url);
+               $text = Http::get( $url );
+               if ( !$text ) {
+                       return wfMsgForContent( 'scarytranscludefailed', $url );
+               }
 
-               $dbw = wfGetDB(DB_MASTER);
-               $dbw->replace('transcache', array('tc_url'), array(
+               $dbw = wfGetDB( DB_MASTER );
+               $dbw->replace( 'transcache', array('tc_url'), array(
                        'tc_url' => $url,
-                       'tc_time' => time(),
-                       'tc_contents' => $text));
+                       'tc_time' => $dbw->timestamp( time() ),
+                       'tc_contents' => $text)
+               );
                return $text;
        }
 
@@ -3190,61 +3433,59 @@ class Parser
         * Return the text to be used for a given extension tag.
         * This is the ghost of strip().
         *
-        * @param array $params Associative array of parameters:
+        * @param $params Associative array of parameters:
         *     name       PPNode for the tag name
         *     attr       PPNode for unparsed text where tag attributes are thought to be
         *     attributes Optional associative array of parsed attributes
         *     inner      Contents of extension element
         *     noClose    Original text did not have a close tag
-        * @param PPFrame $frame
+        * @param $frame PPFrame
         */
        function extensionSubstitution( $params, $frame ) {
-               global $wgRawHtml, $wgContLang;
-
                $name = $frame->expand( $params['name'] );
                $attrText = !isset( $params['attr'] ) ? null : $frame->expand( $params['attr'] );
                $content = !isset( $params['inner'] ) ? null : $frame->expand( $params['inner'] );
+               $marker = "{$this->mUniqPrefix}-$name-" . sprintf( '%08X', $this->mMarkerIndex++ ) . self::MARKER_SUFFIX;
 
-               $marker = "{$this->mUniqPrefix}-$name-" . sprintf('%08X', $this->mMarkerIndex++) . self::MARKER_SUFFIX;
-
-               if ( $this->ot['html'] ) {
+               $isFunctionTag = isset( $this->mFunctionTagHooks[strtolower($name)] ) &&
+                       ( $this->ot['html'] || $this->ot['pre'] );
+               if ( $isFunctionTag ) {
+                       $markerType = 'none';
+               } else {
+                       $markerType = 'general';
+               }
+               if ( $this->ot['html'] || $isFunctionTag ) {
                        $name = strtolower( $name );
-
                        $attributes = Sanitizer::decodeTagAttributes( $attrText );
                        if ( isset( $params['attributes'] ) ) {
                                $attributes = $attributes + $params['attributes'];
                        }
-                       switch ( $name ) {
-                               case 'html':
-                                       if( $wgRawHtml ) {
-                                               $output = $content;
-                                               break;
-                                       } else {
-                                               throw new MWException( '<html> extension tag encountered unexpectedly' );
-                                       }
-                               case 'nowiki':
-                                       $content = strtr($content, array('-{' => '-&#123;', '}-' => '&#125;-'));
-                                       $output = Xml::escapeTagsOnly( $content );
-                                       break;
-                               case 'math':
-                                       $output = $wgContLang->armourMath(
-                                               MathRenderer::renderMath( $content, $attributes ) );
-                                       break;
-                               case 'gallery':
-                                       $output = $this->renderImageGallery( $content, $attributes );
-                                       break;
-                               default:
-                                       if( isset( $this->mTagHooks[$name] ) ) {
-                                               # Workaround for PHP bug 35229 and similar
-                                               if ( !is_callable( $this->mTagHooks[$name] ) ) {
-                                                       throw new MWException( "Tag hook for $name is not callable\n" );
-                                               }
-                                               $output = call_user_func_array( $this->mTagHooks[$name],
-                                                       array( $content, $attributes, $this ) );
-                                       } else {
-                                               $output = '<span class="error">Invalid tag extension name: ' .
-                                                       htmlspecialchars( $name ) . '</span>';
-                                       }
+
+                       if ( isset( $this->mTagHooks[$name] ) ) {
+                               # Workaround for PHP bug 35229 and similar
+                               if ( !is_callable( $this->mTagHooks[$name] ) ) {
+                                       throw new MWException( "Tag hook for $name is not callable\n" );
+                               }
+                               $output = call_user_func_array( $this->mTagHooks[$name],
+                                       array( $content, $attributes, $this, $frame ) );
+                       } elseif ( isset( $this->mFunctionTagHooks[$name] ) ) {
+                               list( $callback, $flags ) = $this->mFunctionTagHooks[$name];
+                               if ( !is_callable( $callback ) ) {
+                                       throw new MWException( "Tag hook for $name is not callable\n" );
+                               }
+
+                               $output = call_user_func_array( $callback, array( &$this, $frame, $content, $attributes ) );
+                       } else {
+                               $output = '<span class="error">Invalid tag extension name: ' .
+                                       htmlspecialchars( $name ) . '</span>';
+                       }
+
+                       if ( is_array( $output ) ) {
+                               # Extract flags to local scope (to override $markerType)
+                               $flags = $output;
+                               $output = $flags[0];
+                               unset( $flags[0] );
+                               extract( $flags );
                        }
                } else {
                        if ( is_null( $attrText ) ) {
@@ -3264,10 +3505,14 @@ class Parser
                        }
                }
 
-               if ( $name === 'html' || $name === 'nowiki' ) {
+               if ( $markerType === 'none' ) {
+                       return $output;
+               } elseif ( $markerType === 'nowiki' ) {
                        $this->mStripState->nowiki->setPair( $marker, $output );
-               } else {
+               } elseif ( $markerType === 'general' ) {
                        $this->mStripState->general->setPair( $marker, $output );
+               } else {
+                       throw new MWException( __METHOD__.': invalid marker type' );
                }
                return $marker;
        }
@@ -3275,9 +3520,9 @@ class Parser
        /**
         * Increment an include size counter
         *
-        * @param string $type The type of expansion
-        * @param integer $size The size of the text
-        * @return boolean False if this inclusion would take it over the maximum, true otherwise
+        * @param $type String: the type of expansion
+        * @param $size Integer: the size of the text
+        * @return Boolean: false if this inclusion would take it over the maximum, true otherwise
         */
        function incrementIncludeSize( $type, $size ) {
                if ( $this->mIncludeSizes[$type] + $size > $this->mOptions->getMaxIncludeSize( $type ) ) {
@@ -3291,12 +3536,12 @@ class Parser
        /**
         * Increment the expensive function count
         *
-        * @return boolean False if the limit has been exceeded
+        * @return Boolean: false if the limit has been exceeded
         */
        function incrementExpensiveFunctionCount() {
                global $wgExpensiveParserFunctionLimit;
                $this->mExpensiveFunctionCount++;
-               if($this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit) {
+               if ( $this->mExpensiveFunctionCount <= $wgExpensiveParserFunctionLimit ) {
                        return true;
                }
                return false;
@@ -3308,20 +3553,21 @@ class Parser
         */
        function doDoubleUnderscore( $text ) {
                wfProfileIn( __METHOD__ );
-               // The position of __TOC__ needs to be recorded
+
+               # The position of __TOC__ needs to be recorded
                $mw = MagicWord::get( 'toc' );
-               if( $mw->match( $text ) ) {
+               if ( $mw->match( $text ) ) {
                        $this->mShowToc = true;
                        $this->mForceTocPosition = true;
 
-                       // Set a placeholder. At the end we'll fill it in with the TOC.
+                       # Set a placeholder. At the end we'll fill it in with the TOC.
                        $text = $mw->replace( '<!--MWTOC-->', $text, 1 );
 
-                       // Only keep the first one.
+                       # Only keep the first one.
                        $text = $mw->replace( '', $text );
                }
 
-               // Now match and remove the rest of them
+               # Now match and remove the rest of them
                $mwa = MagicWord::getDoubleUnderscoreArray();
                $this->mDoubleUnderscores = $mwa->matchAndRemove( $text );
 
@@ -3332,28 +3578,55 @@ class Parser
                        $this->mShowToc = false;
                }
                if ( isset( $this->mDoubleUnderscores['hiddencat'] ) && $this->mTitle->getNamespace() == NS_CATEGORY ) {
-                       $this->mOutput->setProperty( 'hiddencat', 'y' );
-
-                       $containerCategory = Title::makeTitleSafe( NS_CATEGORY, wfMsgForContent( 'hidden-category-category' ) );
-                       if ( $containerCategory ) {
-                               $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
-                       } else {
-                               wfDebug( __METHOD__.": [[MediaWiki:hidden-category-category]] is not a valid title!\n" );
-                       }
+                       $this->addTrackingCategory( 'hidden-category-category' );
                }
                # (bug 8068) Allow control over whether robots index a page.
                #
                # FIXME (bug 14899): __INDEX__ always overrides __NOINDEX__ here!  This
                # is not desirable, the last one on the page should win.
-               if( isset( $this->mDoubleUnderscores['noindex'] ) ) {
+               if ( isset( $this->mDoubleUnderscores['noindex'] ) && $this->mTitle->canUseNoindex() ) {
                        $this->mOutput->setIndexPolicy( 'noindex' );
-               } elseif( isset( $this->mDoubleUnderscores['index'] ) ) {
+                       $this->addTrackingCategory( 'noindex-category' );
+               }
+               if ( isset( $this->mDoubleUnderscores['index'] ) && $this->mTitle->canUseNoindex() ) {
                        $this->mOutput->setIndexPolicy( 'index' );
+                       $this->addTrackingCategory( 'index-category' );
+               }
+               
+               # Cache all double underscores in the database
+               foreach ( $this->mDoubleUnderscores as $key => $val ) {
+                       $this->mOutput->setProperty( $key, '' );
                }
+
                wfProfileOut( __METHOD__ );
                return $text;
        }
 
+       /**
+        * Add a tracking category, getting the title from a system message,
+        * or print a debug message if the title is invalid.
+        *
+        * @param $msg String: message key
+        * @return Boolean: whether the addition was successful
+        */
+       protected function addTrackingCategory( $msg ) {
+               $cat = wfMsgForContent( $msg );
+
+               # Allow tracking categories to be disabled by setting them to "-"
+               if ( $cat === '-' ) {
+                       return false;
+               }
+
+               $containerCategory = Title::makeTitleSafe( NS_CATEGORY, $cat );
+               if ( $containerCategory ) {
+                       $this->mOutput->addCategory( $containerCategory->getDBkey(), $this->getDefaultSort() );
+                       return true;
+               } else {
+                       wfDebug( __METHOD__.": [[MediaWiki:$msg]] is not a valid title!\n" );
+                       return false;
+               }
+       }
+
        /**
         * This function accomplishes several tasks:
         * 1) Auto-number headings if that option is enabled
@@ -3364,24 +3637,21 @@ class Parser
         * It loops through all headlines, collects the necessary data, then splits up the
         * string and re-inserts the newly formatted headlines.
         *
-        * @param string $text
-        * @param boolean $isMain
+        * @param $text String
+        * @param $origText String: original, untouched wikitext
+        * @param $isMain Boolean
         * @private
         */
-       function formatHeadings( $text, $isMain=true ) {
-               global $wgMaxTocLevel, $wgContLang, $wgEnforceHtmlIds;
+       function formatHeadings( $text, $origText, $isMain=true ) {
+               global $wgMaxTocLevel, $wgContLang, $wgHtml5, $wgExperimentalHtmlIds;
 
                $doNumberHeadings = $this->mOptions->getNumberHeadings();
-               $showEditLink = $this->mOptions->getEditSection();
-
-               // Do not call quickUserCan unless necessary
-               if( $showEditLink && !$this->mTitle->quickUserCan( 'edit' ) ) {
-                       $showEditLink = 0;
-               }
-
+               
                # Inhibit editsection links if requested in the page
-               if ( isset( $this->mDoubleUnderscores['noeditsection'] )  || $this->mOptions->getIsPrintable() ) {
+               if ( isset( $this->mDoubleUnderscores['noeditsection'] ) ) {
                        $showEditLink = 0;
+               } else {
+                       $showEditLink = $this->mOptions->getEditSection();
                }
 
                # Get all headlines for numbering them and adding funky stuff like [edit]
@@ -3392,7 +3662,7 @@ class Parser
                # if there are fewer than 4 headlines in the article, do not show TOC
                # unless it's been explicitly enabled.
                $enoughToc = $this->mShowToc &&
-                       (($numMatches >= 4) || $this->mForceTocPosition);
+                       ( ( $numMatches >= 4 ) || $this->mForceTocPosition );
 
                # Allow user to stipulate that a page should have a "new section"
                # link added via __NEWSECTIONLINK__
@@ -3414,7 +3684,7 @@ class Parser
                }
 
                # We need this to perform operations on the HTML
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
 
                # headline counter
                $headlineCount = 0;
@@ -3427,96 +3697,93 @@ class Parser
                $head = array();
                $sublevelCount = array();
                $levelCount = array();
-               $toclevel = 0;
                $level = 0;
                $prevlevel = 0;
                $toclevel = 0;
                $prevtoclevel = 0;
                $markerRegex = "{$this->mUniqPrefix}-h-(\d+)-" . self::MARKER_SUFFIX;
                $baseTitleText = $this->mTitle->getPrefixedDBkey();
+               $oldType = $this->mOutputType;
+               $this->setOutputType( self::OT_WIKI );
+               $frame = $this->getPreprocessor()->newFrame();
+               $root = $this->preprocessToDom( $origText );
+               $node = $root->getFirstChild();
+               $byteOffset = 0;
                $tocraw = array();
+               $refers = array();
 
-               foreach( $matches[3] as $headline ) {
+               foreach ( $matches[3] as $headline ) {
                        $isTemplate = false;
                        $titleText = false;
                        $sectionIndex = false;
                        $numbering = '';
                        $markerMatches = array();
-                       if (preg_match("/^$markerRegex/", $headline, $markerMatches)) {
+                       if ( preg_match("/^$markerRegex/", $headline, $markerMatches ) ) {
                                $serial = $markerMatches[1];
                                list( $titleText, $sectionIndex ) = $this->mHeadings[$serial];
-                               $isTemplate = ($titleText != $baseTitleText);
-                               $headline = preg_replace("/^$markerRegex/", "", $headline);
+                               $isTemplate = ( $titleText != $baseTitleText );
+                               $headline = preg_replace( "/^$markerRegex/", "", $headline );
                        }
 
-                       if( $toclevel ) {
+                       if ( $toclevel ) {
                                $prevlevel = $level;
-                               $prevtoclevel = $toclevel;
                        }
                        $level = $matches[1][$headlineCount];
 
-                       if( $doNumberHeadings || $enoughToc ) {
-
-                               if ( $level > $prevlevel ) {
-                                       # Increase TOC level
-                                       $toclevel++;
-                                       $sublevelCount[$toclevel] = 0;
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               $prevtoclevel = $toclevel;
-                                               $toc .= $sk->tocIndent();
-                                               $numVisible++;
-                                       }
+                       if ( $level > $prevlevel ) {
+                               # Increase TOC level
+                               $toclevel++;
+                               $sublevelCount[$toclevel] = 0;
+                               if ( $toclevel<$wgMaxTocLevel ) {
+                                       $prevtoclevel = $toclevel;
+                                       $toc .= $sk->tocIndent();
+                                       $numVisible++;
                                }
-                               elseif ( $level < $prevlevel && $toclevel > 1 ) {
-                                       # Decrease TOC level, find level to jump to
+                       } elseif ( $level < $prevlevel && $toclevel > 1 ) {
+                               # Decrease TOC level, find level to jump to
 
-                                       if ( $toclevel == 2 && $level <= $levelCount[1] ) {
-                                               # Can only go down to level 1
-                                               $toclevel = 1;
-                                       } else {
-                                               for ($i = $toclevel; $i > 0; $i--) {
-                                                       if ( $levelCount[$i] == $level ) {
-                                                               # Found last matching level
-                                                               $toclevel = $i;
-                                                               break;
-                                                       }
-                                                       elseif ( $levelCount[$i] < $level ) {
-                                                               # Found first matching level below current level
-                                                               $toclevel = $i + 1;
-                                                               break;
-                                                       }
-                                               }
-                                       }
-                                       if( $toclevel<$wgMaxTocLevel ) {
-                                               if($prevtoclevel < $wgMaxTocLevel) {
-                                                       # Unindent only if the previous toc level was shown :p
-                                                       $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
-                                                       $prevtoclevel = $toclevel;
-                                               } else {
-                                                       $toc .= $sk->tocLineEnd();
-                                               }
+                               for ( $i = $toclevel; $i > 0; $i-- ) {
+                                       if ( $levelCount[$i] == $level ) {
+                                               # Found last matching level
+                                               $toclevel = $i;
+                                               break;
+                                       } elseif ( $levelCount[$i] < $level ) {
+                                               # Found first matching level below current level
+                                               $toclevel = $i + 1;
+                                               break;
                                        }
                                }
-                               else {
-                                       # No change in level, end TOC line
-                                       if( $toclevel<$wgMaxTocLevel ) {
+                               if ( $i == 0 ) {
+                                       $toclevel = 1;
+                               }
+                               if ( $toclevel<$wgMaxTocLevel ) {
+                                       if ( $prevtoclevel < $wgMaxTocLevel ) {
+                                               # Unindent only if the previous toc level was shown :p
+                                               $toc .= $sk->tocUnindent( $prevtoclevel - $toclevel );
+                                               $prevtoclevel = $toclevel;
+                                       } else {
                                                $toc .= $sk->tocLineEnd();
                                        }
                                }
+                       } else {
+                               # No change in level, end TOC line
+                               if ( $toclevel<$wgMaxTocLevel ) {
+                                       $toc .= $sk->tocLineEnd();
+                               }
+                       }
 
-                               $levelCount[$toclevel] = $level;
+                       $levelCount[$toclevel] = $level;
 
-                               # count number of headlines for each level
-                               @$sublevelCount[$toclevel]++;
-                               $dot = 0;
-                               for( $i = 1; $i <= $toclevel; $i++ ) {
-                                       if( !empty( $sublevelCount[$i] ) ) {
-                                               if( $dot ) {
-                                                       $numbering .= '.';
-                                               }
-                                               $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
-                                               $dot = 1;
+                       # count number of headlines for each level
+                       @$sublevelCount[$toclevel]++;
+                       $dot = 0;
+                       for( $i = 1; $i <= $toclevel; $i++ ) {
+                               if ( !empty( $sublevelCount[$i] ) ) {
+                                       if ( $dot ) {
+                                               $numbering .= '.';
                                        }
+                                       $numbering .= $wgContLang->formatNum( $sublevelCount[$i] );
+                                       $dot = 1;
                                }
                        }
 
@@ -3533,23 +3800,19 @@ class Parser
                        # Strip out HTML (other than plain <sup> and <sub>: bug 8393)
                        $tocline = preg_replace(
                                array( '#<(?!/?(sup|sub)).*?'.'>#', '#<(/?(sup|sub)).*?'.'>#' ),
-                               array( '',                          '<$1>'),
+                               array( '',                          '<$1>' ),
                                $safeHeadline
                        );
                        $tocline = trim( $tocline );
 
                        # For the anchor, strip out HTML-y stuff period
                        $safeHeadline = preg_replace( '/<.*?'.'>/', '', $safeHeadline );
-                       $safeHeadline = trim( $safeHeadline );
+                       $safeHeadline = Sanitizer::normalizeSectionNameWhitespace( $safeHeadline );
 
                        # Save headline for section edit hint before it's escaped
                        $headlineHint = $safeHeadline;
 
-                       if ( $wgEnforceHtmlIds ) {
-                               $legacyHeadline = false;
-                               $safeHeadline = Sanitizer::escapeId( $safeHeadline,
-                                       'noninitial' );
-                       } else {
+                       if ( $wgHtml5 && $wgExperimentalHtmlIds ) {
                                # For reverse compatibility, provide an id that's
                                # HTML4-compatible, like we used to.
                                #
@@ -3561,25 +3824,23 @@ class Parser
                                # to type in section names like "abc_.D7.93.D7.90.D7.A4"
                                # manually, so let's not bother worrying about it.
                                $legacyHeadline = Sanitizer::escapeId( $safeHeadline,
-                                       'noninitial' );
-                               $safeHeadline = Sanitizer::escapeId( $safeHeadline, 'xml' );
+                                       array( 'noninitial', 'legacy' ) );
+                               $safeHeadline = Sanitizer::escapeId( $safeHeadline );
 
                                if ( $legacyHeadline == $safeHeadline ) {
                                        # No reason to have both (in fact, we can't)
                                        $legacyHeadline = false;
-                               } elseif ( $legacyHeadline != Sanitizer::escapeId(
-                               $legacyHeadline, 'xml' ) ) {
-                                       # The legacy id is invalid XML.  We used to allow this, but
-                                       # there's no reason to do so anymore.  Backward
-                                       # compatibility will fail slightly in this case, but it's
-                                       # no big deal.
-                                       $legacyHeadline = false;
                                }
+                       } else {
+                               $legacyHeadline = false;
+                               $safeHeadline = Sanitizer::escapeId( $safeHeadline,
+                                       'noninitial' );
                        }
 
-                       # HTML names must be case-insensitively unique (bug 10721).  FIXME:
-                       # Does this apply to Unicode characters?  Because we aren't
-                       # handling those here.
+                       # HTML names must be case-insensitively unique (bug 10721). 
+                       # This does not apply to Unicode characters per 
+                       # http://dev.w3.org/html5/spec/infrastructure.html#case-sensitivity-and-string-comparison
+                       # FIXME: We may be changing them depending on the current locale.
                        $arrayKey = strtolower( $safeHeadline );
                        if ( $legacyHeadline === false ) {
                                $legacyArrayKey = false;
@@ -3600,9 +3861,9 @@ class Parser
                        }
 
                        # Don't number the heading if it is the only one (looks silly)
-                       if( $doNumberHeadings && count( $matches[3] ) > 1) {
+                       if ( $doNumberHeadings && count( $matches[3] ) > 1) {
                                # the two are different if the line contains a link
-                               $headline=$numbering . ' ' . $headline;
+                               $headline = $numbering . ' ' . $headline;
                        }
 
                        # Create the anchor for linking from the TOC to the section
@@ -3614,18 +3875,43 @@ class Parser
                        if ( $legacyHeadline !== false && $refers[$legacyArrayKey] > 1 ) {
                                $legacyAnchor .= '_' . $refers[$legacyArrayKey];
                        }
-                       if( $enoughToc && ( !isset($wgMaxTocLevel) || $toclevel<$wgMaxTocLevel ) ) {
-                               $toc .= $sk->tocLine($anchor, $tocline, $numbering, $toclevel);
-                               $tocraw[] = array( 'toclevel' => $toclevel, 'level' => $level, 'line' => $tocline, 'number' => $numbering );
+                       if ( $enoughToc && ( !isset( $wgMaxTocLevel ) || $toclevel < $wgMaxTocLevel ) ) {
+                               $toc .= $sk->tocLine( $anchor, $tocline,
+                                       $numbering, $toclevel, ( $isTemplate ? false : $sectionIndex ) );
+                       }
+
+                       # Add the section to the section tree
+                       # Find the DOM node for this header
+                       while ( $node && !$isTemplate ) {
+                               if ( $node->getName() === 'h' ) {
+                                       $bits = $node->splitHeading();
+                                       if ( $bits['i'] == $sectionIndex ) {
+                                               break;
+                                       }
+                               }
+                               $byteOffset += mb_strlen( $this->mStripState->unstripBoth(
+                                       $frame->expand( $node, PPFrame::RECOVER_ORIG ) ) );
+                               $node = $node->getNextSibling();
                        }
+                       $tocraw[] = array(
+                               'toclevel' => $toclevel,
+                               'level' => $level,
+                               'line' => $tocline,
+                               'number' => $numbering,
+                               'index' => ( $isTemplate ? 'T-' : '' ) . $sectionIndex,
+                               'fromtitle' => $titleText,
+                               'byteoffset' => ( $isTemplate ? null : $byteOffset ),
+                               'anchor' => $anchor,
+                       );
+
                        # give headline the correct <h#> tag
-                       if( $showEditLink && $sectionIndex !== false ) {
-                               if( $isTemplate ) {
+                       if ( $showEditLink && $sectionIndex !== false ) {
+                               if ( $isTemplate ) {
                                        # Put a T flag in the section identifier, to indicate to extractSections()
                                        # that sections inside <includeonly> should be counted.
-                                       $editlink = $sk->doEditSectionLink(Title::newFromText( $titleText ), "T-$sectionIndex");
+                                       $editlink = $sk->doEditSectionLink( Title::newFromText( $titleText ), "T-$sectionIndex", null, $this->mOptions->getUserLang() );
                                } else {
-                                       $editlink = $sk->doEditSectionLink($this->mTitle, $sectionIndex, $headlineHint);
+                                       $editlink = $sk->doEditSectionLink( $this->mTitle, $sectionIndex, $headlineHint, $this->mOptions->getUserLang() );
                                }
                        } else {
                                $editlink = '';
@@ -3637,18 +3923,23 @@ class Parser
                        $headlineCount++;
                }
 
-               $this->mOutput->setSections( $tocraw );
+               $this->setOutputType( $oldType );
 
                # Never ever show TOC if no headers
-               if( $numVisible < 1 ) {
+               if ( $numVisible < 1 ) {
                        $enoughToc = false;
                }
 
-               if( $enoughToc ) {
-                       if( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
+               if ( $enoughToc ) {
+                       if ( $prevtoclevel > 0 && $prevtoclevel < $wgMaxTocLevel ) {
                                $toc .= $sk->tocUnindent( $prevtoclevel - 1 );
                        }
-                       $toc = $sk->tocList( $toc );
+                       $toc = $sk->tocList( $toc, $this->mOptions->getUserLang() );
+                       $this->mOutput->setTOCHTML( $toc );
+               }
+
+               if ( $isMain ) {
+                       $this->mOutput->setSections( $tocraw );
                }
 
                # split up and insert constructed headlines
@@ -3656,8 +3947,8 @@ class Parser
                $blocks = preg_split( '/<H[1-6].*?' . '>.*?<\/H[1-6]>/i', $text );
                $i = 0;
 
-               foreach( $blocks as $block ) {
-                       if( $showEditLink && $headlineCount > 0 && $i == 0 && $block !== "\n" ) {
+               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
 
@@ -3666,17 +3957,17 @@ class Parser
                                # $full .= $sk->editSectionLink(0);
                        }
                        $full .= $block;
-                       if( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
+                       if ( $enoughToc && !$i && $isMain && !$this->mForceTocPosition ) {
                                # Top anchor now in skin
                                $full = $full.$toc;
                        }
 
-                       if( !empty( $head[$i] ) ) {
+                       if ( !empty( $head[$i] ) ) {
                                $full .= $head[$i];
                        }
                        $i++;
                }
-               if( $this->mForceTocPosition ) {
+               if ( $this->mForceTocPosition ) {
                        return str_replace( '<!--MWTOC-->', $toc, $full );
                } else {
                        return $full;
@@ -3687,15 +3978,14 @@ class Parser
         * Transform wiki markup when saving a page by doing \r\n -> \n
         * conversion, substitting signatures, {{subst:}} templates, etc.
         *
-        * @param string $text the text to transform
-        * @param Title &$title the Title object for the current article
-        * @param User $user the User object describing the current user
-        * @param ParserOptions $options parsing options
-        * @param bool $clearState whether to clear the parser state first
-        * @return string the altered wiki markup
-        * @public
+        * @param $text String: the text to transform
+        * @param $title Title: the Title object for the current article
+        * @param $user User: the User object describing the current user
+        * @param $options ParserOptions: parsing options
+        * @param $clearState Boolean: whether to clear the parser state first
+        * @return String: the altered wiki markup
         */
-       function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
+       public function preSaveTransform( $text, Title $title, $user, $options, $clearState = true ) {
                $this->mOptions = $options;
                $this->setTitle( $title );
                $this->setOutputType( self::OT_WIKI );
@@ -3720,39 +4010,44 @@ class Parser
        function pstPass2( $text, $user ) {
                global $wgContLang, $wgLocaltimezone;
 
-               /* Note: This is the timestamp saved as hardcoded wikitext to
-                * the database, we use $wgContLang here in order to give
-                * everyone the same signature and use the default one rather
-                * than the one selected in each user's preferences.
-                *
-                * (see also bug 12815)
-                */
+               # Note: This is the timestamp saved as hardcoded wikitext to
+               # the database, we use $wgContLang here in order to give
+               # everyone the same signature and use the default one rather
+               # than the one selected in each user's preferences.
+               # (see also bug 12815)
                $ts = $this->mOptions->getTimestamp();
-               $tz = wfMsgForContent( 'timezone-utc' );
                if ( isset( $wgLocaltimezone ) ) {
-                       $unixts = wfTimestamp( TS_UNIX, $ts );
-                       $oldtz = getenv( 'TZ' );
-                       putenv( 'TZ='.$wgLocaltimezone );
-                       $ts = date( 'YmdHis', $unixts );
-                       $tz = date( 'T', $unixts );  # might vary on DST changeover!
+                       $tz = $wgLocaltimezone;
+               } else {
+                       $tz = date_default_timezone_get();
+               }
 
-                       /* Allow translation of timezones trough wiki. date() can return
-                        * whatever crap the system uses, localised or not, so we cannot
-                        * ship premade translations.
-                        */
-                       $key = 'timezone-' . strtolower( trim( $tz ) );
-                       $value = wfMsgForContent( $key );
-                       if ( !wfEmptyMsg( $key, $value ) ) $tz = $value;
+               $unixts = wfTimestamp( TS_UNIX, $ts );
+               $oldtz = date_default_timezone_get();
+               date_default_timezone_set( $tz );
+               $ts = date( 'YmdHis', $unixts );
+               $tzMsg = date( 'T', $unixts );  # might vary on DST changeover!
 
-                       putenv( 'TZ='.$oldtz );
+               # Allow translation of timezones through wiki. date() can return
+               # whatever crap the system uses, localised or not, so we cannot
+               # ship premade translations.
+               $key = 'timezone-' . strtolower( trim( $tzMsg ) );
+               $value = wfMsgForContent( $key );
+               if ( !wfEmptyMsg( $key, $value ) ) {
+                       $tzMsg = $value;
                }
 
-               $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tz)";
+               date_default_timezone_set( $oldtz );
+
+               $d = $wgContLang->timeanddate( $ts, false, false ) . " ($tzMsg)";
 
                # Variable replacement
                # Because mOutputType is OT_WIKI, this will only process {{subst:xxx}} type tags
                $text = $this->replaceVariables( $text );
 
+               # This works almost by chance, as the replaceVariables are done before the getUserSig(),
+               # which may corrupt this parser instance via its wfMsgExt( parsemag ) call-
+
                # Signatures
                $sigText = $this->getUserSig( $user );
                $text = strtr( $text, array(
@@ -3762,7 +4057,6 @@ class Parser
                ) );
 
                # Context links: [[|name]] and [[name (context)|]]
-               #
                global $wgLegalTitleChars;
                $tc = "[$wgLegalTitleChars]";
                $nc = '[ _0-9A-Za-z\x80-\xff-]'; # Namespaces can use non-ascii!
@@ -3781,7 +4075,7 @@ class Parser
                $m = array();
                if ( preg_match( "/^($nc+:|)$tc+?( \\($tc+\\))$/", $t, $m ) ) {
                        $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
-               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && '' != "$m[1]$m[2]" ) {
+               } elseif ( preg_match( "/^($nc+:|)$tc+?(, $tc+|)$/", $t, $m ) && "$m[1]$m[2]" != '' ) {
                        $text = preg_replace( $p2, "[[$m[1]\\1$m[2]|\\1]]", $text );
                } else {
                        # if there's no context, don't bother duplicating the title
@@ -3797,24 +4091,36 @@ class Parser
        /**
         * Fetch the user's signature text, if any, and normalize to
         * validated, ready-to-insert wikitext.
+        * If you have pre-fetched the nickname or the fancySig option, you can
+        * specify them here to save a database query.
         *
-        * @param User $user
+        * @param $user User
+        * @param $nickname String: nickname to use or false to use user's default nickname
+        * @param $fancySig Boolean: whether the nicknname is the complete signature
+        *                  or null to use default value
         * @return string
-        * @private
         */
-       function getUserSig( &$user ) {
+       function getUserSig( &$user, $nickname = false, $fancySig = null ) {
                global $wgMaxSigChars;
 
                $username = $user->getName();
-               $nickname = $user->getOption( 'nickname' );
-               $nickname = $nickname === '' ? $username : $nickname;
 
-               if( mb_strlen( $nickname ) > $wgMaxSigChars ) {
+               # If not given, retrieve from the user object.
+               if ( $nickname === false )
+                       $nickname = $user->getOption( 'nickname' );
+
+               if ( is_null( $fancySig ) ) {
+                       $fancySig = $user->getBoolOption( 'fancysig' );
+               }
+
+               $nickname = $nickname == null ? $username : $nickname;
+
+               if ( mb_strlen( $nickname ) > $wgMaxSigChars ) {
                        $nickname = $username;
                        wfDebug( __METHOD__ . ": $username has overlong signature.\n" );
-               } elseif( $user->getBoolOption( 'fancysig' ) !== false ) {
+               } elseif ( $fancySig !== false ) {
                        # Sig. might contain markup; validate this
-                       if( $this->validateSig( $nickname ) !== false ) {
+                       if ( $this->validateSig( $nickname ) !== false ) {
                                # Validated; clean up (if needed) and return it
                                return $this->cleanSig( $nickname, true );
                        } else {
@@ -3824,7 +4130,7 @@ class Parser
                        }
                }
 
-               // Make sure nickname doesnt get a sig in a sig
+               # Make sure nickname doesnt get a sig in a sig
                $nickname = $this->cleanSigInSig( $nickname );
 
                # If we're still here, make it a link to the user page
@@ -3840,7 +4146,7 @@ class Parser
        /**
         * Check that the user's signature contains no bad XML
         *
-        * @param string $text
+        * @param $text String
         * @return mixed An expanded string, or false if invalid.
         */
        function validateSig( $text ) {
@@ -3853,16 +4159,16 @@ class Parser
         * 1) Strip ~~~, ~~~~ and ~~~~~ out of signatures @see cleanSigInSig
         * 2) Substitute all transclusions
         *
-        * @param string $text
+        * @param $text String
         * @param $parsing Whether we're cleaning (preferences save) or parsing
-        * @return string Signature text
+        * @return String: signature text
         */
        function cleanSig( $text, $parsing = false ) {
                if ( !$parsing ) {
                        global $wgTitle;
+                       $this->mOptions = new ParserOptions;
                        $this->clearState();
                        $this->setTitle( $wgTitle );
-                       $this->mOptions = new ParserOptions;
                        $this->setOutputType = self::OT_PREPROCESS;
                }
 
@@ -3892,8 +4198,9 @@ class Parser
 
        /**
         * Strip ~~~, ~~~~ and ~~~~~ out of signatures
-        * @param string $text
-        * @return string Signature text with /~{3,5}/ removed
+        *
+        * @param $text String
+        * @return String: signature text with /~{3,5}/ removed
         */
        function cleanSigInSig( $text ) {
                $text = preg_replace( '/~{3,5}/', '', $text );
@@ -3903,9 +4210,8 @@ class Parser
        /**
         * Set up some variables which are usually set up in parse()
         * so that an external function can call some class members with confidence
-        * @public
         */
-       function startExternalParse( &$title, $options, $outputType, $clearState = true ) {
+       public function startExternalParse( Title $title = null, ParserOptions $options, $outputType, $clearState = true ) {
                $this->setTitle( $title );
                $this->mOptions = $options;
                $this->setOutputType( $outputType );
@@ -3917,12 +4223,11 @@ class Parser
        /**
         * Wrapper for preprocess()
         *
-        * @param string $text the text to preprocess
-        * @param ParserOptions $options  options
-        * @return string
-        * @public
+        * @param $text String: the text to preprocess
+        * @param $options ParserOptions: options
+        * @return String
         */
-       function transformMsg( $text, $options ) {
+       public function transformMsg( $text, $options ) {
                global $wgTitle;
                static $executing = false;
 
@@ -3932,34 +4237,37 @@ class Parser
                }
                $executing = true;
 
-               wfProfileIn(__METHOD__);
-               $text = $this->preprocess( $text, $wgTitle, $options );
+               wfProfileIn( __METHOD__ );
+               $title = $wgTitle;
+               if ( !$title ) {
+                       # It's not uncommon having a null $wgTitle in scripts. See r80898
+                       # Create a ghost title in such case
+                       $title = Title::newFromText( 'Dwimmerlaik' );
+               }
+               $text = $this->preprocess( $text, $title, $options );
 
                $executing = false;
-               wfProfileOut(__METHOD__);
+               wfProfileOut( __METHOD__ );
                return $text;
        }
 
        /**
         * Create an HTML-style tag, e.g. <yourtag>special text</yourtag>
         * The callback should have the following form:
-        *    function myParserHook( $text, $params, &$parser ) { ... }
+        *    function myParserHook( $text, $params, $parser ) { ... }
         *
         * Transform and return $text. Use $parser for any required context, e.g. use
         * $parser->getTitle() and $parser->getOptions() not $wgTitle or $wgOut->mParserOptions
         *
-        * @public
-        *
-        * @param mixed $tag The tag to use, e.g. 'hook' for <hook>
-        * @param mixed $callback The callback function (and object) to use for the tag
-        *
+        * @param $tag Mixed: the tag to use, e.g. 'hook' for <hook>
+        * @param $callback Mixed: the callback function (and object) to use for the tag
         * @return The old value of the mTagHooks array associated with the hook
         */
-       function setHook( $tag, $callback ) {
+       public function setHook( $tag, $callback ) {
                $tag = strtolower( $tag );
                $oldVal = isset( $this->mTagHooks[$tag] ) ? $this->mTagHooks[$tag] : null;
                $this->mTagHooks[$tag] = $callback;
-               if( !in_array( $tag, $this->mStripList ) ) {
+               if ( !in_array( $tag, $this->mStripList ) ) {
                        $this->mStripList[] = $tag;
                }
 
@@ -3998,41 +4306,41 @@ class Parser
         *   nowiki                    Wiki markup in the return value should be escaped
         *   isHTML                    The returned text is HTML, armour it against wikitext transformation
         *
-        * @public
-        *
-        * @param string $id The magic word ID
-        * @param mixed $callback The callback function (and object) to use
-        * @param integer $flags a combination of the following flags:
+        * @param $id String: The magic word ID
+        * @param $callback Mixed: the callback function (and object) to use
+        * @param $flags Integer: a combination of the following flags:
         *     SFH_NO_HASH   No leading hash, i.e. {{plural:...}} instead of {{#if:...}}
         *
-        *     SFH_OBJECT_ARGS   Pass the template arguments as PPNode objects instead of text. This 
+        *     SFH_OBJECT_ARGS   Pass the template arguments as PPNode objects instead of text. This
         *     allows for conditional expansion of the parse tree, allowing you to eliminate dead
-        *     branches and thus speed up parsing. It is also possible to analyse the parse tree of 
+        *     branches and thus speed up parsing. It is also possible to analyse the parse tree of
         *     the arguments, and to control the way they are expanded.
         *
         *     The $frame parameter is a PPFrame. This can be used to produce expanded text from the
         *     arguments, for instance:
         *         $text = isset( $args[0] ) ? $frame->expand( $args[0] ) : '';
         *
-        *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in 
+        *     For technical reasons, $args[0] is pre-expanded and will be a string. This may change in
         *     future versions. Please call $frame->expand() on it anyway so that your code keeps
         *     working if/when this is changed.
         *
         *     If you want whitespace to be trimmed from $args, you need to do it yourself, post-
         *     expansion.
         *
-        *     Please read the documentation in includes/parser/Preprocessor.php for more information 
+        *     Please read the documentation in includes/parser/Preprocessor.php for more information
         *     about the methods available in PPFrame and PPNode.
         *
         * @return The old callback function for this name, if any
         */
-       function setFunctionHook( $id, $callback, $flags = 0 ) {
+       public function setFunctionHook( $id, $callback, $flags = 0 ) {
+               global $wgContLang;
+
                $oldVal = isset( $this->mFunctionHooks[$id] ) ? $this->mFunctionHooks[$id][0] : null;
                $this->mFunctionHooks[$id] = array( $callback, $flags );
 
                # Add to function cache
                $mw = MagicWord::get( $id );
-               if( !$mw )
+               if ( !$mw )
                        throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
 
                $synonyms = $mw->getSynonyms();
@@ -4041,7 +4349,7 @@ class Parser
                foreach ( $synonyms as $syn ) {
                        # Case
                        if ( !$sensitive ) {
-                               $syn = strtolower( $syn );
+                               $syn = $wgContLang->lc( $syn );
                        }
                        # Add leading hash
                        if ( !( $flags & SFH_NO_HASH ) ) {
@@ -4059,13 +4367,32 @@ class Parser
        /**
         * Get all registered function hook identifiers
         *
-        * @return array
+        * @return Array
         */
        function getFunctionHooks() {
                return array_keys( $this->mFunctionHooks );
        }
 
        /**
+        * Create a tag function, e.g. <test>some stuff</test>.
+        * Unlike tag hooks, tag functions are parsed at preprocessor level.
+        * Unlike parser functions, their content is not preprocessed.
+        */
+       function setFunctionTagHook( $tag, $callback, $flags ) {
+               $tag = strtolower( $tag );
+               $old = isset( $this->mFunctionTagHooks[$tag] ) ?
+                       $this->mFunctionTagHooks[$tag] : null;
+               $this->mFunctionTagHooks[$tag] = array( $callback, $flags );
+
+               if ( !in_array( $tag, $this->mStripList ) ) {
+                       $this->mStripList[] = $tag;
+               }
+
+               return $old;
+       }
+
+       /**
+        * FIXME: update documentation. makeLinkObj() is deprecated.
         * Replace <!--LINK--> link placeholders with actual links, in the buffer
         * Placeholders created in Skin::makeLinkObj()
         * Returns an array of link CSS classes, indexed by PDBK.
@@ -4077,26 +4404,14 @@ class Parser
        /**
         * Replace <!--LINK--> link placeholders with plain text of links
         * (not HTML-formatted).
-        * @param string $text
-        * @return string
+        *
+        * @param $text String
+        * @return String
         */
        function replaceLinkHoldersText( $text ) {
                return $this->mLinkHolders->replaceText( $text );
        }
 
-       /**
-        * Tag hook handler for 'pre'.
-        */
-       function renderPreTag( $text, $attribs ) {
-               // Backwards-compatibility hack
-               $content = StringUtils::delimiterReplace( '<nowiki>', '</nowiki>', '$1', $text, 'i' );
-
-               $attribs = Sanitizer::validateTagAttributes( $attribs, 'pre' );
-               return Xml::openElement( 'pre', $attribs ) .
-                       Xml::escapeTagsOnly( $content ) .
-                       '</pre>';
-       }
-
        /**
         * Renders an image gallery from a text with one line per image.
         * text labels may be given by using |-style alternative text. E.g.
@@ -4114,22 +4429,27 @@ class Parser
                $ig->setParser( $this );
                $ig->setHideBadImages();
                $ig->setAttributes( Sanitizer::validateTagAttributes( $params, 'table' ) );
-               $ig->useSkin( $this->mOptions->getSkin() );
+               $ig->useSkin( $this->mOptions->getSkin( $this->mTitle ) );
                $ig->mRevisionId = $this->mRevisionId;
 
-               if( isset( $params['caption'] ) ) {
+               if ( isset( $params['showfilename'] ) ) {
+                       $ig->setShowFilename( true );
+               } else {
+                       $ig->setShowFilename( false );
+               }
+               if ( isset( $params['caption'] ) ) {
                        $caption = $params['caption'];
                        $caption = htmlspecialchars( $caption );
                        $caption = $this->replaceInternalLinks( $caption );
                        $ig->setCaptionHtml( $caption );
                }
-               if( isset( $params['perrow'] ) ) {
+               if ( isset( $params['perrow'] ) ) {
                        $ig->setPerRow( $params['perrow'] );
                }
-               if( isset( $params['widths'] ) ) {
+               if ( isset( $params['widths'] ) ) {
                        $ig->setWidths( $params['widths'] );
                }
-               if( isset( $params['heights'] ) ) {
+               if ( isset( $params['heights'] ) ) {
                        $ig->setHeights( $params['heights'] );
                }
 
@@ -4145,12 +4465,13 @@ class Parser
                        if ( count( $matches ) == 0 ) {
                                continue;
                        }
-                       
-                       if ( strpos( $matches[0], '%' ) !== false )
+
+                       if ( strpos( $matches[0], '%' ) !== false ) {
                                $matches[1] = urldecode( $matches[1] );
-                       $tp = Title::newFromText( $matches[1]/*, NS_FILE*/ );
+                       }
+                       $tp = Title::newFromText( $matches[1] );
                        $nt =& $tp;
-                       if( is_null( $nt ) ) {
+                       if ( is_null( $nt ) ) {
                                # Bogus title. Ignore these so we don't bomb out later.
                                continue;
                        }
@@ -4179,7 +4500,7 @@ class Parser
                        $handlerClass = '';
                }
                if ( !isset( $this->mImageParams[$handlerClass]  ) ) {
-                       // Initialise static lists
+                       # Initialise static lists
                        static $internalParamNames = array(
                                'horizAlign' => array( 'left', 'right', 'center', 'none' ),
                                'vertAlign' => array( 'baseline', 'sub', 'super', 'top', 'text-top', 'middle',
@@ -4198,7 +4519,7 @@ class Parser
                                }
                        }
 
-                       // Add handler params
+                       # Add handler params
                        $paramMap = $internalParamMap;
                        if ( $handler ) {
                                $handlerParamMap = $handler->getParamMap();
@@ -4214,9 +4535,10 @@ class Parser
 
        /**
         * Parse image options text and use it to make an image
-        * @param Title $title
-        * @param string $options
-        * @param LinkHolderArray $holders
+        *
+        * @param $title Title
+        * @param $options String
+        * @param $holders LinkHolderArray
         */
        function makeImage( $title, $options, $holders = false ) {
                # Check if the options text is of the form "options|alt text"
@@ -4227,11 +4549,13 @@ class Parser
                #  * none       same, but not aligned
                #  * ___px      scale to ___ pixels width, no aligning. e.g. use in taxobox
                #  * center     center the image
-               #  * framed     Keep original image size, no magnify-button.
+               #  * frame      Keep original image size, no magnify-button.
+               #  * framed     Same as "frame"
                #  * frameless  like 'thumb' but without a frame. Keeps user preferences for width
                #  * upright    reduce width for upright images, rounded to full __0 px
                #  * border     draw a 1px border around the image
                #  * alt        Text for HTML alt attribute (defaults to empty)
+               #  * link       Set the target of the image link. Can be external, interwiki, or local
                # vertical-align values (no % or length right now):
                #  * baseline
                #  * sub
@@ -4243,7 +4567,7 @@ class Parser
                #  * text-bottom
 
                $parts = StringUtils::explode( "|", $options );
-               $sk = $this->mOptions->getSkin();
+               $sk = $this->mOptions->getSkin( $this->mTitle );
 
                # Give extensions a chance to select the file revision for us
                $skip = $time = $descQuery = false;
@@ -4254,16 +4578,7 @@ class Parser
                }
 
                # Get the file
-               $imagename = $title->getDBkey();
-               if ( isset( $this->mFileCache[$imagename][$time] ) ) {
-                       $file = $this->mFileCache[$imagename][$time];
-               } else {
-                       $file = wfFindFile( $title, $time );
-                       if ( count( $this->mFileCache ) > 1000 ) {
-                               $this->mFileCache = array();
-                       }
-                       $this->mFileCache[$imagename][$time] = $file;
-               }
+               $file = wfFindFile( $title, array( 'time' => $time ) );
                # Get parameter map
                $handler = $file ? $file->getHandler() : false;
 
@@ -4273,15 +4588,15 @@ class Parser
                $caption = '';
                $params = array( 'frame' => array(), 'handler' => array(),
                        'horizAlign' => array(), 'vertAlign' => array() );
-               foreach( $parts as $part ) {
+               foreach ( $parts as $part ) {
                        $part = trim( $part );
                        list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
                        $validated = false;
-                       if( isset( $paramMap[$magicName] ) ) {
+                       if ( isset( $paramMap[$magicName] ) ) {
                                list( $type, $paramName ) = $paramMap[$magicName];
 
-                               // Special case; width and height come in one variable together
-                               if( $type === 'handler' && $paramName === 'width' ) {
+                               # Special case; width and height come in one variable together
+                               if ( $type === 'handler' && $paramName === 'width' ) {
                                        $m = array();
                                        # (bug 13500) In both cases (width/height and width only),
                                        # permit trailing "px" for backward compatibility.
@@ -4302,7 +4617,7 @@ class Parser
                                                        $params[$type]['width'] = $width;
                                                        $validated = true;
                                                }
-                                       } // else no validation -- bug 13436
+                                       } # else no validation -- bug 13436
                                } else {
                                        if ( $type === 'handler' ) {
                                                # Validate handler parameter
@@ -4312,9 +4627,9 @@ class Parser
                                                switch( $paramName ) {
                                                case 'manualthumb':
                                                case 'alt':
-                                                       // @fixme - possibly check validity here for
-                                                       // manualthumb? downstream behavior seems odd with
-                                                       // missing manual thumbs.
+                                                       # @todo Fixme: possibly check validity here for
+                                                       # manualthumb? downstream behavior seems odd with
+                                                       # missing manual thumbs.
                                                        $validated = true;
                                                        $value = $this->stripAltText( $value, $holders );
                                                        break;
@@ -4329,6 +4644,9 @@ class Parser
                                                                if ( preg_match( "/^($prots)$chars+$/", $value, $m ) ) {
                                                                        $paramName = 'link-url';
                                                                        $this->mOutput->addExternalLink( $value );
+                                                                       if ( $this->mOptions->getExternalLinkTarget() ) {
+                                                                               $params[$type]['link-target'] = $this->mOptions->getExternalLinkTarget();
+                                                                       }
                                                                        $validated = true;
                                                                }
                                                        } else {
@@ -4342,7 +4660,7 @@ class Parser
                                                        }
                                                        break;
                                                default:
-                                                       // Most other things appear to be empty or numeric...
+                                                       # Most other things appear to be empty or numeric...
                                                        $validated = ( $value === false || is_numeric( trim( $value ) ) );
                                                }
                                        }
@@ -4367,7 +4685,11 @@ class Parser
 
                $params['frame']['caption'] = $caption;
 
-               $params['frame']['title'] = $this->stripAltText( $caption, $holders );
+               # Will the image be presented in a frame, with the caption below?
+               $imageIsFramed = isset( $params['frame']['frame'] ) ||
+                                isset( $params['frame']['framed'] ) ||
+                                isset( $params['frame']['thumbnail'] ) ||
+                                isset( $params['frame']['manualthumb'] );
 
                # In the old days, [[Image:Foo|text...]] would set alt text.  Later it
                # came to also set the caption, ordinary text after the image -- which
@@ -4385,17 +4707,33 @@ class Parser
                # named parameter entirely for images without a caption; adding an ex-
                # plicit caption= parameter and preserving the old magic unnamed para-
                # meter for BC; ...
-               if( $caption !== '' && !isset( $params['frame']['alt'] )
-               && !isset( $params['frame']['framed'] )
-               && !isset( $params['frame']['thumbnail'] )
-               && !isset( $params['frame']['manualthumb'] ) ) {
-                       $params['frame']['alt'] = $params['frame']['title'];
+               if ( $imageIsFramed ) { # Framed image
+                       if ( $caption === '' && !isset( $params['frame']['alt'] ) ) {
+                               # No caption or alt text, add the filename as the alt text so
+                               # that screen readers at least get some description of the image
+                               $params['frame']['alt'] = $title->getText();
+                       }
+                       # Do not set $params['frame']['title'] because tooltips don't make sense
+                       # for framed images
+               } else { # Inline image
+                       if ( !isset( $params['frame']['alt'] ) ) {
+                               # No alt text, use the "caption" for the alt text
+                               if ( $caption !== '') {
+                                       $params['frame']['alt'] = $this->stripAltText( $caption, $holders );
+                               } else {
+                                       # No caption, fall back to using the filename for the
+                                       # alt text
+                                       $params['frame']['alt'] = $title->getText();
+                               }
+                       }
+                       # Use the "caption" for the tooltip text
+                       $params['frame']['title'] = $this->stripAltText( $caption, $holders );
                }
 
                wfRunHooks( 'ParserMakeImageParams', array( $title, $file, &$params ) );
 
                # Linker does the rest
-               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery );
+               $ret = $sk->makeImageLink2( $title, $file, $params['frame'], $params['handler'], $time, $descQuery, $this->mOptions->getThumbSize() );
 
                # Give the handler a chance to modify the parser object
                if ( $handler ) {
@@ -4404,7 +4742,7 @@ class Parser
 
                return $ret;
        }
-       
+
        protected function stripAltText( $caption, $holders ) {
                # Strip bad stuff out of the title (tooltip).  We can't just use
                # replaceLinkHoldersText() here, because if this function is called
@@ -4420,7 +4758,7 @@ class Parser
                # remove the tags
                $tooltip = $this->mStripState->unstripBoth( $tooltip );
                $tooltip = Sanitizer::stripAllTags( $tooltip );
-               
+
                return $tooltip;
        }
 
@@ -4430,15 +4768,17 @@ class Parser
         */
        function disableCache() {
                wfDebug( "Parser output marked as uncacheable.\n" );
-               $this->mOutput->mCacheTime = -1;
+               $this->mOutput->setCacheTime( -1 ); // old style, for compatibility
+               $this->mOutput->updateCacheExpiry( 0 ); // new style, for consistency
        }
 
-       /**#@+
+       /**
         * Callback from the Sanitizer for expanding items found in HTML attribute
         * values, so they can be safely tested and escaped.
-        * @param string $text
-        * @param PPFrame $frame
-        * @return string
+        *
+        * @param $text String
+        * @param $frame PPFrame
+        * @return String
         * @private
         */
        function attributeStripCallback( &$text, $frame = false ) {
@@ -4447,22 +4787,12 @@ class Parser
                return $text;
        }
 
-       /**#@-*/
-
-       /**#@+
-        * Accessor/mutator
-        */
-       function Title( $x = NULL ) { return wfSetVar( $this->mTitle, $x ); }
-       function Options( $x = NULL ) { return wfSetVar( $this->mOptions, $x ); }
-       function OutputType( $x = NULL ) { return wfSetVar( $this->mOutputType, $x ); }
-       /**#@-*/
-
-       /**#@+
+       /**
         * Accessor
         */
-       function getTags() { return array_merge( array_keys($this->mTransparentTagHooks), array_keys( $this->mTagHooks ) ); }
-       /**#@-*/
-
+       function getTags() {
+               return array_merge( array_keys( $this->mTransparentTagHooks ), array_keys( $this->mTagHooks ) );
+       }
 
        /**
         * Break wikitext input into sections, and either pull or replace
@@ -4470,8 +4800,8 @@ class Parser
         *
         * External callers should use the getSection and replaceSection methods.
         *
-        * @param string $text Page wikitext
-        * @param string $section A section identifier string of the form:
+        * @param $text String: Page wikitext
+        * @param $section String: a section identifier string of the form:
         *   <flag1> - <flag2> - ... - <section number>
         *
         * Currently the only recognised flag is "T", which means the target section number
@@ -4484,21 +4814,21 @@ class Parser
         * pull the given section along with its lower-level subsections. If the section is
         * not found, $mode=get will return $newtext, and $mode=replace will return $text.
         *
-        * @param string $mode One of "get" or "replace"
-        * @param string $newText Replacement text for section data.
-        * @return string for "get", the extracted section text.
-        *                for "replace", the whole page with the section replaced.
+        * @param $mode String: one of "get" or "replace"
+        * @param $newText String: replacement text for section data.
+        * @return String: for "get", the extracted section text.
+        *                 for "replace", the whole page with the section replaced.
         */
        private function extractSections( $text, $section, $mode, $newText='' ) {
                global $wgTitle;
-               $this->clearState();
-               $this->setTitle( $wgTitle ); // not generally used but removes an ugly failure mode
                $this->mOptions = new ParserOptions;
-               $this->setOutputType( self::OT_WIKI );
+               $this->clearState();
+               $this->setTitle( $wgTitle ); # not generally used but removes an ugly failure mode
+               $this->setOutputType( self::OT_PLAIN );
                $outText = '';
                $frame = $this->getPreprocessor()->newFrame();
 
-               // Process section extraction flags
+               # Process section extraction flags
                $flags = 0;
                $sectionParts = explode( '-', $section );
                $sectionIndex = array_pop( $sectionParts );
@@ -4507,23 +4837,23 @@ class Parser
                                $flags |= self::PTD_FOR_INCLUSION;
                        }
                }
-               // Preprocess the text
+               # Preprocess the text
                $root = $this->preprocessToDom( $text, $flags );
 
-               // <h> nodes indicate section breaks
-               // They can only occur at the top level, so we can find them by iterating the root's children
+               # <h> nodes indicate section breaks
+               # They can only occur at the top level, so we can find them by iterating the root's children
                $node = $root->getFirstChild();
 
-               // Find the target section
+               # Find the target section
                if ( $sectionIndex == 0 ) {
-                       // Section zero doesn't nest, level=big
+                       # Section zero doesn't nest, level=big
                        $targetLevel = 1000;
                } else {
-            while ( $node ) {
-                if ( $node->getName() === 'h' ) {
-                    $bits = $node->splitHeading();
+                       while ( $node ) {
+                               if ( $node->getName() === 'h' ) {
+                                       $bits = $node->splitHeading();
                                        if ( $bits['i'] == $sectionIndex ) {
-                                       $targetLevel = $bits['level'];
+                                               $targetLevel = $bits['level'];
                                                break;
                                        }
                                }
@@ -4535,7 +4865,7 @@ class Parser
                }
 
                if ( !$node ) {
-                       // Not found
+                       # Not found
                        if ( $mode === 'get' ) {
                                return $newText;
                        } else {
@@ -4543,7 +4873,7 @@ class Parser
                        }
                }
 
-               // Find the end of the section, including nested sections
+               # Find the end of the section, including nested sections
                do {
                        if ( $node->getName() === 'h' ) {
                                $bits = $node->splitHeading();
@@ -4558,13 +4888,13 @@ class Parser
                        $node = $node->getNextSibling();
                } while ( $node );
 
-               // Write out the remainder (in replace mode only)
+               # Write out the remainder (in replace mode only)
                if ( $mode === 'replace' ) {
-                       // Output the replacement text
-                       // Add two newlines on -- trailing whitespace in $newText is conventionally
-                       // stripped by the editor, so we need both newlines to restore the paragraph gap
-                       // Only add trailing whitespace if there is newText
-                       if($newText != "") {
+                       # Output the replacement text
+                       # Add two newlines on -- trailing whitespace in $newText is conventionally
+                       # stripped by the editor, so we need both newlines to restore the paragraph gap
+                       # Only add trailing whitespace if there is newText
+                       if ( $newText != "" ) {
                                $outText .= $newText . "\n\n";
                        }
 
@@ -4575,7 +4905,7 @@ class Parser
                }
 
                if ( is_string( $outText ) ) {
-                       // Re-insert stripped tags
+                       # Re-insert stripped tags
                        $outText = rtrim( $this->mStripState->unstripBoth( $outText ) );
                }
 
@@ -4589,19 +4919,37 @@ class Parser
         *
         * If a section contains subsections, these are also returned.
         *
-        * @param string $text text to look in
-        * @param string $section section identifier
-        * @param string $deftext default to return if section is not found
+        * @param $text String: text to look in
+        * @param $section String: section identifier
+        * @param $deftext String: default to return if section is not found
         * @return string text of the requested section
         */
        public function getSection( $text, $section, $deftext='' ) {
                return $this->extractSections( $text, $section, "get", $deftext );
        }
 
+       /**
+        * This function returns $oldtext after the content of the section
+        * specified by $section has been replaced with $text.
+        *
+        * @param $text String: former text of the article
+        * @param $section Numeric: section identifier
+        * @param $text String: replacing text
+        * #return String: modified text
+        */
        public function replaceSection( $oldtext, $section, $text ) {
                return $this->extractSections( $oldtext, $section, "replace", $text );
        }
 
+       /**
+        * Get the ID of the revision we are parsing
+        *
+        * @return Mixed: integer or null
+        */
+       function getRevisionId() {
+               return $this->mRevisionId;
+       }
+
        /**
         * Get the timestamp associated with the current revision, adjusted for
         * the default server-local timestamp
@@ -4614,18 +4962,18 @@ class Parser
                        $timestamp = $dbr->selectField( 'revision', 'rev_timestamp',
                                        array( 'rev_id' => $this->mRevisionId ), __METHOD__ );
 
-                       // Normalize timestamp to internal MW format for timezone processing.
-                       // This has the added side-effect of replacing a null value with
-                       // the current time, which gives us more sensible behavior for
-                       // previews.
+                       # Normalize timestamp to internal MW format for timezone processing.
+                       # This has the added side-effect of replacing a null value with
+                       # the current time, which gives us more sensible behavior for
+                       # previews.
                        $timestamp = wfTimestamp( TS_MW, $timestamp );
 
-                       // The cryptic '' timezone parameter tells to use the site-default
-                       // timezone offset instead of the user settings.
-                       //
-                       // Since this value will be saved into the parser cache, served
-                       // to other users, and potentially even used inside links and such,
-                       // it needs to be consistent for all visitors.
+                       # The cryptic '' timezone parameter tells to use the site-default
+                       # timezone offset instead of the user settings.
+                       #
+                       # Since this value will be saved into the parser cache, served
+                       # to other users, and potentially even used inside links and such,
+                       # it needs to be consistent for all visitors.
                        $this->mRevisionTimestamp = $wgContLang->userAdjust( $timestamp, '' );
 
                        wfProfileOut( __METHOD__ );
@@ -4635,11 +4983,13 @@ class Parser
 
        /**
         * Get the name of the user that edited the last revision
+        *
+        * @return String: user name
         */
        function getRevisionUser() {
-               // if this template is subst: the revision id will be blank,
-               // so just use the current user's name
-               if( $this->mRevisionId ) {
+               # if this template is subst: the revision id will be blank,
+               # so just use the current user's name
+               if ( $this->mRevisionId ) {
                        $revision = Revision::newFromId( $this->mRevisionId );
                        $revuser = $revision->getUserText();
                } else {
@@ -4656,23 +5006,24 @@ class Parser
         */
        public function setDefaultSort( $sort ) {
                $this->mDefaultSort = $sort;
+               $this->mOutput->setProperty( 'defaultsort', $sort );
        }
 
        /**
         * Accessor for $mDefaultSort
-        * Will use the title/prefixed title if none is set
+        * Will use the empty string if none is set.
+        *
+        * This value is treated as a prefix, so the
+        * empty string is equivalent to sorting by
+        * page name.
         *
         * @return string
         */
        public function getDefaultSort() {
-               global $wgCategoryPrefixedDefaultSortkey;
-               if( $this->mDefaultSort !== false ) {
+               if ( $this->mDefaultSort !== false ) {
                        return $this->mDefaultSort;
-               } elseif ($this->mTitle->getNamespace() == NS_CATEGORY ||
-                       !$wgCategoryPrefixedDefaultSortkey) {
-                       return $this->mTitle->getText();
                } else {
-                       return $this->mTitle->getPrefixedText();
+                       return '';
                }
        }
 
@@ -4694,19 +5045,23 @@ class Parser
        public function guessSectionNameFromWikiText( $text ) {
                # Strip out wikitext links(they break the anchor)
                $text = $this->stripSectionName( $text );
-               $headline = Sanitizer::decodeCharReferences( $text );
-               # strip out HTML
-               $headline = StringUtils::delimiterReplace( '<', '>', '', $headline );
-               $headline = trim( $headline );
-               $sectionanchor = '#' . urlencode( str_replace( ' ', '_', $headline ) );
-               $replacearray = array(
-                       '%3A' => ':',
-                       '%' => '.'
-               );
-               return str_replace(
-                       array_keys( $replacearray ),
-                       array_values( $replacearray ),
-                       $sectionanchor );
+               $text = Sanitizer::normalizeSectionNameWhitespace( $text );
+               return '#' . Sanitizer::escapeId( $text, 'noninitial' );
+       }
+
+       /**
+        * Same as guessSectionNameFromWikiText(), but produces legacy anchors
+        * instead.  For use in redirects, since IE6 interprets Redirect: headers
+        * as something other than UTF-8 (apparently?), resulting in breakage.
+        *
+        * @param $text String: The section name
+        * @return string An anchor
+        */
+       public function guessLegacySectionNameFromWikiText( $text ) {
+               # Strip out wikitext links(they break the anchor)
+               $text = $this->stripSectionName( $text );
+               $text = Sanitizer::normalizeSectionNameWhitespace( $text );
+               return '#' . Sanitizer::escapeId( $text, array( 'noninitial', 'legacy' ) );
        }
 
        /**
@@ -4719,42 +5074,38 @@ class Parser
         * to create valid section anchors by mimicing the output of the
         * parser when headings are parsed.
         *
-        * @param $text string Text string to be stripped of wikitext
+        * @param $text String: text string to be stripped of wikitext
         * for use in a Section anchor
         * @return Filtered text string
         */
        public function stripSectionName( $text ) {
                # Strip internal link markup
-               $text = preg_replace('/\[\[:?([^[|]+)\|([^[]+)\]\]/','$2',$text);
-               $text = preg_replace('/\[\[:?([^[]+)\|?\]\]/','$1',$text);
+               $text = preg_replace( '/\[\[:?([^[|]+)\|([^[]+)\]\]/', '$2', $text );
+               $text = preg_replace( '/\[\[:?([^[]+)\|?\]\]/', '$1', $text );
 
                # Strip external link markup (FIXME: Not Tolerant to blank link text
                # I.E. [http://www.mediawiki.org] will render as [1] or something depending
                # on how many empty links there are on the page - need to figure that out.
-               $text = preg_replace('/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/','$2',$text);
+               $text = preg_replace( '/\[(?:' . wfUrlProtocols() . ')([^ ]+?) ([^[]+)\]/', '$2', $text );
 
                # Parse wikitext quotes (italics & bold)
-               $text = $this->doQuotes($text);
+               $text = $this->doQuotes( $text );
 
                # Strip HTML tags
                $text = StringUtils::delimiterReplace( '<', '>', '', $text );
                return $text;
        }
 
-       function srvus( $text ) {
-               return $this->testSrvus( $text, $this->mOutputType );
-       }
-
        /**
         * strip/replaceVariables/unstrip for preprocessor regression testing
         */
-       function testSrvus( $text, $title, $options, $outputType = self::OT_HTML ) {
+       function testSrvus( $text, $title, ParserOptions $options, $outputType = self::OT_HTML ) {
+               $this->mOptions = $options;
                $this->clearState();
-               if ( ! ( $title instanceof Title ) ) {
+               if ( !$title instanceof Title ) {
                        $title = Title::newFromText( $title );
                }
                $this->mTitle = $title;
-               $this->mOptions = $options;
                $this->setOutputType( $outputType );
                $text = $this->replaceVariables( $text );
                $text = $this->mStripState->unstripBoth( $text );
@@ -4764,14 +5115,14 @@ class Parser
 
        function testPst( $text, $title, $options ) {
                global $wgUser;
-               if ( ! ( $title instanceof Title ) ) {
+               if ( !$title instanceof Title ) {
                        $title = Title::newFromText( $title );
                }
                return $this->preSaveTransform( $text, $title, $wgUser, $options );
        }
 
        function testPreprocess( $text, $title, $options ) {
-               if ( ! ( $title instanceof Title ) ) {
+               if ( !$title instanceof Title ) {
                        $title = Title::newFromText( $title );
                }
                return $this->testSrvus( $text, $title, $options, self::OT_PREPROCESS );
@@ -4805,11 +5156,13 @@ class Parser
                $data = array();
                $data['text'] = $text;
 
-               // First, find all strip markers, and store their
-               //  data in an array.
+               # First, find all strip markers, and store their
+               #  data in an array.
                $stripState = new StripState;
                $pos = 0;
-               while( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) ) && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) ) {
+               while ( ( $start_pos = strpos( $text, $this->mUniqPrefix, $pos ) )
+                       && ( $end_pos = strpos( $text, self::MARKER_SUFFIX, $pos ) ) )
+               {
                        $end_pos += strlen( self::MARKER_SUFFIX );
                        $marker = substr( $text, $start_pos, $end_pos-$start_pos );
 
@@ -4828,17 +5181,17 @@ class Parser
                }
                $data['stripstate'] = $stripState;
 
-               // Now, find all of our links, and store THEIR
-               //  data in an array! :)
+               # Now, find all of our links, and store THEIR
+               #  data in an array! :)
                $links = array( 'internal' => array(), 'interwiki' => array() );
                $pos = 0;
 
-               // Internal links
-               while( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
+               # Internal links
+               while ( ( $start_pos = strpos( $text, '<!--LINK ', $pos ) ) ) {
                        list( $ns, $trail ) = explode( ':', substr( $text, $start_pos + strlen( '<!--LINK ' ) ), 2 );
 
-                       $ns = trim($ns);
-                       if (empty( $links['internal'][$ns] )) {
+                       $ns = trim( $ns );
+                       if ( empty( $links['internal'][$ns] ) ) {
                                $links['internal'][$ns] = array();
                        }
 
@@ -4849,35 +5202,52 @@ class Parser
 
                $pos = 0;
 
-               // Interwiki links
-               while( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
+               # Interwiki links
+               while ( ( $start_pos = strpos( $text, '<!--IWLINK ', $pos ) ) ) {
                        $data = substr( $text, $start_pos );
                        $key = trim( substr( $data, 0, strpos( $data, '-->' ) ) );
                        $links['interwiki'][] = $this->mLinkHolders->interwiki[$key];
                        $pos = $start_pos + strlen( "<!--IWLINK $key-->" );
                }
-               
+
                $data['linkholder'] = $links;
 
                return $data;
        }
 
-       function unserialiseHalfParsedText( $data, $intPrefix = null /* Unique identifying prefix */ ) {
-               if (!$intPrefix)
-                       $intPrefix = $this->getRandomString();
-               
-               // First, extract the strip state.
+       /**
+        * Remove any strip markers found in the given text.
+        *
+        * @param $text Input string
+        * @return string
+        */
+       function killMarkers( $text ) {
+               return preg_replace( "/{$this->mUniqPrefix}[^\x7f]+" . self::MARKER_SUFFIX . '/', '', $text );
+       }
+
+       /**
+        * TODO: document
+        * @param $data Array
+        * @param $intPrefix String unique identifying prefix
+        * @return String
+        */
+       function unserialiseHalfParsedText( $data, $intPrefix = null ) {
+               if ( !$intPrefix ) {
+                       $intPrefix = self::getRandomString();
+               }
+
+               # First, extract the strip state.
                $stripState = $data['stripstate'];
                $this->mStripState->general->merge( $stripState->general );
                $this->mStripState->nowiki->merge( $stripState->nowiki );
 
-               // Now, extract the text, and renumber links
+               # Now, extract the text, and renumber links
                $text = $data['text'];
                $links = $data['linkholder'];
 
-               // Internal...
-               foreach( $links['internal'] as $ns => $nsLinks ) {
-                       foreach( $nsLinks as $key => $entry ) {
+               # Internal...
+               foreach ( $links['internal'] as $ns => $nsLinks ) {
+                       foreach ( $nsLinks as $key => $entry ) {
                                $newKey = $intPrefix . '-' . $key;
                                $this->mLinkHolders->internals[$ns][$newKey] = $entry;
 
@@ -4885,15 +5255,15 @@ class Parser
                        }
                }
 
-               // Interwiki...
-               foreach( $links['interwiki'] as $key => $entry ) {
+               # Interwiki...
+               foreach ( $links['interwiki'] as $key => $entry ) {
                        $newKey = "$intPrefix-$key";
                        $this->mLinkHolders->interwikis[$newKey] = $entry;
 
                        $text = str_replace( "<!--IWLINK $key-->", "<!--IWLINK $newKey-->", $text );
                }
 
-               // Should be good to go.
+               # Should be good to go.
                return $text;
        }
 }