MediaWiki r40020 - Code Review

Jump to: navigation, search
Repository:MediaWiki
Revision:r40019‎ | r40020 (on ViewVC)‎ | r40021 >
Date:14:37, 26 August 2008
Author:tstarling
Status:old
Tags:
Comment:
* Revert back to my parser pseudo-branch again. Note: if you feel like reverting this, reverting Parser.php alone to r40010 will work just fine.
* Merged replaceFreeExternalLinks() with doMagicLinks(). Makes a lot of sense, very similar operations, doesn't break any parser tests. Stops free links from interacting with other parser stages, the same way ISBN links don't.
* The pass order change fixes Brion's complaint in r39980. Early link expansion, triggered by having more than 1000 links in the page, was outputting URLs which were destroyed by RFEL. Added parser test.
* Fixed an unrelated bug in LinkHolderArray::replace(): if a link to a redirect appears in two separate RLH calls, the second and subsequent calls do not add the mw-redirect class. Caused by an unmigrated LinkCache fetch.
* Added a parser test for a pass interaction bug that the pass order change fixes.
* The fuzzer told me to tell you that free external links in non-caption image parameters, which are and have always been invisible, are now not registered either.
* Miscellaneous supporting updates to the test infrastructure.
Modified paths:

Diff [purge]

Index: trunk/phase3/maintenance/parserTests.inc
===================================================================
--- trunk/phase3/maintenance/parserTests.inc	(revision 40019)
+++ trunk/phase3/maintenance/parserTests.inc	(revision 40020)
@@ -26,7 +26,7 @@
 
 /** */
 $options = array( 'quick', 'color', 'quiet', 'help', 'show-output', 'record' );
-$optionsWithArgs = array( 'regex' );
+$optionsWithArgs = array( 'regex', 'seed' );
 
 require_once( 'commandLine.inc' );
 require_once( "$IP/maintenance/parserTestsParserHook.php" );
@@ -62,6 +62,10 @@
 	 */
 	private $oldTablePrefix;
 
+	private $maxFuzzTestLength = 300;
+	private $fuzzSeed = 0;
+	private $memoryLimit = 50;
+
 	/**
 	 * Sets terminal colorization and diff/quick modes depending on OS and
 	 * command-line options (--color and --quick).
@@ -117,6 +121,10 @@
 		}
 		$this->keepUploads = isset( $options['keep-uploads'] );
 
+		if ( isset( $options['seed'] ) ) {
+			$this->fuzzSeed = intval( $options['seed'] ) - 1;
+		}
+
 		$this->hooks = array();
 		$this->functionHooks = array();
 	}
@@ -134,6 +142,119 @@
 	}
 
 	/**
+	 * Run a fuzz test series
+	 * Draw input from a set of test files
+	 */
+	function fuzzTest( $filenames ) {
+		$dict = $this->getFuzzInput( $filenames );
+		$dictSize = strlen( $dict );
+		$logMaxLength = log( $this->maxFuzzTestLength );
+		$this->setupDatabase();
+		ini_set( 'memory_limit', $this->memoryLimit * 1048576 );
+
+		$numTotal = 0;
+		$numSuccess = 0;
+		$user = new User;
+		$opts = ParserOptions::newFromUser( $user );
+		$title = Title::makeTitle( NS_MAIN, 'Parser_test' );
+
+		while ( true ) {
+			// Generate test input
+			mt_srand( ++$this->fuzzSeed );
+			$totalLength = mt_rand( 1, $this->maxFuzzTestLength );
+			$input = '';
+			while ( strlen( $input ) < $totalLength ) {
+				$logHairLength = mt_rand( 0, 1000000 ) / 1000000 * $logMaxLength;
+				$hairLength = min( intval( exp( $logHairLength ) ), $dictSize );
+				$offset = mt_rand( 0, $dictSize - $hairLength );
+				$input .= substr( $dict, $offset, $hairLength );
+			}
+
+			$this->setupGlobals();
+			$parser = $this->getParser();
+			// Run the test
+			try {
+				$parser->parse( $input, $title, $opts );
+				$fail = false;
+			} catch ( Exception $exception ) {
+				$fail = true;
+			}
+
+			if ( $fail ) {
+				echo "Test failed with seed {$this->fuzzSeed}\n";
+				echo "Input:\n";
+				var_dump( $input );
+				echo "\n\n";
+				echo "$exception\n";
+			} else {
+				$numSuccess++;
+			}
+			$numTotal++;
+			$this->teardownGlobals();
+			$parser->__destruct();
+
+			if ( $numTotal % 100 == 0 ) {
+				$usage = intval( memory_get_usage( true ) / $this->memoryLimit / 1048576 * 100 );
+				echo "{$this->fuzzSeed}: $numSuccess/$numTotal (mem: $usage%)\n";
+				if ( $usage > 90 ) {
+					echo "Out of memory:\n";
+					$memStats = $this->getMemoryBreakdown();
+					foreach ( $memStats as $name => $usage ) {
+						echo "$name: $usage\n";
+					}
+					$this->abort();
+				}
+			}
+		}
+	}
+
+	/**
+	 * Get an input dictionary from a set of parser test files
+	 */
+	function getFuzzInput( $filenames ) {
+		$dict = '';
+		foreach( $filenames as $filename ) {
+			$contents = file_get_contents( $filename );
+			preg_match_all( '/!!\s*input\n(.*?)\n!!\s*result/s', $contents, $matches );
+			foreach ( $matches[1] as $match ) {
+				$dict .= $match . "\n";
+			}
+		}
+		return $dict;
+	}
+
+	/**
+	 * Get a memory usage breakdown
+	 */
+	function getMemoryBreakdown() {
+		$memStats = array();
+		foreach ( $GLOBALS as $name => $value ) {
+			$memStats['$'.$name] = strlen( serialize( $value ) );
+		}
+		$classes = get_declared_classes();
+		foreach ( $classes as $class ) {
+			$rc = new ReflectionClass( $class );
+			$props = $rc->getStaticProperties();
+			$memStats[$class] = strlen( serialize( $props ) );
+			$methods = $rc->getMethods();
+			foreach ( $methods as $method ) {
+				$memStats[$class] += strlen( serialize( $method->getStaticVariables() ) );
+			}
+		}
+		$functions = get_defined_functions();
+		foreach ( $functions['user'] as $function ) {
+			$rf = new ReflectionFunction( $function );
+			$memStats["$function()"] = strlen( serialize( $rf->getStaticVariables() ) );
+		}
+		asort( $memStats );
+		return $memStats;
+	}
+
+	function abort() {
+		$this->abort();
+	}
+
+	/**
 	 * Run a series of tests listed in the given text files.
 	 * Each test consists of a brief description, wikitext input,
 	 * and the expected HTML output.
@@ -267,6 +388,24 @@
 	}
 
 	/**
+	 * Get a Parser object
+	 */
+	function getParser() {
+		global $wgParserConf;
+		$class = $wgParserConf['class'];
+		$parser = new $class( $wgParserConf );
+		foreach( $this->hooks as $tag => $callback ) {
+			$parser->setHook( $tag, $callback );
+		}
+		foreach( $this->functionHooks as $tag => $bits ) {
+			list( $callback, $flags ) = $bits;
+			$parser->setFunctionHook( $tag, $callback, $flags );
+		}
+		wfRunHooks( 'ParserTestParser', array( &$parser ) );
+		return $parser;
+	}
+
+	/**
 	 * Run a given wikitext input through a freshly-constructed wiki parser,
 	 * and compare the output against the expected results.
 	 * Prints status and explanatory messages to stdout.
@@ -276,7 +415,6 @@
 	 * @return bool
 	 */
 	private function runTest( $desc, $input, $result, $opts ) {
-		global $wgParserConf;
 		if( $this->showProgress ) {
 			$this->showTesting( $desc );
 		}
@@ -300,18 +438,7 @@
 		}
 
 		$noxml = (bool)preg_match( '~\\b noxml \\b~x', $opts );
-
-		$class = $wgParserConf['class'];
-		$parser = new $class( $wgParserConf );
-		foreach( $this->hooks as $tag => $callback ) {
-			$parser->setHook( $tag, $callback );
-		}
-		foreach( $this->functionHooks as $tag => $bits ) {
-			list( $callback, $flags ) = $bits;
-			$parser->setFunctionHook( $tag, $callback, $flags );
-		}
-		wfRunHooks( 'ParserTestParser', array( &$parser ) );
-
+		$parser = $this->getParser();
 		$title =& Title::makeTitle( NS_MAIN, $titleText );
 
 		$matches = array();
@@ -387,6 +514,8 @@
 			self::getOptionValue( '/variant=([a-z]+(?:-[a-z]+)?)/', $opts, false );
 		$maxtoclevel =
 			self::getOptionValue( '/wgMaxTocLevel=(\d+)/', $opts, 999 );
+		$linkHolderBatchSize = 
+			self::getOptionValue( '/wgLinkHolderBatchSize=(\d+)/', $opts, 1000 );
 
 		$settings = array(
 			'wgServer' => 'http://localhost',
@@ -432,6 +561,7 @@
 			) ),
 			'wgDefaultExternalStore' => array(),
 			'wgForeignFileRepos' => array(),
+			'wgLinkHolderBatchSize' => $linkHolderBatchSize,
 			);
 		$this->savedGlobals = array();
 		foreach( $settings as $var => $val ) {
@@ -441,6 +571,7 @@
 		$langObj = Language::factory( $lang );
 		$GLOBALS['wgLang'] = $langObj;
 		$GLOBALS['wgContLang'] = $langObj;
+		$GLOBALS['wgMemc'] = new FakeMemCachedClient;
 
 		//$GLOBALS['wgMessageCache'] = new MessageCache( new BagOStuff(), false, 0, $GLOBALS['wgDBname'] );
 
@@ -551,10 +682,10 @@
 		# Hack: insert a few Wikipedia in-project interwiki prefixes,
 		# for testing inter-language links
 		$db->insert( 'interwiki', array(
-			array( 'iw_prefix' => 'Wikipedia',
+			array( 'iw_prefix' => 'wikipedia',
 				   'iw_url'    => 'http://en.wikipedia.org/wiki/$1',
 				   'iw_local'  => 0 ),
-			array( 'iw_prefix' => 'MeatBall',
+			array( 'iw_prefix' => 'meatball',
 				   'iw_url'    => 'http://www.usemod.com/cgi-bin/mb.pl?$1',
 				   'iw_local'  => 0 ),
 			array( 'iw_prefix' => 'zh',
@@ -621,11 +752,12 @@
 			return;
 		}
 
+		/*
 		$tables = $this->listTables();
 		$db = wfGetDB( DB_MASTER );
 		foreach ( $tables as $table ) {
 			$db->query( "DROP TABLE `parsertest_$table`" );
-		}
+		}*/
 	}
 	
 	/**
@@ -645,6 +777,10 @@
 		}
 
 		wfDebug( "Creating upload directory $dir\n" );
+		if ( file_exists( $dir ) ) {
+			wfDebug( "Already exists!\n" );
+			return $dir;
+		}
 		mkdir( $dir );
 		mkdir( $dir . '/3' );
 		mkdir( $dir . '/3/3a' );
@@ -658,6 +794,8 @@
 	 */
 	private function teardownGlobals() {
 		RepoGroup::destroySingleton();
+		LinkCache::singleton()->clear();
+		$GLOBALS['wgLang']->__destruct();
 		foreach( $this->savedGlobals as $var => $val ) {
 			$GLOBALS[$var] = $val;
 		}
Index: trunk/phase3/maintenance/parserTests.php
===================================================================
--- trunk/phase3/maintenance/parserTests.php	(revision 40019)
+++ trunk/phase3/maintenance/parserTests.php	(revision 40020)
@@ -28,22 +28,21 @@
 if( isset( $options['help'] ) ) {
     echo <<<ENDS
 MediaWiki $wgVersion parser test suite
-Usage: php parserTests.php [--quick] [--quiet] [--show-output]
-                           [--color[=(yes|no)]]
-                           [--regex=<expression>] [--file=<testfile>]
-                           [--record] [--compare]
-                           [--help]
+Usage: php parserTests.php [options...]
+
 Options:
   --quick          Suppress diff output of failed tests
   --quiet          Suppress notification of passed tests (shows only failed tests)
   --show-output    Show expected and actual output
-  --color          Override terminal detection and force color output on or off
+  --color[=yes|no] Override terminal detection and force color output on or off
                    use wgCommandLineDarkBg = true; if your term is dark 
   --regex          Only run tests whose descriptions which match given regex
-  --file           Run test cases from a custom file instead of parserTests.txt
+  --file=<testfile> Run test cases from a custom file instead of parserTests.txt
   --record         Record tests in database
   --compare        Compare with recorded results, without updating the database.
   --keep-uploads   Re-use the same upload directory for each test, don't delete it
+  --fuzz           Do a fuzz test instead of a normal test
+  --seed <n>       Start the fuzz test from the specified seed
   --help           Show this help message
 
 
@@ -67,7 +66,10 @@
 # Print out software version to assist with locating regressions
 $version = SpecialVersion::getVersion();
 echo( "This is MediaWiki version {$version}.\n\n" );
-$ok = $tester->runTestsFromFiles( $files );
 
-exit ($ok ? 0 : -1);
-
+if ( isset( $options['fuzz'] ) ) {
+	$tester->fuzzTest( $files );
+} else {
+	$ok = $tester->runTestsFromFiles( $files );
+	exit ($ok ? 0 : -1);
+}
Index: trunk/phase3/maintenance/parserTests.txt
===================================================================
--- trunk/phase3/maintenance/parserTests.txt	(revision 40019)
+++ trunk/phase3/maintenance/parserTests.txt	(revision 40020)
@@ -7066,7 +7066,30 @@
 
 !! end
 
+!! test
+Interwiki links trounced by replaceExternalLinks after early LinkHolderArray expansion
+!! options
+wgLinkHolderBatchSize=0
+!! input
+[[meatball:1]]
+[[meatball:2]]
+[[meatball:3]]
+!! result
+<p><a href="http://www.usemod.com/cgi-bin/mb.pl?1" class="extiw" title="meatball:1">meatball:1</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?2" class="extiw" title="meatball:2">meatball:2</a>
+<a href="http://www.usemod.com/cgi-bin/mb.pl?3" class="extiw" title="meatball:3">meatball:3</a>
+</p>
+!! end
 
+!! test
+Free external link invading image caption
+!! input
+[[Image:Foobar.jpg|thumb|http://x|hello]]
+!! result
+<div class="thumb tright"><div class="thumbinner" style="width:182px;"><a href="/wiki/Image:Foobar.jpg" class="image" title="hello"><img alt="hello" src="http://example.com/images/thumb/3/3a/Foobar.jpg/180px-Foobar.jpg" width="180" height="20" border="0" class="thumbimage" /></a>  <div class="thumbcaption"><div class="magnify"><a href="/wiki/Image:Foobar.jpg" class="internal" title="Enlarge"><img src="/skins/common/images/magnify-clip.png" width="15" height="11" alt="" /></a></div>hello</div></div></div>
+
+!! end
+
 #
 #
 #
Index: trunk/phase3/maintenance/parserTestsStaticParserHook.php
===================================================================
--- trunk/phase3/maintenance/parserTestsStaticParserHook.php	(revision 40019)
+++ trunk/phase3/maintenance/parserTestsStaticParserHook.php	(revision 40020)
@@ -21,24 +21,27 @@
 	return true;
 }
 
-function wfParserTestStaticParserHookHook( $in, $argv ) {
-	static $buf = null;
-	
+function wfParserTestStaticParserHookHook( $in, $argv, $parser ) {
 	if ( ! count( $argv ) ) {
-		$buf = $in;
+		$parser->static_tag_buf = $in;
 		return '';
-	} else if ( count( $argv ) === 1 && $argv['action'] === 'flush' && $in === null ) {
+	} else if ( count( $argv ) === 1 && isset( $argv['action'] ) 
+		&& $argv['action'] === 'flush' && $in === null ) 
+	{
 		// Clear the buffer, we probably don't need to
-		$tmp = $buf;
-		$buf = null;
+		if ( isset( $parser->static_tag_buf ) ) {
+			$tmp = $parser->static_tag_buf;
+		} else {
+			$tmp = '';
+		}
+		$parser->static_tag_buf = null;
 		return $tmp;
 	} else
 		// wtf?
-		die(
+		return
 			"\nCall this extension as <statictag>string</statictag> or as" .
 			" <statictag action=flush/>, not in any other way.\n" .
 			"text: " . var_export( $in, true ) . "\n" .
-			"argv: " . var_export( $argv, true ) . "\n"
-		);
+			"argv: " . var_export( $argv, true ) . "\n";
 }
 
Index: trunk/phase3/includes/parser/LinkHolderArray.php
===================================================================
--- trunk/phase3/includes/parser/LinkHolderArray.php	(revision 40019)
+++ trunk/phase3/includes/parser/LinkHolderArray.php	(revision 40020)
@@ -1,8 +1,6 @@
 <?php
 
 class LinkHolderArray {
-	var $batchSize = 1000;
-
 	var $internals = array(), $interwikis = array();
 	var $size = 0;
 	var $parent;
@@ -12,6 +10,15 @@
 	}
 
 	/**
+	 * Reduce memory usage to reduce the impact of circular references
+	 */
+	function __destruct() {
+		foreach ( $this as $name => $value ) {
+			unset( $this->$name );
+		}
+	}
+
+	/**
 	 * Merge another LinkHolderArray into this one
 	 */
 	function merge( $other ) {
@@ -30,7 +37,8 @@
 	 * Returns true if the memory requirements of this object are getting large
 	 */
 	function isBig() {
-		return $this->size > $this->batchSize;
+		global $wgLinkHolderBatchSize;
+		return $this->size > $wgLinkHolderBatchSize;
 	}
 
 	/**
@@ -145,7 +153,7 @@
 				if ( $title->isAlwaysKnown() ) {
 					$colours[$pdbk] = '';
 				} elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-					$colours[$pdbk] = '';
+					$colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
 					$output->addLink( $title, $id );
 				} elseif ( $linkCache->isBadLink( $pdbk ) ) {
 					$colours[$pdbk] = 'new';
@@ -180,6 +188,9 @@
 				$pdbk = $title->getPrefixedDBkey();
 				$linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
 				$output->addLink( $title, $s->page_id );
+				# FIXME: convoluted data flow
+				# The redirect status and length is passed to getLinkColour via the LinkCache
+				# Use formal parameters instead
 				$colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
 				//add id to the extension todolist
 				$linkcolour_ids[$s->page_id] = $pdbk;
@@ -274,6 +285,9 @@
 							$entry['pdbk'] = $varPdbk;
 
 							// set pdbk and colour
+							# FIXME: convoluted data flow
+							# The redirect status and length is passed to getLinkColour via the LinkCache
+							# Use formal parameters instead
 							$colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
 							$linkcolour_ids[$s->page_id] = $pdbk;
 						}
Index: trunk/phase3/includes/parser/Parser.php
===================================================================
--- trunk/phase3/includes/parser/Parser.php	(revision 40019)
+++ trunk/phase3/includes/parser/Parser.php	(revision 40020)
@@ -92,13 +92,13 @@
 	# Persistent:
 	var $mTagHooks, $mTransparentTagHooks, $mFunctionHooks, $mFunctionSynonyms, $mVariables,
 		$mImageParams, $mImageParamsMagicArray, $mStripList, $mMarkerIndex, $mPreprocessor,
-		$mExtLinkBracketedRegex, $mDefaultStripList, $mVarCache, $mConf;
+		$mExtLinkBracketedRegex, $mUrlProtocols, $mDefaultStripList, $mVarCache, $mConf;
 
 
 	# Cleared with clearState():
 	var $mOutput, $mAutonumber, $mDTopen, $mStripState;
 	var $mIncludeCount, $mArgStack, $mLastSection, $mInPre;
-	var $mInterwikiLinkHolders, $mLinkHolders;
+	var $mLinkHolders, $mLinkID;
 	var $mIncludeSizes, $mPPNodeCount, $mDefaultSort;
 	var $mTplExpandCache; // empty-frame expansion cache
 	var $mTplRedirCache, $mTplDomCache, $mHeadings, $mDoubleUnderscores;
@@ -128,6 +128,7 @@
 		$this->mFunctionHooks = array();
 		$this->mFunctionSynonyms = array( 0 => array(), 1 => array() );
 		$this->mDefaultStripList = $this->mStripList = array( 'nowiki', 'gallery' );
+		$this->mUrlProtocols = wfUrlProtocols();
 		$this->mExtLinkBracketedRegex = '/\[(\b(' . wfUrlProtocols() . ')'.
 			'[^][<>"\\x00-\\x20\\x7F]+) *([^\]\\x0a\\x0d]*?)\]/S';
 		$this->mVarCache = array();
@@ -146,6 +147,9 @@
 	 * Reduce memory usage to reduce the impact of circular references
 	 */
 	function __destruct() {
+		if ( isset( $this->mLinkHolders ) ) {
+			$this->mLinkHolders->__destruct();
+		}
 		foreach ( $this as $name => $value ) {
 			unset( $this->$name );
 		}
@@ -188,17 +192,8 @@
 		$this->mStripState = new StripState;
 		$this->mArgStack = false;
 		$this->mInPre = false;
-		$this->mInterwikiLinkHolders = array(
-			'texts' => array(),
-			'titles' => array()
-		);
-		$this->mLinkHolders = array(
-			'namespaces' => array(),
-			'dbkeys' => array(),
-			'queries' => array(),
-			'texts' => array(),
-			'titles' => array()
-		);
+		$this->mLinkHolders = new LinkHolderArray( $this );
+		$this->mLinkID = 0;
 		$this->mRevisionTimestamp = $this->mRevisionId = null;
 
 		/**
@@ -213,7 +208,7 @@
 		 */
 		#$this->mUniqPrefix = "\x07UNIQ" . Parser::getRandomString();
 		# Changed to \x7f to allow XML double-parsing -- TS
-		$this->mUniqPrefix = "\x7fUNIQ" . Parser::getRandomString();
+		$this->mUniqPrefix = "\x7fUNIQ" . self::getRandomString();
 
 
 		# Clear these on every parse, bug 4549
@@ -303,7 +298,7 @@
 		 */
 
 		global $wgUseTidy, $wgAlwaysUseTidy, $wgContLang;
-		$fname = 'Parser::parse-' . wfGetCaller();
+		$fname = __METHOD__.'-' . wfGetCaller();
 		wfProfileIn( __METHOD__ );
 		wfProfileIn( $fname );
 
@@ -337,7 +332,6 @@
 		);
 		$text = preg_replace( array_keys($fixtags), array_values($fixtags), $text );
 
-		# only once and last
 		$text = $this->doBlockLevels( $text, $linestart );
 
 		$this->replaceLinkHolders( $text );
@@ -357,7 +351,7 @@
 		$uniq_prefix = $this->mUniqPrefix;
 		$matches = array();
 		$elements = array_keys( $this->mTransparentTagHooks );
-		$text = Parser::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
+		$text = self::extractTagsAndParams( $elements, $text, $matches, $uniq_prefix );
 
 		foreach( $matches as $marker => $data ) {
 			list( $element, $content, $params, $tag ) = $data;
@@ -375,7 +369,7 @@
 		$text = Sanitizer::normalizeCharReferences( $text );
 
 		if (($wgUseTidy and $this->mOptions->mTidy) or $wgAlwaysUseTidy) {
-			$text = Parser::tidy($text);
+			$text = self::tidy($text);
 		} else {
 			# attempt to sanitize at least some nesting problems
 			# (bug #2702 and quite a few others)
@@ -480,6 +474,8 @@
 	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++; }
 
 	function getFunctionLang() {
 		global $wgLang, $wgContLang;
@@ -558,7 +554,7 @@
 				$text = $inside;
 				$tail = null;
 			} else {
-				if( $element == '!--' ) {
+				if( $element === '!--' ) {
 					$end = '/(-->)/';
 				} else {
 					$end = "/(<\\/$element\\s*>)/i";
@@ -667,9 +663,9 @@
 ' "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html>'.
 '<head><title>test</title></head><body>'.$text.'</body></html>';
 		if( $wgTidyInternal ) {
-			$correctedtext = Parser::internalTidy( $wrappedtext );
+			$correctedtext = self::internalTidy( $wrappedtext );
 		} else {
-			$correctedtext = Parser::externalTidy( $wrappedtext );
+			$correctedtext = self::externalTidy( $wrappedtext );
 		}
 		if( is_null( $correctedtext ) ) {
 			wfDebug( "Tidy error detected!\n" );
@@ -686,8 +682,7 @@
 	 */
 	function externalTidy( $text ) {
 		global $wgTidyConf, $wgTidyBin, $wgTidyOpts;
-		$fname = 'Parser::externalTidy';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		$cleansource = '';
 		$opts = ' -utf8';
@@ -716,7 +711,7 @@
 			}
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 
 		if( $cleansource == '' && $text != '') {
 			// Some kind of error happened, so we couldn't get the corrected text.
@@ -738,8 +733,7 @@
 	 */
 	function internalTidy( $text ) {
 		global $wgTidyConf, $IP, $wgDebugTidy;
-		$fname = 'Parser::internalTidy';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		$tidy = new tidy;
 		$tidy->parseString( $text, $wgTidyConf, 'utf8' );
@@ -757,7 +751,7 @@
 				"\n-->";
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $cleansource;
 	}
 
@@ -767,34 +761,35 @@
 	 * @private
 	 */
 	function doTableStuff ( $text ) {
-		$fname = 'Parser::doTableStuff';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
-		$lines = explode ( "\n" , $text );
+		$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
-		foreach ( $lines as $key => $line )
-		{
-			$line = trim ( $line );
 
+		foreach ( $lines as $outLine ) {
+			$line = trim( $outLine );
+
 			if( $line == '' ) { // empty line, go to next line
+				$out .= $outLine."\n";
 				continue;
 			}
-			$first_character = $line{0};
+			$first_character = $line[0];
 			$matches = array();
 
-			if ( preg_match( '/^(:*)\{\|(.*)$/' , $line , $matches ) ) {
+			if ( preg_match( '/^(:*)\{\|(.*)$/', $line , $matches ) ) {
 				// 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' );
 
-				$lines[$key] = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
+				$outLine = str_repeat( '<dl><dd>' , $indent_level ) . "<table{$attributes}>";
 				array_push ( $td_history , false );
 				array_push ( $last_tag_history , '' );
 				array_push ( $tr_history , false );
@@ -802,8 +797,9 @@
 				array_push ( $has_opened_tr , false );
 			} else if ( count ( $td_history ) == 0 ) {
 				// Don't do any of the following
+				$out .= $outLine."\n";
 				continue;
-			} else if ( substr ( $line , 0 , 2 ) == '|}' ) {
+			} else if ( substr ( $line , 0 , 2 ) === '|}' ) {
 				// We are ending a table
 				$line = '</table>' . substr ( $line , 2 );
 				$last_tag = array_pop ( $last_tag_history );
@@ -820,8 +816,8 @@
 					$line = "</{$last_tag}>{$line}";
 				}
 				array_pop ( $tr_attributes );
-				$lines[$key] = $line . str_repeat( '</dd></dl>' , $indent_level );
-			} else if ( substr ( $line , 0 , 2 ) == '|-' ) {
+				$outLine = $line . str_repeat( '</dd></dl>' , $indent_level );
+			} else if ( substr ( $line , 0 , 2 ) === '|-' ) {
 				// Now we have a table row
 				$line = preg_replace( '#^\|-+#', '', $line );
 
@@ -844,21 +840,21 @@
 					$line = "</{$last_tag}>{$line}";
 				}
 
-				$lines[$key] = $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 )  == '|+' ) {
+			else if ( $first_character === '|' || $first_character === '!' || substr ( $line , 0 , 2 )  === '|+' ) {
 				// This might be cell elements, td, th or captions
-				if ( substr ( $line , 0 , 2 ) == '|+' ) {
+				if ( substr ( $line , 0 , 2 ) === '|+' ) {
 					$first_character = '+';
 					$line = substr ( $line , 1 );
 				}
 
 				$line = substr ( $line , 1 );
 
-				if ( $first_character == '!' ) {
+				if ( $first_character === '!' ) {
 					$line = str_replace ( '!!' , '||' , $line );
 				}
 
@@ -868,13 +864,13 @@
 				// attribute values containing literal "||".
 				$cells = StringUtils::explodeMarkup( '||' , $line );
 
-				$lines[$key] = '';
+				$outLine = '';
 
 				// Loop through each table cell
 				foreach ( $cells as $cell )
 				{
 					$previous = '';
-					if ( $first_character != '+' )
+					if ( $first_character !== '+' )
 					{
 						$tr_after = array_pop ( $tr_attributes );
 						if ( !array_pop ( $tr_history ) ) {
@@ -892,11 +888,11 @@
 						$previous = "</{$last_tag}>{$previous}";
 					}
 
-					if ( $first_character == '|' ) {
+					if ( $first_character === '|' ) {
 						$last_tag = 'td';
-					} else if ( $first_character == '!' ) {
+					} else if ( $first_character === '!' ) {
 						$last_tag = 'th';
-					} else if ( $first_character == '+' ) {
+					} else if ( $first_character === '+' ) {
 						$last_tag = 'caption';
 					} else {
 						$last_tag = '';
@@ -919,38 +915,42 @@
 						$cell = "{$previous}<{$last_tag}{$attributes}>{$cell_data[1]}";
 					}
 
-					$lines[$key] .= $cell;
+					$outLine .= $cell;
 					array_push ( $td_history , true );
 				}
 			}
+			$out .= $outLine . "\n";
 		}
 
 		// Closing open td, tr && table
 		while ( count ( $td_history ) > 0 )
 		{
 			if ( array_pop ( $td_history ) ) {
-				$lines[] = '</td>' ;
+				$out .= "</td>\n";
 			}
 			if ( array_pop ( $tr_history ) ) {
-				$lines[] = '</tr>' ;
+				$out .= "</tr>\n";
 			}
 			if ( !array_pop ( $has_opened_tr ) ) {
-				$lines[] = "<tr><td></td></tr>" ;
+				$out .= "<tr><td></td></tr>\n" ;
 			}
 
-			$lines[] = '</table>' ;
+			$out .= "</table>\n";
 		}
 
-		$output = implode ( "\n" , $lines ) ;
+		// Remove trailing line-ending (b/c)
+		if ( substr( $out, -1 ) === "\n" ) {
+			$out = substr( $out, 0, -1 );
+		}
 
 		// special case: don't return empty table
-		if( $output == "<table>\n<tr><td></td></tr>\n</table>" ) {
-			$output = '';
+		if( $out === "<table>\n<tr><td></td></tr>\n</table>" ) {
+			$out = '';
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 
-		return $output;
+		return $out;
 	}
 
 	/**
@@ -961,12 +961,11 @@
 	 */
 	function internalParse( $text ) {
 		$isMain = true;
-		$fname = 'Parser::internalParse';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		# Hook to suspend the parser in this state
 		if ( !wfRunHooks( 'ParserBeforeInternalParse', array( &$this, &$text, &$this->mStripState ) ) ) {
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return $text ;
 		}
 
@@ -999,84 +998,146 @@
 		$text = $this->doMagicLinks( $text );
 		$text = $this->formatHeadings( $text, $isMain );
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $text;
 	}
 
 	/**
 	 * Replace special strings like "ISBN xxx" and "RFC xxx" with
 	 * magic external links.
-	 *
+	 * 
+	 * DML
 	 * @private
 	 */
 	function doMagicLinks( $text ) {
 		wfProfileIn( __METHOD__ );
+		$prots = $this->mUrlProtocols;
+		$urlChar = self::EXT_LINK_URL_CLASS;
 		$text = preg_replace_callback(
 			'!(?:                           # Start cases
-			    <a.*?</a> |                 # Skip link text
-			    <.*?> |                     # Skip stuff inside HTML elements
-			    (?:RFC|PMID)\s+([0-9]+) |   # RFC or PMID, capture number as m[1]
-			    ISBN\s+(\b                  # ISBN, capture number as m[2]
-                                     (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
-                                     (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
-                                     [0-9Xx]                 # check digit
-                                   \b)
+				(<a.*?</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
+				ISBN\s+(\b                  # m[5]: ISBN, capture number
+				    (?: 97[89] [\ \-]? )?   # optional 13-digit ISBN prefix
+				    (?: [0-9]  [\ \-]? ){9} # 9 digits with opt. delimiters
+				    [0-9Xx]                 # check digit
+				    \b)
 			)!x', array( &$this, 'magicLinkCallback' ), $text );
 		wfProfileOut( __METHOD__ );
 		return $text;
 	}
 
 	function magicLinkCallback( $m ) {
-		if ( substr( $m[0], 0, 1 ) == '<' ) {
+		if ( isset( $m[1] ) && strval( $m[1] ) !== '' ) {
+			# Skip anchor
+			return $m[0];
+		} elseif ( isset( $m[2] ) && strval( $m[2] ) !== '' ) {
 			# Skip HTML element
 			return $m[0];
-		} elseif ( substr( $m[0], 0, 4 ) == 'ISBN' ) {
-			$isbn = $m[2];
-			$num = strtr( $isbn, array(
-				'-' => '',
-				' ' => '',
-				'x' => 'X',
-			));
-			$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
-			$text = '<a href="' .
-				$titleObj->escapeLocalUrl() .
-				"\" class=\"internal\">ISBN $isbn</a>";
-		} else {
-			if ( substr( $m[0], 0, 3 ) == 'RFC' ) {
+		} elseif ( isset( $m[3] ) && strval( $m[3] ) !== '' ) {
+			# Free external link
+			return $this->makeFreeExternalLink( $m[0] );
+		} elseif ( isset( $m[4] ) && strval( $m[4] ) !== '' ) {
+			# RFC or PMID
+			if ( substr( $m[0], 0, 3 ) === 'RFC' ) {
 				$keyword = 'RFC';
 				$urlmsg = 'rfcurl';
-				$id = $m[1];
-			} elseif ( substr( $m[0], 0, 4 ) == 'PMID' ) {
+				$id = $m[4];
+			} elseif ( substr( $m[0], 0, 4 ) === 'PMID' ) {
 				$keyword = 'PMID';
 				$urlmsg = 'pubmedurl';
-				$id = $m[1];
+				$id = $m[4];
 			} else {
 				throw new MWException( __METHOD__.': unrecognised match type "' .
 					substr($m[0], 0, 20 ) . '"' );
 			}
-
 			$url = wfMsg( $urlmsg, $id);
 			$sk = $this->mOptions->getSkin();
 			$la = $sk->getExternalLinkAttributes( $url, $keyword.$id );
-			$text = "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+			return "<a href=\"{$url}\"{$la}>{$keyword} {$id}</a>";
+		} elseif ( isset( $m[5] ) && strval( $m[5] ) !== '' ) {
+			# ISBN
+			$isbn = $m[5];
+			$num = strtr( $isbn, array(
+				'-' => '',
+				' ' => '',
+				'x' => 'X',
+			));
+			$titleObj = SpecialPage::getTitleFor( 'Booksources', $num );
+			return'<a href="' .
+				$titleObj->escapeLocalUrl() .
+				"\" class=\"internal\">ISBN $isbn</a>";
+		} else {
+			return $m[0];
 		}
-		return $text;
 	}
 
 	/**
+	 * Make a free external link, given a user-supplied URL
+	 * @return HTML
+	 * @private
+	 */
+	function makeFreeExternalLink( $url ) {
+		global $wgContLang;
+		wfProfileIn( __METHOD__ );
+
+		$sk = $this->mOptions->getSkin();
+		$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]);
+		}
+
+		# Move trailing punctuation to $trail
+		$sep = ',;\.:!?';
+		# If there is no left bracket, then consider right brackets fair game too
+		if ( strpos( $url, '(' ) === false ) {
+			$sep .= ')';
+		}
+
+		$numSepChars = strspn( strrev( $url ), $sep );
+		if ( $numSepChars ) {
+			$trail = substr( $url, -$numSepChars ) . $trail;
+			$url = substr( $url, 0, -$numSepChars );
+		}
+
+		$url = Sanitizer::cleanUrl( $url );
+
+		# Is this an external image?
+		$text = $this->maybeMakeExternalImage( $url );
+		if ( $text === false ) {
+			# Not an image, make a link
+			$text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
+			# Register it in the output object...
+			# Replace unnecessary URL escape codes with their equivalent characters
+			$pasteurized = self::replaceUnusualEscapes( $url );
+			$this->mOutput->addExternalLink( $pasteurized );
+		}
+		wfProfileOut( __METHOD__ );
+		return $text . $trail;
+	}
+
+
+	/**
 	 * Parse headers and return html
 	 *
 	 * @private
 	 */
 	function doHeadings( $text ) {
-		$fname = 'Parser::doHeadings';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 		for ( $i = 6; $i >= 1; --$i ) {
 			$h = str_repeat( '=', $i );
 			$text = preg_replace( "/^$h(.+)$h\\s*$/m",
 			  "<h$i>\\1</h$i>", $text );
 		}
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $text;
 	}
 
@@ -1086,15 +1147,14 @@
 	 * @return string the altered text
 	 */
 	function doAllQuotes( $text ) {
-		$fname = 'Parser::doAllQuotes';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 		$outtext = '';
-		$lines = explode( "\n", $text );
+		$lines = StringUtils::explode( "\n", $text );
 		foreach ( $lines as $line ) {
-			$outtext .= $this->doQuotes ( $line ) . "\n";
+			$outtext .= $this->doQuotes( $line ) . "\n";
 		}
 		$outtext = substr($outtext, 0,-1);
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $outtext;
 	}
 
@@ -1156,9 +1216,9 @@
 					{
 						$x1 = substr ($arr[$i-1], -1);
 						$x2 = substr ($arr[$i-1], -2, 1);
-						if ($x1 == ' ') {
+						if ($x1 === ' ') {
 							if ($firstspace == -1) $firstspace = $i;
-						} else if ($x2 == ' ') {
+						} else if ($x2 === ' ') {
 							if ($firstsingleletterword == -1) $firstsingleletterword = $i;
 						} else {
 							if ($firstmultiletterword == -1) $firstmultiletterword = $i;
@@ -1198,7 +1258,7 @@
 			{
 				if (($i % 2) == 0)
 				{
-					if ($state == 'both')
+					if ($state === 'both')
 						$buffer .= $r;
 					else
 						$output .= $r;
@@ -1207,41 +1267,41 @@
 				{
 					if (strlen ($r) == 2)
 					{
-						if ($state == 'i')
+						if ($state === 'i')
 						{ $output .= '</i>'; $state = ''; }
-						else if ($state == 'bi')
+						else if ($state === 'bi')
 						{ $output .= '</i>'; $state = 'b'; }
-						else if ($state == 'ib')
+						else if ($state === 'ib')
 						{ $output .= '</b></i><b>'; $state = 'b'; }
-						else if ($state == 'both')
+						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')
+						if ($state === 'b')
 						{ $output .= '</b>'; $state = ''; }
-						else if ($state == 'bi')
+						else if ($state === 'bi')
 						{ $output .= '</i></b><i>'; $state = 'i'; }
-						else if ($state == 'ib')
+						else if ($state === 'ib')
 						{ $output .= '</b>'; $state = 'i'; }
-						else if ($state == 'both')
+						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')
+						if ($state === 'b')
 						{ $output .= '</b><i>'; $state = 'i'; }
-						else if ($state == 'i')
+						else if ($state === 'i')
 						{ $output .= '</i><b>'; $state = 'b'; }
-						else if ($state == 'bi')
+						else if ($state === 'bi')
 						{ $output .= '</i></b>'; $state = ''; }
-						else if ($state == 'ib')
+						else if ($state === 'ib')
 						{ $output .= '</b></i>'; $state = ''; }
-						else if ($state == 'both')
+						else if ($state === 'both')
 						{ $output .= '<i><b>'.$buffer.'</b></i>'; $state = ''; }
 						else # ($state == '')
 						{ $buffer = ''; $state = 'both'; }
@@ -1250,21 +1310,21 @@
 				$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;
 		}
 	}
 
 	/**
-	 * Replace external links
+	 * Replace external links (REL)
 	 *
  	 * Note: this is all very hackish and the order of execution matters a lot.
 	 * Make sure to run maintenance/parserTests.php if you change this code.
@@ -1273,15 +1333,13 @@
 	 */
 	function replaceExternalLinks( $text ) {
 		global $wgContLang;
-		$fname = 'Parser::replaceExternalLinks';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		$sk = $this->mOptions->getSkin();
 
 		$bits = preg_split( $this->mExtLinkBracketedRegex, $text, -1, PREG_SPLIT_DELIM_CAPTURE );
+		$s = array_shift( $bits );
 
-		$s = $this->replaceFreeExternalLinks( array_shift( $bits ) );
-
 		$i = 0;
 		while ( $i<count( $bits ) ) {
 			$url = $bits[$i++];
@@ -1308,7 +1366,7 @@
 			$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 == '' ) {
@@ -1331,10 +1389,6 @@
 
 			$url = Sanitizer::cleanUrl( $url );
 
-			# Process the trail (i.e. everything after this link up until start of the next link),
-			# replacing any non-bracketed links
-			$trail = $this->replaceFreeExternalLinks( $trail );
-
 			# 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
@@ -1344,96 +1398,15 @@
 			# Register link in the output object.
 			# Replace unnecessary URL escape codes with the referenced character
 			# This prevents spammers from hiding links from the filters
-			$pasteurized = Parser::replaceUnusualEscapes( $url );
+			$pasteurized = self::replaceUnusualEscapes( $url );
 			$this->mOutput->addExternalLink( $pasteurized );
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $s;
 	}
 
 	/**
-	 * Replace anything that looks like a URL with a link
-	 * @private
-	 */
-	function replaceFreeExternalLinks( $text ) {
-		global $wgContLang;
-		$fname = 'Parser::replaceFreeExternalLinks';
-		wfProfileIn( $fname );
-
-		$bits = preg_split( '/(\b(?:' . wfUrlProtocols() . '))/S', $text, -1, PREG_SPLIT_DELIM_CAPTURE );
-		$s = array_shift( $bits );
-		$i = 0;
-
-		$sk = $this->mOptions->getSkin();
-
-		while ( $i < count( $bits ) ){
-			$protocol = $bits[$i++];
-			$remainder = $bits[$i++];
-
-			$m = array();
-			if ( preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $remainder, $m ) ) {
-				# Found some characters after the protocol that look promising
-				$url = $protocol . $m[1];
-				$trail = $m[2];
-
-				# special case: handle urls as url args:
-				# http://www.example.com/foo?=http://www.example.com/bar
-				if(strlen($trail) == 0 &&
-					isset($bits[$i]) &&
-					preg_match('/^'. wfUrlProtocols() . '$/S', $bits[$i]) &&
-					preg_match( '/^('.self::EXT_LINK_URL_CLASS.'+)(.*)$/s', $bits[$i + 1], $m ))
-				{
-					# add protocol, arg
-					$url .= $bits[$i] . $m[1]; # protocol, url as arg to previous link
-					$i += 2;
-					$trail = $m[2];
-				}
-
-				# 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]);
-				}
-
-				# Move trailing punctuation to $trail
-				$sep = ',;\.:!?';
-				# If there is no left bracket, then consider right brackets fair game too
-				if ( strpos( $url, '(' ) === false ) {
-					$sep .= ')';
-				}
-
-				$numSepChars = strspn( strrev( $url ), $sep );
-				if ( $numSepChars ) {
-					$trail = substr( $url, -$numSepChars ) . $trail;
-					$url = substr( $url, 0, -$numSepChars );
-				}
-
-				$url = Sanitizer::cleanUrl( $url );
-
-				# Is this an external image?
-				$text = $this->maybeMakeExternalImage( $url );
-				if ( $text === false ) {
-					# Not an image, make a link
-					$text = $sk->makeExternalLink( $url, $wgContLang->markNoConversion($url), true, 'free', $this->mTitle->getNamespace() );
-					# Register it in the output object...
-					# Replace unnecessary URL escape codes with their equivalent characters
-					$pasteurized = Parser::replaceUnusualEscapes( $url );
-					$this->mOutput->addExternalLink( $pasteurized );
-				}
-				$s .= $text . $trail;
-			} else {
-				$s .= $protocol . $remainder;
-			}
-		}
-		wfProfileOut( $fname );
-		return $s;
-	}
-
-	/**
 	 * Replace unusual URL escape codes with their equivalent characters
 	 * @param string
 	 * @return string
@@ -1445,7 +1418,7 @@
 	 */
 	static function replaceUnusualEscapes( $url ) {
 		return preg_replace_callback( '/%[0-9A-Fa-f]{2}/',
-			array( 'Parser', 'replaceUnusualEscapesCallback' ), $url );
+			array( __CLASS__, 'replaceUnusualEscapesCallback' ), $url );
 	}
 
 	/**
@@ -1489,35 +1462,48 @@
 
 	/**
 	 * Process [[ ]] wikilinks
+	 * @return processed text
 	 *
 	 * @private
 	 */
 	function replaceInternalLinks( $s ) {
+		$this->mLinkHolders->merge( $this->replaceInternalLinks2( $s ) );
+		return $s;
+	}
+
+	/**
+	 * Process [[ ]] wikilinks (RIL)
+	 * @return LinkHolderArray
+	 *
+	 * @private
+	 */
+	function replaceInternalLinks2( &$s ) {
 		global $wgContLang;
-		static $fname = 'Parser::replaceInternalLinks' ;
 
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
-		wfProfileIn( $fname.'-setup' );
-		static $tc = FALSE;
+		wfProfileIn( __METHOD__.'-setup' );
+		static $tc = FALSE, $e1, $e1_img;
 		# the % is needed to support urlencoded titles as well
-		if ( !$tc ) { $tc = Title::legalChars() . '#%'; }
+		if ( !$tc ) { 
+			$tc = Title::legalChars() . '#%';
+			# Match a link having the form [[namespace:link|alternate]]trail
+			$e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD";
+			# Match cases where there is no "]]", which might still be images
+			$e1_img = "/^([{$tc}]+)\\|(.*)\$/sD";
+		}
 
 		$sk = $this->mOptions->getSkin();
+		$holders = new LinkHolderArray( $this );
 
 		#split the entire text string on occurences of [[
-		$a = explode( '[[', ' ' . $s );
+		$a = StringUtils::explode( '[[', ' ' . $s );
 		#get the first element (all text up to first [[), and remove the space we added
-		$s = array_shift( $a );
+		$s = $a->current();
+		$a->next();
+		$line = $a->current(); # Workaround for broken ArrayIterator::next() that returns "void"
 		$s = substr( $s, 1 );
 
-		# Match a link having the form [[namespace:link|alternate]]trail
-		static $e1 = FALSE;
-		if ( !$e1 ) { $e1 = "/^([{$tc}]+)(?:\\|(.+?))?]](.*)\$/sD"; }
-		# Match cases where there is no "]]", which might still be images
-		static $e1_img = FALSE;
-		if ( !$e1_img ) { $e1_img = "/^([{$tc}]+)\\|(.*)\$/sD"; }
-
 		$useLinkPrefixExtension = $wgContLang->linkPrefixExtension();
 		$e2 = null;
 		if ( $useLinkPrefixExtension ) {
@@ -1527,8 +1513,8 @@
 		}
 
 		if( is_null( $this->mTitle ) ) {
-			wfProfileOut( $fname );
-			wfProfileOut( $fname.'-setup' );
+			wfProfileOut( __METHOD__.'-setup' );
+			wfProfileOut( __METHOD__ );
 			throw new MWException( __METHOD__.": \$this->mTitle is null\n" );
 		}
 		$nottalk = !$this->mTitle->isTalkPage();
@@ -1550,13 +1536,20 @@
 			$selflink = array($this->mTitle->getPrefixedText());
 		}
 		$useSubpages = $this->areSubpagesAllowed();
-		wfProfileOut( $fname.'-setup' );
+		wfProfileOut( __METHOD__.'-setup' );
 
 		# Loop for each link
-		for ($k = 0; isset( $a[$k] ); $k++) {
-			$line = $a[$k];
+		for ( ; $line !== false && $line !== null ; $a->next(), $line = $a->current() ) {
+			# Check for excessive memory usage
+			if ( $holders->isBig() ) {
+				# Too big
+				# Do the existence check, replace the link holders and clear the array
+				$holders->replace( $s );
+				$holders->clear();
+			}
+
 			if ( $useLinkPrefixExtension ) {
-				wfProfileIn( $fname.'-prefixhandling' );
+				wfProfileIn( __METHOD__.'-prefixhandling' );
 				if ( preg_match( $e2, $s, $m ) ) {
 					$prefix = $m[2];
 					$s = $m[1];
@@ -1568,12 +1561,12 @@
 					$prefix = $first_prefix;
 					$first_prefix = false;
 				}
-				wfProfileOut( $fname.'-prefixhandling' );
+				wfProfileOut( __METHOD__.'-prefixhandling' );
 			}
 
 			$might_be_img = false;
 
-			wfProfileIn( "$fname-e1" );
+			wfProfileIn( __METHOD__."-e1" );
 			if ( preg_match( $e1, $line, $m ) ) { # page with normal text or alt
 				$text = $m[2];
 				# If we get a ] at the beginning of $m[3] that means we have a link that's something like:
@@ -1607,18 +1600,18 @@
 				$trail = "";
 			} else { # Invalid form; output directly
 				$s .= $prefix . '[[' . $line ;
-				wfProfileOut( "$fname-e1" );
+				wfProfileOut( __METHOD__."-e1" );
 				continue;
 			}
-			wfProfileOut( "$fname-e1" );
-			wfProfileIn( "$fname-misc" );
+			wfProfileOut( __METHOD__."-e1" );
+			wfProfileIn( __METHOD__."-misc" );
 
 			# 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])) {
 				$s .= $prefix . '[[' . $line ;
-				wfProfileOut( "$fname-misc" );
+				wfProfileOut( __METHOD__."-misc" );
 				continue;
 			}
 
@@ -1629,33 +1622,36 @@
 				$link = $m[1];
 			}
 
-			$noforce = (substr($m[1], 0, 1) != ':');
+			$noforce = (substr($m[1], 0, 1) !== ':');
 			if (!$noforce) {
 				# Strip off leading ':'
 				$link = substr($link, 1);
 			}
 
-			wfProfileOut( "$fname-misc" );
-			wfProfileIn( "$fname-title" );
+			wfProfileOut( __METHOD__."-misc" );
+			wfProfileIn( __METHOD__."-title" );
 			$nt = Title::newFromText( $this->mStripState->unstripNoWiki($link) );
 			if( !$nt ) {
 				$s .= $prefix . '[[' . $line;
-				wfProfileOut( "$fname-title" );
+				wfProfileOut( __METHOD__."-title" );
 				continue;
 			}
 
 			$ns = $nt->getNamespace();
 			$iw = $nt->getInterWiki();
-			wfProfileOut( "$fname-title" );
+			wfProfileOut( __METHOD__."-title" );
 
 			if ($might_be_img) { # if this is actually an invalid link
-				wfProfileIn( "$fname-might_be_img" );
+				wfProfileIn( __METHOD__."-might_be_img" );
 				if ($ns == NS_IMAGE && $noforce) { #but might be an image
 					$found = false;
-					while (isset ($a[$k+1]) ) {
+					while ( true ) {
 						#look at the next 'line' to see if we can close it there
-						$spliced = array_splice( $a, $k + 1, 1 );
-						$next_line = array_shift( $spliced );
+						$a->next();
+						$next_line = $a->current();
+						if ( $next_line === false || $next_line === null ) {
+							break;
+						}
 						$m = explode( ']]', $next_line, 3 );
 						if ( count( $m ) == 3 ) {
 							# the first ]] closes the inner link, the second the image
@@ -1675,19 +1671,19 @@
 					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
-						$text = $this->replaceInternalLinks($text);
+						$holders->merge( $this->replaceInternalLinks2( $text ) );
 						$s .= "{$prefix}[[$link|$text";
 						# note: no $trail, because without an end, there *is* no trail
-						wfProfileOut( "$fname-might_be_img" );
+						wfProfileOut( __METHOD__."-might_be_img" );
 						continue;
 					}
 				} 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( "$fname-might_be_img" );
+					wfProfileOut( __METHOD__."-might_be_img" );
 					continue;
 				}
-				wfProfileOut( "$fname-might_be_img" );
+				wfProfileOut( __METHOD__."-might_be_img" );
 			}
 
 			$wasblank = ( '' == $text );
@@ -1697,41 +1693,36 @@
 			if( $noforce ) {
 
 				# Interwikis
-				wfProfileIn( "$fname-interwiki" );
+				wfProfileIn( __METHOD__."-interwiki" );
 				if( $iw && $this->mOptions->getInterwikiMagic() && $nottalk && $wgContLang->getLanguageName( $iw ) ) {
 					$this->mOutput->addLanguageLink( $nt->getFullText() );
 					$s = rtrim($s . $prefix);
 					$s .= trim($trail, "\n") == '' ? '': $prefix . $trail;
-					wfProfileOut( "$fname-interwiki" );
+					wfProfileOut( __METHOD__."-interwiki" );
 					continue;
 				}
-				wfProfileOut( "$fname-interwiki" );
+				wfProfileOut( __METHOD__."-interwiki" );
 
 				if ( $ns == NS_IMAGE ) {
-					wfProfileIn( "$fname-image" );
+					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);
-						$text = $this->replaceInternalLinks($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 ) ) . $trail;
-						$this->mOutput->addImage( $nt->getDBkey() );
-
-						wfProfileOut( "$fname-image" );
-						continue;
-					} else {
-						# We still need to record the image's presence on the page
-						$this->mOutput->addImage( $nt->getDBkey() );
+						$s .= $prefix . $this->armorLinks( $this->makeImage( $nt, $text, $holders ) ) . $trail;
 					}
-					wfProfileOut( "$fname-image" );
+					$this->mOutput->addImage( $nt->getDBkey() );
+					wfProfileOut( __METHOD__."-image" );
+					continue;
 
 				}
 
 				if ( $ns == NS_CATEGORY ) {
-					wfProfileIn( "$fname-category" );
+					wfProfileIn( __METHOD__."-category" );
 					$s = rtrim($s . "\n"); # bug 87
 
 					if ( $wasblank ) {
@@ -1750,7 +1741,7 @@
 					 */
 					$s .= trim($prefix . $trail, "\n") == '' ? '': $prefix . $trail;
 
-					wfProfileOut( "$fname-category" );
+					wfProfileOut( __METHOD__."-category" );
 					continue;
 				}
 			}
@@ -1781,7 +1772,7 @@
 				if( SpecialPage::exists( $nt->getDBkey() ) ) {
 					$s .= $this->makeKnownLinkHolder( $nt, $text, '', $trail, $prefix );
 				} else {
-					$s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+					$s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
 				}
 				continue;
 			} elseif( $ns == NS_IMAGE ) {
@@ -1795,10 +1786,10 @@
 					continue;
 				}
 			}
-			$s .= $this->makeLinkHolder( $nt, $text, '', $trail, $prefix );
+			$s .= $holders->makeHolder( $nt, $text, '', $trail, $prefix );
 		}
-		wfProfileOut( $fname );
-		return $s;
+		wfProfileOut( __METHOD__ );
+		return $holders;
 	}
 
 	/**
@@ -1807,32 +1798,10 @@
 	 * parsing of interwiki links, and secondly to allow all existence checks and
 	 * article length checks (for stub links) to be bundled into a single query.
 	 *
+	 * @deprecated
 	 */
 	function makeLinkHolder( &$nt, $text = '', $query = '', $trail = '', $prefix = '' ) {
-		wfProfileIn( __METHOD__ );
-		if ( ! is_object($nt) ) {
-			# Fail gracefully
-			$retVal = "<!-- ERROR -->{$prefix}{$text}{$trail}";
-		} else {
-			# Separate the link trail from the rest of the link
-			list( $inside, $trail ) = Linker::splitTrail( $trail );
-
-			if ( $nt->isExternal() ) {
-				$nr = array_push( $this->mInterwikiLinkHolders['texts'], $prefix.$text.$inside );
-				$this->mInterwikiLinkHolders['titles'][] = $nt;
-				$retVal = '<!--IWLINK '. ($nr-1) ."-->{$trail}";
-			} else {
-				$nr = array_push( $this->mLinkHolders['namespaces'], $nt->getNamespace() );
-				$this->mLinkHolders['dbkeys'][] = $nt->getDBkey();
-				$this->mLinkHolders['queries'][] = $query;
-				$this->mLinkHolders['texts'][] = $prefix.$text.$inside;
-				$this->mLinkHolders['titles'][] = $nt;
-
-				$retVal = '<!--LINK '. ($nr-1) ."-->{$trail}";
-			}
-		}
-		wfProfileOut( __METHOD__ );
-		return $retVal;
+		return $this->mLinkHolders->makeHolder( $nt, $text, $query, $trail, $prefix );
 	}
 
 	/**
@@ -1860,11 +1829,9 @@
 	 * Insert a NOPARSE hacky thing into any inline links in a chunk that's
 	 * going to go through further parsing steps before inline URL expansion.
 	 *
-	 * In particular this is important when using action=render, which causes
-	 * full URLs to be included.
+	 * 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.
 	 *
-	 * Oh man I hate our multi-layer parser!
-	 *
 	 * @param string more-or-less HTML
 	 * @return string less-or-more HTML with NOPARSE bits
 	 */
@@ -1898,8 +1865,7 @@
 		# ../ -- convert to CurrentPage, from CurrentPage/CurrentSubPage
 		# ../Foobar -- convert to CurrentPage/Foobar, from CurrentPage/CurrentSubPage
 
-		$fname = 'Parser::maybeDoSubpageLink';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 		$ret = $target; # default return value is no change
 
 		# Some namespaces don't allow subpages,
@@ -1915,7 +1881,7 @@
 			# bug 7425
 			$target = trim( $target );
 			# Look at the first character
-			if( $target != '' && $target{0} == '/' ) {
+			if( $target != '' && $target{0} === '/' ) {
 				# / at end means we don't want the slash to be shown
 				$m = array();
 				$trailingSlashes = preg_match_all( '%(/+)$%', $target, $m );
@@ -1942,7 +1908,7 @@
 					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 ) == '/' ) {
+						if( substr( $nodotdot, -1, 1 ) === '/' ) {
 							$nodotdot = substr( $nodotdot, 0, -1 );
 							if( '' === $text ) {
 								$text = $nodotdot . $suffix;
@@ -1958,7 +1924,7 @@
 			}
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $ret;
 	}
 
@@ -1994,10 +1960,10 @@
 	/* 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>'; }
+		else if ( '#' === $char ) { $result .= '<ol><li>'; }
+		else if ( ':' === $char ) { $result .= '<dl><dd>'; }
+		else if ( ';' === $char ) {
 			$result .= '<dl><dt>';
 			$this->mDTopen = true;
 		}
@@ -2007,11 +1973,11 @@
 	}
 
 	/* private */ function nextItem( $char ) {
-		if ( '*' == $char || '#' == $char ) { return '</li><li>'; }
-		else if ( ':' == $char || ';' == $char ) {
+		if ( '*' === $char || '#' === $char ) { return '</li><li>'; }
+		else if ( ':' === $char || ';' === $char ) {
 			$close = '</dd>';
 			if ( $this->mDTopen ) { $close = '</dt>'; }
-			if ( ';' == $char ) {
+			if ( ';' === $char ) {
 				$this->mDTopen = true;
 				return $close . '<dt>';
 			} else {
@@ -2023,9 +1989,9 @@
 	}
 
 	/* private */ function closeList( $char ) {
-		if ( '*' == $char ) { $text = '</li></ul>'; }
-		else if ( '#' == $char ) { $text = '</li></ol>'; }
-		else if ( ':' == $char ) {
+		if ( '*' === $char ) { $text = '</li></ul>'; }
+		else if ( '#' === $char ) { $text = '</li></ol>'; }
+		else if ( ':' === $char ) {
 			if ( $this->mDTopen ) {
 				$this->mDTopen = false;
 				$text = '</dt></dl>';
@@ -2039,56 +2005,59 @@
 	/**#@-*/
 
 	/**
-	 * Make lists from lines starting with ':', '*', '#', etc.
+	 * Make lists from lines starting with ':', '*', '#', etc. (DBL)
 	 *
 	 * @private
 	 * @return string the lists rendered as HTML
 	 */
 	function doBlockLevels( $text, $linestart ) {
-		$fname = 'Parser::doBlockLevels';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		# Parsing through the text line by line.  The main thing
 		# happening here is handling of block-level elements p, pre,
 		# and making lists from lines starting with * # : etc.
 		#
-		$textLines = explode( "\n", $text );
+		$textLines = StringUtils::explode( "\n", $text );
 
 		$lastPrefix = $output = '';
 		$this->mDTopen = $inBlockElem = false;
 		$prefixLength = 0;
 		$paragraphStack = false;
 
-		if ( !$linestart ) {
-			$output .= array_shift( $textLines );
-		}
 		foreach ( $textLines as $oLine ) {
+			# Fix up $linestart
+			if ( !$linestart ) {
+				$output .= $oLine;
+				$linestart = true;
+				continue;
+			}
+
 			$lastPrefixLength = strlen( $lastPrefix );
 			$preCloseMatch = preg_match('/<\\/pre/i', $oLine );
 			$preOpenMatch = preg_match('/<pre/i', $oLine );
 			if ( !$this->mInPre ) {
 				# Multiple prefixes may abut each other for nested lists.
 				$prefixLength = strspn( $oLine, '*#:;' );
-				$pref = substr( $oLine, 0, $prefixLength );
+				$prefix = substr( $oLine, 0, $prefixLength );
 
 				# eh?
-				$pref2 = str_replace( ';', ':', $pref );
+				$prefix2 = str_replace( ';', ':', $prefix );
 				$t = substr( $oLine, $prefixLength );
-				$this->mInPre = !empty($preOpenMatch);
+				$this->mInPre = (bool)$preOpenMatch;
 			} else {
 				# Don't interpret any other prefixes in preformatted text
 				$prefixLength = 0;
-				$pref = $pref2 = '';
+				$prefix = $prefix2 = '';
 				$t = $oLine;
 			}
 
 			# List generation
-			if( $prefixLength && 0 == strcmp( $lastPrefix, $pref2 ) ) {
+			if( $prefixLength && $lastPrefix === $prefix2 ) {
 				# Same as the last item, so no need to deal with nesting or opening stuff
-				$output .= $this->nextItem( substr( $pref, -1 ) );
+				$output .= $this->nextItem( substr( $prefix, -1 ) );
 				$paragraphStack = false;
 
-				if ( substr( $pref, -1 ) == ';') {
+				if ( substr( $prefix, -1 ) === ';') {
 					# The one nasty exception: definition lists work like this:
 					# ; title : definition text
 					# So we check for : in the remainder text to split up the
@@ -2101,21 +2070,21 @@
 				}
 			} elseif( $prefixLength || $lastPrefixLength ) {
 				# Either open or close a level...
-				$commonPrefixLength = $this->getCommon( $pref, $lastPrefix );
+				$commonPrefixLength = $this->getCommon( $prefix, $lastPrefix );
 				$paragraphStack = false;
 
 				while( $commonPrefixLength < $lastPrefixLength ) {
-					$output .= $this->closeList( $lastPrefix{$lastPrefixLength-1} );
+					$output .= $this->closeList( $lastPrefix[$lastPrefixLength-1] );
 					--$lastPrefixLength;
 				}
 				if ( $prefixLength <= $commonPrefixLength && $commonPrefixLength > 0 ) {
-					$output .= $this->nextItem( $pref{$commonPrefixLength-1} );
+					$output .= $this->nextItem( $prefix[$commonPrefixLength-1] );
 				}
 				while ( $prefixLength > $commonPrefixLength ) {
-					$char = substr( $pref, $commonPrefixLength, 1 );
+					$char = substr( $prefix, $commonPrefixLength, 1 );
 					$output .= $this->openList( $char );
 
-					if ( ';' == $char ) {
+					if ( ';' === $char ) {
 						# FIXME: This is dupe of code above
 						if ($this->findColonNoLinks($t, $term, $t2) !== false) {
 							$t = $t2;
@@ -2124,10 +2093,10 @@
 					}
 					++$commonPrefixLength;
 				}
-				$lastPrefix = $pref2;
+				$lastPrefix = $prefix2;
 			}
 			if( 0 == $prefixLength ) {
-				wfProfileIn( "$fname-paragraph" );
+				wfProfileIn( __METHOD__."-paragraph" );
 				# No prefix (not in list)--go to paragraph mode
 				// 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 );
@@ -2147,9 +2116,9 @@
 						$inBlockElem = true;
 					}
 				} else if ( !$inBlockElem && !$this->mInPre ) {
-					if ( ' ' == $t{0} and ( $this->mLastSection == 'pre' or trim($t) != '' ) ) {
+					if ( ' ' == $t{0} and ( $this->mLastSection === 'pre' or trim($t) != '' ) ) {
 						// pre
-						if ($this->mLastSection != 'pre') {
+						if ($this->mLastSection !== 'pre') {
 							$paragraphStack = false;
 							$output .= $this->closeParagraph().'<pre>';
 							$this->mLastSection = 'pre';
@@ -2163,7 +2132,7 @@
 								$paragraphStack = false;
 								$this->mLastSection = 'p';
 							} else {
-								if ($this->mLastSection != 'p' ) {
+								if ($this->mLastSection !== 'p' ) {
 									$output .= $this->closeParagraph();
 									$this->mLastSection = '';
 									$paragraphStack = '<p>';
@@ -2176,14 +2145,14 @@
 								$output .= $paragraphStack;
 								$paragraphStack = false;
 								$this->mLastSection = 'p';
-							} else if ($this->mLastSection != 'p') {
+							} else if ($this->mLastSection !== 'p') {
 								$output .= $this->closeParagraph().'<p>';
 								$this->mLastSection = 'p';
 							}
 						}
 					}
 				}
-				wfProfileOut( "$fname-paragraph" );
+				wfProfileOut( __METHOD__."-paragraph" );
 			}
 			// somewhere above we forget to get out of pre block (bug 785)
 			if($preCloseMatch && $this->mInPre) {
@@ -2194,7 +2163,7 @@
 			}
 		}
 		while ( $prefixLength ) {
-			$output .= $this->closeList( $pref2{$prefixLength-1} );
+			$output .= $this->closeList( $prefix2[$prefixLength-1] );
 			--$prefixLength;
 		}
 		if ( '' != $this->mLastSection ) {
@@ -2202,7 +2171,7 @@
 			$this->mLastSection = '';
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $output;
 	}
 
@@ -2215,13 +2184,12 @@
 	 * return string the position of the ':', or false if none found
 	 */
 	function findColonNoLinks($str, &$before, &$after) {
-		$fname = 'Parser::findColonNoLinks';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		$pos = strpos( $str, ':' );
 		if( $pos === false ) {
 			// Nothing to find!
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return false;
 		}
 
@@ -2230,7 +2198,7 @@
 			// Easy; no tag nesting to worry about
 			$before = substr( $str, 0, $pos );
 			$after = substr( $str, $pos+1 );
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return $pos;
 		}
 
@@ -2254,7 +2222,7 @@
 						// We found it!
 						$before = substr( $str, 0, $i );
 						$after = substr( $str, $i + 1 );
-						wfProfileOut( $fname );
+						wfProfileOut( __METHOD__ );
 						return $i;
 					}
 					// Embedded in a tag; don't break it.
@@ -2264,7 +2232,7 @@
 					$colon = strpos( $str, ':', $i );
 					if( $colon === false ) {
 						// Nothing else interesting
-						wfProfileOut( $fname );
+						wfProfileOut( __METHOD__ );
 						return false;
 					}
 					$lt = strpos( $str, '<', $i );
@@ -2273,7 +2241,7 @@
 							// We found it!
 							$before = substr( $str, 0, $colon );
 							$after = substr( $str, $colon + 1 );
-							wfProfileOut( $fname );
+							wfProfileOut( __METHOD__ );
 							return $i;
 						}
 					}
@@ -2320,18 +2288,18 @@
 				break;
 			case 3: // self::COLON_STATE_CLOSETAG:
 				// In a </tag>
-				if( $c == ">" ) {
+				if( $c === ">" ) {
 					$stack--;
 					if( $stack < 0 ) {
-						wfDebug( "Invalid input in $fname; too many close tags\n" );
-						wfProfileOut( $fname );
+						wfDebug( __METHOD__.": Invalid input; too many close tags\n" );
+						wfProfileOut( __METHOD__ );
 						return false;
 					}
 					$state = self::COLON_STATE_TEXT;
 				}
 				break;
 			case self::COLON_STATE_TAGSLASH:
-				if( $c == ">" ) {
+				if( $c === ">" ) {
 					// Yes, a self-closed tag <blah/>
 					$state = self::COLON_STATE_TEXT;
 				} else {
@@ -2340,33 +2308,33 @@
 				}
 				break;
 			case 5: // self::COLON_STATE_COMMENT:
-				if( $c == "-" ) {
+				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;
 				}
 				break;
 			default:
-				throw new MWException( "State machine error in $fname" );
+				throw new MWException( "State machine error in " . __METHOD__ );
 			}
 		}
 		if( $stack > 0 ) {
-			wfDebug( "Invalid input in $fname; not enough close tags (stack $stack, state $state)\n" );
+			wfDebug( __METHOD__.": Invalid input; not enough close tags (stack $stack, state $state)\n" );
 			return false;
 		}
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return false;
 	}
 
@@ -2596,12 +2564,11 @@
 	 * @private
 	 */
 	function initialiseVariables() {
-		$fname = 'Parser::initialiseVariables';
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 		$variableIDs = MagicWord::getVariableIDs();
 
 		$this->mVariables = new MagicWordArray( $variableIDs );
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 	}
 
 	/**
@@ -2670,8 +2637,7 @@
 			return $text;
 		}
 
-		$fname = __METHOD__;
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 
 		if ( $frame === false ) {
 			$frame = $this->getPreprocessor()->newFrame();
@@ -2684,7 +2650,7 @@
 		$flags = $argsOnly ? PPFrame::NO_TEMPLATES : 0;
 		$text = $frame->expand( $dom, $flags );
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $text;
 	}
 
@@ -2747,8 +2713,7 @@
 	 */
 	function braceSubstitution( $piece, $frame ) {
 		global $wgContLang, $wgLang, $wgAllowDisplayTitle, $wgNonincludableNamespaces;
-		$fname = __METHOD__;
-		wfProfileIn( $fname );
+		wfProfileIn( __METHOD__ );
 		wfProfileIn( __METHOD__.'-setup' );
 
 		# Flags
@@ -2935,7 +2900,7 @@
 					}
 				} else if ( $wgNonincludableNamespaces && in_array( $title->getNamespace(), $wgNonincludableNamespaces ) ) {
 					$found = false; //access denied
-					wfDebug( "$fname: template inclusion denied for " . $title->getPrefixedDBkey() );
+					wfDebug( __METHOD__.": template inclusion denied for " . $title->getPrefixedDBkey() );
 				} else {
 					list( $text, $title ) = $this->getTemplateDom( $title );
 					if ( $text !== false ) {
@@ -2969,7 +2934,7 @@
 		# Recover the source wikitext and return it
 		if ( !$found ) {
 			$text = $frame->virtualBracketedImplode( '{{', '|', '}}', $titleWithSpaces, $args );
-			wfProfileOut( $fname );
+			wfProfileOut( __METHOD__ );
 			return array( 'object' => $text );
 		}
 
@@ -3028,7 +2993,7 @@
 			$ret = array( 'text' => $text );
 		}
 
-		wfProfileOut( $fname );
+		wfProfileOut( __METHOD__ );
 		return $ret;
 	}
 
@@ -3315,7 +3280,7 @@
 			}
 		}
 
-		if ( $name == 'html' || $name == 'nowiki' ) {
+		if ( $name === 'html' || $name === 'nowiki' ) {
 			$this->mStripState->nowiki->setPair( $marker, $output );
 		} else {
 			$this->mStripState->general->setPair( $marker, $output );
@@ -3571,12 +3536,7 @@
 			#     <!--LINK number-->
 			# turns into
 			#     link text with suffix
-			$safeHeadline = preg_replace( '/<!--LINK ([0-9]*)-->/e',
-							    "\$this->mLinkHolders['texts'][\$1]",
-							    $safeHeadline );
-			$safeHeadline = preg_replace( '/<!--IWLINK ([0-9]*)-->/e',
-							    "\$this->mInterwikiLinkHolders['texts'][\$1]",
-							    $safeHeadline );
+			$safeHeadline = $this->replaceLinkHoldersText( $safeHeadline );
 
 			# Strip out HTML (other than plain <sup> and <sub>: bug 8393)
 			$tocline = preg_replace(
@@ -3652,7 +3612,7 @@
 		$i = 0;
 
 		foreach( $blocks as $block ) {
-			if( $showEditLink && $headlineCount > 0 && $i == 0 && $block != "\n" ) {
+			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
 
@@ -3804,7 +3764,7 @@
 			} else {
 				# Failed to validate; fall back to the default
 				$nickname = $username;
-				wfDebug( "Parser::getUserSig: $username has bad XML tags in signature.\n" );
+				wfDebug( __METHOD__.": $username has bad XML tags in signature.\n" );
 			}
 		}
 
@@ -3910,19 +3870,17 @@
 		global $wgTitle;
 		static $executing = false;
 
-		$fname = "Parser::transformMsg";
-
 		# Guard against infinite recursion
 		if ( $executing ) {
 			return $text;
 		}
 		$executing = true;
 
-		wfProfileIn($fname);
+		wfProfileIn(__METHOD__);
 		$text = $this->preprocess( $text, $wgTitle, $options );
 
 		$executing = false;
-		wfProfileOut($fname);
+		wfProfileOut(__METHOD__);
 		return $text;
 	}
 
@@ -4019,7 +3977,7 @@
 		# Add to function cache
 		$mw = MagicWord::get( $id );
 		if( !$mw )
-			throw new MWException( 'Parser::setFunctionHook() expecting a magic word identifier.' );
+			throw new MWException( __METHOD__.'() expecting a magic word identifier.' );
 
 		$synonyms = $mw->getSynonyms();
 		$sensitive = intval( $mw->isCaseSensitive() );
@@ -4034,7 +3992,7 @@
 				$syn = '#' . $syn;
 			}
 			# Remove trailing colon
-			if ( substr( $syn, -1, 1 ) == ':' ) {
+			if ( substr( $syn, -1, 1 ) === ':' ) {
 				$syn = substr( $syn, 0, -1 );
 			}
 			$this->mFunctionSynonyms[$sensitive][$syn] = $id;
@@ -4055,266 +4013,9 @@
 	 * 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.
-	 * $options is a bit field, RLH_FOR_UPDATE to select for update
 	 */
 	function replaceLinkHolders( &$text, $options = 0 ) {
-		global $wgUser;
-		global $wgContLang;
-
-		$fname = 'Parser::replaceLinkHolders';
-		wfProfileIn( $fname );
-
-		$pdbks = array();
-		$colours = array();
-		$linkcolour_ids = array();
-		$sk = $this->mOptions->getSkin();
-		$linkCache = LinkCache::singleton();
-
-		if ( !empty( $this->mLinkHolders['namespaces'] ) ) {
-			wfProfileIn( $fname.'-check' );
-			$dbr = wfGetDB( DB_SLAVE );
-			$page = $dbr->tableName( 'page' );
-			$threshold = $wgUser->getOption('stubthreshold');
-
-			# Sort by namespace
-			asort( $this->mLinkHolders['namespaces'] );
-
-			# Generate query
-			$query = false;
-			$current = null;
-			foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-				# Make title object
-				$title = $this->mLinkHolders['titles'][$key];
-
-				# Skip invalid entries.
-				# Result will be ugly, but prevents crash.
-				if ( is_null( $title ) ) {
-					continue;
-				}
-				$pdbk = $pdbks[$key] = $title->getPrefixedDBkey();
-
-				# Check if it's a static known link, e.g. interwiki
-				if ( $title->isAlwaysKnown() ) {
-					$colours[$pdbk] = '';
-				} elseif ( ( $id = $linkCache->getGoodLinkID( $pdbk ) ) != 0 ) {
-					$colours[$pdbk] = '';
-					$this->mOutput->addLink( $title, $id );
-				} elseif ( $linkCache->isBadLink( $pdbk ) ) {
-					$colours[$pdbk] = 'new';
-				} elseif ( $title->getNamespace() == NS_SPECIAL && !SpecialPage::exists( $pdbk ) ) {
-					$colours[$pdbk] = 'new';
-				} else {
-					# Not in the link cache, add it to the query
-					if ( !isset( $current ) ) {
-						$current = $ns;
-						$query =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-						$query .= " FROM $page WHERE (page_namespace=$ns AND page_title IN(";
-					} elseif ( $current != $ns ) {
-						$current = $ns;
-						$query .= ")) OR (page_namespace=$ns AND page_title IN(";
-					} else {
-						$query .= ', ';
-					}
-
-					$query .= $dbr->addQuotes( $this->mLinkHolders['dbkeys'][$key] );
-				}
-			}
-			if ( $query ) {
-				$query .= '))';
-				if ( $options & RLH_FOR_UPDATE ) {
-					$query .= ' FOR UPDATE';
-				}
-
-				$res = $dbr->query( $query, $fname );
-
-				# Fetch data and form into an associative array
-				# non-existent = broken
-				while ( $s = $dbr->fetchObject($res) ) {
-					$title = Title::makeTitle( $s->page_namespace, $s->page_title );
-					$pdbk = $title->getPrefixedDBkey();
-					$linkCache->addGoodLinkObj( $s->page_id, $title, $s->page_len, $s->page_is_redirect );
-					$this->mOutput->addLink( $title, $s->page_id );
-					$colours[$pdbk] = $sk->getLinkColour( $title, $threshold );
-					//add id to the extension todolist
-					$linkcolour_ids[$s->page_id] = $pdbk;
-				}
-				//pass an array of page_ids to an extension
-				wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-			}
-			wfProfileOut( $fname.'-check' );
-
-			# Do a second query for different language variants of links and categories
-			if($wgContLang->hasVariants()){
-				$linkBatch = new LinkBatch();
-				$variantMap = array(); // maps $pdbkey_Variant => $keys (of link holders)
-				$categoryMap = array(); // maps $category_variant => $category (dbkeys)
-				$varCategories = array(); // category replacements oldDBkey => newDBkey
-
-				$categories = $this->mOutput->getCategoryLinks();
-
-				// Add variants of links to link batch
-				foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-					$title = $this->mLinkHolders['titles'][$key];
-					if ( is_null( $title ) )
-						continue;
-
-					$pdbk = $title->getPrefixedDBkey();
-					$titleText = $title->getText();
-
-					// generate all variants of the link title text
-					$allTextVariants = $wgContLang->convertLinkToAllVariants($titleText);
-
-					// if link was not found (in first query), add all variants to query
-					if ( !isset($colours[$pdbk]) ){
-						foreach($allTextVariants as $textVariant){
-							if($textVariant != $titleText){
-								$variantTitle = Title::makeTitle( $ns, $textVariant );
-								if(is_null($variantTitle)) continue;
-								$linkBatch->addObj( $variantTitle );
-								$variantMap[$variantTitle->getPrefixedDBkey()][] = $key;
-							}
-						}
-					}
-				}
-
-				// process categories, check if a category exists in some variant
-				foreach( $categories as $category ){
-					$variants = $wgContLang->convertLinkToAllVariants($category);
-					foreach($variants as $variant){
-						if($variant != $category){
-							$variantTitle = Title::newFromDBkey( Title::makeName(NS_CATEGORY,$variant) );
-							if(is_null($variantTitle)) continue;
-							$linkBatch->addObj( $variantTitle );
-							$categoryMap[$variant] = $category;
-						}
-					}
-				}
-
-
-				if(!$linkBatch->isEmpty()){
-					// construct query
-					$titleClause = $linkBatch->constructSet('page', $dbr);
-
-					$variantQuery =  "SELECT page_id, page_namespace, page_title, page_is_redirect, page_len";
-
-					$variantQuery .= " FROM $page WHERE $titleClause";
-					if ( $options & RLH_FOR_UPDATE ) {
-						$variantQuery .= ' FOR UPDATE';
-					}
-
-					$varRes = $dbr->query( $variantQuery, $fname );
-
-					// for each found variants, figure out link holders and replace
-					while ( $s = $dbr->fetchObject($varRes) ) {
-
-						$variantTitle = Title::makeTitle( $s->page_namespace, $s->page_title );
-						$varPdbk = $variantTitle->getPrefixedDBkey();
-						$vardbk = $variantTitle->getDBkey();
-
-						$holderKeys = array();
-						if(isset($variantMap[$varPdbk])){
-							$holderKeys = $variantMap[$varPdbk];
-							$linkCache->addGoodLinkObj( $s->page_id, $variantTitle, $s->page_len, $s->page_is_redirect );
-							$this->mOutput->addLink( $variantTitle, $s->page_id );
-						}
-
-						// loop over link holders
-						foreach($holderKeys as $key){
-							$title = $this->mLinkHolders['titles'][$key];
-							if ( is_null( $title ) ) continue;
-
-							$pdbk = $title->getPrefixedDBkey();
-
-							if(!isset($colours[$pdbk])){
-								// found link in some of the variants, replace the link holder data
-								$this->mLinkHolders['titles'][$key] = $variantTitle;
-								$this->mLinkHolders['dbkeys'][$key] = $variantTitle->getDBkey();
-
-								// set pdbk and colour
-								$pdbks[$key] = $varPdbk;
-								$colours[$varPdbk] = $sk->getLinkColour( $variantTitle, $threshold );
-								$linkcolour_ids[$s->page_id] = $pdbk;
-							}
-							wfRunHooks( 'GetLinkColours', array( $linkcolour_ids, &$colours ) );
-						}
-
-						// check if the object is a variant of a category
-						if(isset($categoryMap[$vardbk])){
-							$oldkey = $categoryMap[$vardbk];
-							if($oldkey != $vardbk)
-								$varCategories[$oldkey]=$vardbk;
-						}
-					}
-
-					// rebuild the categories in original order (if there are replacements)
-					if(count($varCategories)>0){
-						$newCats = array();
-						$originalCats = $this->mOutput->getCategories();
-						foreach($originalCats as $cat => $sortkey){
-							// make the replacement
-							if( array_key_exists($cat,$varCategories) )
-								$newCats[$varCategories[$cat]] = $sortkey;
-							else $newCats[$cat] = $sortkey;
-						}
-						$this->mOutput->setCategoryLinks($newCats);
-					}
-				}
-			}
-
-			# Construct search and replace arrays
-			wfProfileIn( $fname.'-construct' );
-			$replacePairs = array();
-			foreach ( $this->mLinkHolders['namespaces'] as $key => $ns ) {
-				$pdbk = $pdbks[$key];
-				$searchkey = "<!--LINK $key-->";
-				$title = $this->mLinkHolders['titles'][$key];
-				if ( !isset( $colours[$pdbk] ) || $colours[$pdbk] == 'new' ) {
-					$linkCache->addBadLinkObj( $title );
-					$colours[$pdbk] = 'new';
-					$this->mOutput->addLink( $title, 0 );
-					$replacePairs[$searchkey] = $sk->makeBrokenLinkObj( $title,
-									$this->mLinkHolders['texts'][$key],
-									$this->mLinkHolders['queries'][$key] );
-				} else {
-					$replacePairs[$searchkey] = $sk->makeColouredLinkObj( $title, $colours[$pdbk],
-									$this->mLinkHolders['texts'][$key],
-									$this->mLinkHolders['queries'][$key] );
-				}
-			}
-			$replacer = new HashtableReplacer( $replacePairs, 1 );
-			wfProfileOut( $fname.'-construct' );
-
-			# Do the thing
-			wfProfileIn( $fname.'-replace' );
-			$text = preg_replace_callback(
-				'/(<!--LINK .*?-->)/',
-				$replacer->cb(),
-				$text);
-
-			wfProfileOut( $fname.'-replace' );
-		}
-
-		# Now process interwiki link holders
-		# This is quite a bit simpler than internal links
-		if ( !empty( $this->mInterwikiLinkHolders['texts'] ) ) {
-			wfProfileIn( $fname.'-interwiki' );
-			# Make interwiki link HTML
-			$replacePairs = array();
-			foreach( $this->mInterwikiLinkHolders['texts'] as $key => $link ) {
-				$title = $this->mInterwikiLinkHolders['titles'][$key];
-				$replacePairs[$key] = $sk->link( $title, $link );
-			}
-			$replacer = new HashtableReplacer( $replacePairs, 1 );
-
-			$text = preg_replace_callback(
-				'/<!--IWLINK (.*?)-->/',
-				$replacer->cb(),
-				$text );
-			wfProfileOut( $fname.'-interwiki' );
-		}
-
-		wfProfileOut( $fname );
-		return $colours;
+		return $this->mLinkHolders->replace( $text );
 	}
 
 	/**
@@ -4324,39 +4025,10 @@
 	 * @return string
 	 */
 	function replaceLinkHoldersText( $text ) {
-		$fname = 'Parser::replaceLinkHoldersText';
-		wfProfileIn( $fname );
-
-		$text = preg_replace_callback(
-			'/<!--(LINK|IWLINK) (.*?)-->/',
-			array( &$this, 'replaceLinkHoldersTextCallback' ),
-			$text );
-
-		wfProfileOut( $fname );
-		return $text;
+		return $this->mLinkHolders->replaceText( $text );
 	}
 
 	/**
-	 * @param array $matches
-	 * @return string
-	 * @private
-	 */
-	function replaceLinkHoldersTextCallback( $matches ) {
-		$type = $matches[1];
-		$key  = $matches[2];
-		if( $type == 'LINK' ) {
-			if( isset( $this->mLinkHolders['texts'][$key] ) ) {
-				return $this->mLinkHolders['texts'][$key];
-			}
-		} elseif( $type == 'IWLINK' ) {
-			if( isset( $this->mInterwikiLinkHolders['texts'][$key] ) ) {
-				return $this->mInterwikiLinkHolders['texts'][$key];
-			}
-		}
-		return $matches[0];
-	}
-
-	/**
 	 * Tag hook handler for 'pre'.
 	 */
 	function renderPreTag( $text, $attribs ) {
@@ -4407,7 +4079,7 @@
 
 		wfRunHooks( 'BeforeParserrenderImageGallery', array( &$this, &$ig ) );
 
-		$lines = explode( "\n", $text );
+		$lines = StringUtils::explode( "\n", $text );
 		foreach ( $lines as $line ) {
 			# match lines like these:
 			# Image:someimage.jpg|This is some image
@@ -4420,7 +4092,7 @@
 			
 			if ( strpos( $matches[0], '%' ) !== false )
 				$matches[1] = urldecode( $matches[1] );
-			$tp = Title::newFromText( $matches[1] );
+			$tp = Title::newFromText( $matches[1]/*, NS_IMAGE*/ );
 			$nt =& $tp;
 			if( is_null( $nt ) ) {
 				# Bogus title. Ignore these so we don't bomb out later.
@@ -4486,8 +4158,11 @@
 
 	/**
 	 * Parse image options text and use it to make an image
+	 * @param Title $title
+	 * @param string $options
+	 * @param LinkHolderArray $holders
 	 */
-	function makeImage( $title, $options ) {
+	function makeImage( $title, $options, $holders = false ) {
 		# Check if the options text is of the form "options|alt text"
 		# Options are:
 		#  * thumbnail       	make a thumbnail with enlarge-icon and caption, alignment depends on lang
@@ -4510,7 +4185,7 @@
 		#  * bottom
 		#  * text-bottom
 
-		$parts = array_map( 'trim', explode( '|', $options) );
+		$parts = StringUtils::explode( "|", $options );
 		$sk = $this->mOptions->getSkin();
 
 		# Give extensions a chance to select the file revision for us
@@ -4532,13 +4207,14 @@
 		$params = array( 'frame' => array(), 'handler' => array(),
 			'horizAlign' => array(), 'vertAlign' => array() );
 		foreach( $parts as $part ) {
+			$part = trim( $part );
 			list( $magicName, $value ) = $mwArray->matchVariableStartToEnd( $part );
 			$validated = false;
 			if( isset( $paramMap[$magicName] ) ) {
 				list( $type, $paramName ) = $paramMap[$magicName];
 
 				// Special case; width and height come in one variable together
-				if( $type == 'handler' && $paramName == 'width' ) {
+				if( $type === 'handler' && $paramName === 'width' ) {
 					$m = array();
 					# (bug 13500) In both cases (width/height and width only),
 					# permit trailing "px" for backward compatibility.
@@ -4561,7 +4237,7 @@
 						}
 					} // else no validation -- bug 13436
 				} else {
-					if ( $type == 'handler' ) {
+					if ( $type === 'handler' ) {
 						# Validate handler parameter
 						$validated = $handler->validateParam( $paramName, $value );
 					} else {
@@ -4597,7 +4273,13 @@
 		}
 
 		# Strip bad stuff out of the alt text
-		$alt = $this->replaceLinkHoldersText( $caption );
+		# We can't just use replaceLinkHoldersText() here, because if this function
+		# is called from replaceInternalLinks2(), mLinkHolders won't be up to date.
+		if ( $holders ) {
+			$alt = $holders->replaceText( $caption );
+		} else {
+			$alt = $this->replaceLinkHoldersText( $caption );
+		}
 
 		# make sure there are no placeholders in thumbnail attributes
 		# that are later expanded to html- so expand them now and
@@ -4700,7 +4382,7 @@
 		$sectionParts = explode( '-', $section );
 		$sectionIndex = array_pop( $sectionParts );
 		foreach ( $sectionParts as $part ) {
-			if ( $part == 'T' ) {
+			if ( $part === 'T' ) {
 				$flags |= self::PTD_FOR_INCLUSION;
 			}
 		}
@@ -4717,14 +4399,14 @@
 			$targetLevel = 1000;
 		} else {
             while ( $node ) {
-                if ( $node->getName() == 'h' ) {
+                if ( $node->getName() === 'h' ) {
                     $bits = $node->splitHeading();
 					if ( $bits['i'] == $sectionIndex ) {
         				$targetLevel = $bits['level'];
 						break;
 					}
 				}
-				if ( $mode == 'replace' ) {
+				if ( $mode === 'replace' ) {
 					$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
 				}
 				$node = $node->getNextSibling();
@@ -4733,7 +4415,7 @@
 
 		if ( !$node ) {
 			// Not found
-			if ( $mode == 'get' ) {
+			if ( $mode === 'get' ) {
 				return $newText;
 			} else {
 				return $text;
@@ -4742,21 +4424,21 @@
 
 		// Find the end of the section, including nested sections
 		do {
-			if ( $node->getName() == 'h' ) {
+			if ( $node->getName() === 'h' ) {
 				$bits = $node->splitHeading();
 				$curLevel = $bits['level'];
 				if ( $bits['i'] != $sectionIndex && $curLevel <= $targetLevel ) {
 					break;
 				}
 			}
-			if ( $mode == 'get' ) {
+			if ( $mode === 'get' ) {
 				$outText .= $frame->expand( $node, PPFrame::RECOVER_ORIG );
 			}
 			$node = $node->getNextSibling();
 		} while ( $node );
 
 		// Write out the remainder (in replace mode only)
-		if ( $mode == 'replace' ) {
+		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
@@ -4986,7 +4668,7 @@
 		do {
 			$oldText = $text;
 			$text = $this->general->replace( $text );
-		} while ( $text != $oldText );
+		} while ( $text !== $oldText );
 		wfProfileOut( __METHOD__ );
 		return $text;
 	}
@@ -4996,7 +4678,7 @@
 		do {
 			$oldText = $text;
 			$text = $this->nowiki->replace( $text );
-		} while ( $text != $oldText );
+		} while ( $text !== $oldText );
 		wfProfileOut( __METHOD__ );
 		return $text;
 	}
@@ -5007,7 +4689,7 @@
 			$oldText = $text;
 			$text = $this->general->replace( $text );
 			$text = $this->nowiki->replace( $text );
-		} while ( $text != $oldText );
+		} while ( $text !== $oldText );
 		wfProfileOut( __METHOD__ );
 		return $text;
 	}
@@ -5021,7 +4703,7 @@
 	var $output = '';
 
 	function replace( $matches ) {
-		if ( substr( $matches[1], -1 ) == "\n" ) {
+		if ( substr( $matches[1], -1 ) === "\n" ) {
 			$this->output .= substr( $matches[1], 0, -1 );
 		} else {
 			$this->output .= $matches[1];
Index: trunk/phase3/includes/parser/Parser_DiffTest.php
===================================================================
--- trunk/phase3/includes/parser/Parser_DiffTest.php	(revision 40019)
+++ trunk/phase3/includes/parser/Parser_DiffTest.php	(revision 40020)
@@ -69,9 +69,22 @@
 			$lastResult = $currentResult;
 		}
 		if ( $mismatch ) {
-			throw new MWException( "Parser_DiffTest: results mismatch on call to $name\n" .
-				'Arguments: ' . $this->formatArray( $args ) . "\n" .
-				'Results: ' . $this->formatArray( $results ) . "\n" );
+			if ( count( $results ) == 2 ) {
+				$resultsList = array();
+				foreach ( $this->parsers as $i => $parser ) {
+					$resultsList[] = var_export( $results[$i], true );
+				}
+				$diff = wfDiff( $resultsList[0], $resultsList[1] );
+			} else {
+				$diff = '[too many parsers]';
+			}
+			$msg = "Parser_DiffTest: results mismatch on call to $name\n";
+			if ( !$this->shortOutput ) {
+				$msg .= 'Arguments: ' . $this->formatArray( $args ) . "\n";
+			}
+			$msg .= 'Results: ' . $this->formatArray( $results ) . "\n" .
+				"Diff: $diff\n";
+			throw new MWException( $msg );
 		}
 		return $lastResult;
 	}
Index: trunk/phase3/includes/MessageCache.php
===================================================================
--- trunk/phase3/includes/MessageCache.php	(revision 40019)
+++ trunk/phase3/includes/MessageCache.php	(revision 40020)
@@ -649,12 +649,18 @@
 			return $message;
 		}
 
-		global $wgParser;
+		global $wgParser, $wgParserConf;
 		if ( !$this->mParser && isset( $wgParser ) ) {
 			# Do some initialisation so that we don't have to do it twice
 			$wgParser->firstCallInit();
 			# Clone it and store it
-			$this->mParser = clone $wgParser;
+			$class = $wgParserConf['class'];
+			if ( $class == 'Parser_DiffTest' ) {
+				# Uncloneable
+				$this->mParser = new $class( $wgParserConf );
+			} else {
+				$this->mParser = clone $wgParser;
+			}
 			#wfDebug( __METHOD__ . ": following contents triggered transform: $message\n" );
 		}
 		if ( $this->mParser ) {
Index: trunk/phase3/includes/Title.php
===================================================================
--- trunk/phase3/includes/Title.php	(revision 40019)
+++ trunk/phase3/includes/Title.php	(revision 40020)
@@ -410,6 +410,12 @@
 		global $wgInterwikiCache, $wgContLang;
 		$fname = 'Title::getInterwikiLink';
 
+		if ( count( Title::$interwikiCache ) >= self::CACHE_MAX ) {
+			// Don't use infinite memory
+			reset( Title::$interwikiCache );
+			unset( Title::$interwikiCache[ key( Title::$interwikiCache ) ] );
+		}
+
 		$key = $wgContLang->lc( $key );
 
 		$k = wfMemcKey( 'interwiki', $key );
Index: trunk/phase3/includes/DefaultSettings.php
===================================================================
--- trunk/phase3/includes/DefaultSettings.php	(revision 40019)
+++ trunk/phase3/includes/DefaultSettings.php	(revision 40020)
@@ -3336,6 +3336,12 @@
 );
 
 /**
+ * LinkHolderArray batch size
+ * For debugging
+ */
+$wgLinkHolderBatchSize = 1000;
+
+/**
  * Hooks that are used for outputting exceptions.  Format is:
  *   $wgExceptionHooks[] = $funcname
  * or:
Index: trunk/phase3/includes/Exception.php
===================================================================
--- trunk/phase3/includes/Exception.php	(revision 40019)
+++ trunk/phase3/includes/Exception.php	(revision 40020)
@@ -83,7 +83,7 @@
 	function getHTML() {
 		global $wgShowExceptionDetails;
 		if( $wgShowExceptionDetails ) {
-			return '<p>' . htmlspecialchars( $this->getMessage() ) .
+			return '<p>' . nl2br( htmlspecialchars( $this->getMessage() ) ) .
 				'</p><p>Backtrace:</p><p>' . nl2br( htmlspecialchars( $this->getTraceAsString() ) ) .
 				"</p>\n";
 		} else {
Index: trunk/phase3/languages/LanguageConverter.php
===================================================================
--- trunk/phase3/languages/LanguageConverter.php	(revision 40019)
+++ trunk/phase3/languages/LanguageConverter.php	(revision 40020)
@@ -435,8 +435,9 @@
 		if ($isTitle) return $this->convertTitle($text);
 
 		$plang = $this->getPreferredVariant();
-		$tarray = explode($this->mMarkup['end'], $text);
+		$tarray = StringUtils::explode($this->mMarkup['end'], $text);
 		$text = '';
+		$lastDelim = false;
 		foreach($tarray as $txt) {
 			$marked = explode($this->mMarkup['begin'], $txt, 2);
 
@@ -452,8 +453,17 @@
 
 				$text .= $crule->getDisplay();
 				$this->applyManualConv($crule);
+				$lastDelim = false;
+			} else {
+				// Reinsert the }- which wasn't part of anything
+				$text .= $this->mMarkup['end'];
+				$lastDelim = true;
 			}
 		}
+		if ( $lastDelim ) {
+			// Remove the last delimiter (wasn't real)
+			$text = substr( $text, 0, -strlen( $this->mMarkup['end'] ) );
+		}
 
 		return $text;
 	}
Index: trunk/phase3/languages/Language.php
===================================================================
--- trunk/phase3/languages/Language.php	(revision 40019)
+++ trunk/phase3/languages/Language.php	(revision 40020)
@@ -177,6 +177,15 @@
 	}
 
 	/**
+	 * Reduce memory usage
+	 */
+	function __destruct() {
+		foreach ( $this as $name => $value ) {
+			unset( $this->$name );
+		}
+	}
+
+	/**
 	 * Hook which will be called if this is the content language.
 	 * Descendants can use this to register hook functions or modify globals
 	 */
Index: trunk/phase3/RELEASE-NOTES
===================================================================
--- trunk/phase3/RELEASE-NOTES	(revision 40019)
+++ trunk/phase3/RELEASE-NOTES	(revision 40020)
@@ -94,6 +94,7 @@
 * HTML entities like &nbsp; now work (are not escaped) in edit summaries.
 * (bug 13815) In the comment for page moves, use the colon-separator message
   instead of a hardcoded colon.
+* Allow <gallery> to accept image names without an Image: prefix
 
 === Bug fixes in 1.14 ===
 

Follow-up revisions

Rev.Commit summaryAuthorDate
r69596Uncommented the DROP TABLE queries for parsertest_*, accidentally commented o...tstarling10:10, 20 July 2010

Status & tagging log

  • 15:31, 12 September 2011 Meno25 (Talk | contribs) changed the status of r40020 [removed: ok added: old]
Personal tools
Namespaces
Variants
Views
Actions
Site
Support
Download
Development
Communication
Toolbox