Index: wp-admin/css/colors-classic.dev.css
===================================================================
--- wp-admin/css/colors-classic.dev.css	(revision 21100)
+++ wp-admin/css/colors-classic.dev.css	(working copy)
@@ -619,7 +619,8 @@
 }
 
 div#media-upload-header,
-div#plugin-information-header {
+div#plugin-information-header,
+div#plugin-readme-header {
 	background-color: #f9f9f9;
 	border-bottom-color: #dfdfdf;
 }
@@ -1926,20 +1927,25 @@
 }
 
 /* Install Plugins */
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	background-color: #eaf3fa;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	background-color: #cee1ef;
 }
 
 #plugin-information pre,
-#plugin-information code {
+#plugin-information code,
+#plugin-readme pre,
+#plugin-readme code {
 	background-color: #ededff;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	border: 1px solid #ccc;
 }
 
Index: wp-admin/css/colors-fresh.dev.css
===================================================================
--- wp-admin/css/colors-fresh.dev.css	(revision 21100)
+++ wp-admin/css/colors-fresh.dev.css	(working copy)
@@ -610,7 +610,8 @@
 }
 
 div#media-upload-header,
-div#plugin-information-header {
+div#plugin-information-header,
+div#plugin-readme-header {
 	background-color: #f9f9f9;
 	border-bottom-color: #dfdfdf;
 }
@@ -1542,20 +1543,25 @@
 }
 
 /* Install Plugins */
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	background-color: #eaf3fa;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	background-color: #cee1ef;
 }
 
 #plugin-information pre,
-#plugin-information code {
+#plugin-information code,
+#plugin-readme pre,
+#plugin-readme code {
 	background-color: #ededff;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	border: 1px solid #ccc;
 }
 
Index: wp-admin/css/wp-admin.dev.css
===================================================================
--- wp-admin/css/wp-admin.dev.css	(revision 21100)
+++ wp-admin/css/wp-admin.dev.css	(working copy)
@@ -7164,7 +7164,8 @@
 }
 
 /* Header on thickbox */
-#plugin-information-header {
+#plugin-information-header,
+#plugin-readme-header {
 	margin: 0;
 	padding: 0 5px;
 	font-weight: bold;
@@ -7173,7 +7174,8 @@
 	border-bottom-style: solid;
 	height: 2.5em;
 }
-#plugin-information ul#sidemenu {
+#plugin-information ul#sidemenu,
+#plugin-readme ul#sidemenu {
 	font-weight: normal;
 	margin: 0 5px;
 	position: absolute;
@@ -7201,28 +7203,33 @@
 	line-height: 2em;
 }
 
-#plugin-information h2 {
+#plugin-information h2,
+#plugin-readme h2 {
 	clear: none !important;
 	margin-right: 200px;
 }
 
-#plugin-information .fyi {
+#plugin-information .fyi,
+#plugin-readme .fyi {
 	margin: 0 10px 50px;
 	width: 210px;
 }
 
-#plugin-information .fyi h2 {
+#plugin-information .fyi h2,
+#plugin-readme .fyi h2 {
 	font-size: 0.9em;
 	margin-bottom: 0;
 	margin-right: 0;
 }
 
-#plugin-information .fyi h2.mainheader {
+#plugin-information .fyi h2.mainheader,
+#plugin-readme .fyi h2.mainheader {
 	padding: 5px;
 	-webkit-border-top-left-radius: 3px;
 	border-top-left-radius: 3px;
 }
 
+#plugin-readme .fyi ul,
 #plugin-information .fyi ul {
 	padding: 10px 5px 10px 7px;
 	margin: 0;
@@ -7231,16 +7238,20 @@
 	border-bottom-left-radius: 3px;
 }
 
-#plugin-information .fyi li {
+#plugin-information .fyi li,
+#plugin-readme .fyi li {
 	margin-right: 0;
 }
 
-#plugin-information #section-holder {
+#plugin-information #section-holder,
+#plugin-readme #section-holder {
 	padding: 10px;
 }
 
 #plugin-information .section ul,
-#plugin-information .section ol {
+#plugin-information .section ol,
+#plugin-readme .section ul,
+#plugin-readme .section ol {
 	margin-left: 16px;
 	list-style-type: square;
 	list-style-image: none;
@@ -7266,11 +7277,14 @@
 
 #plugin-information #section-screenshots ol,
 #plugin-information .updated,
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme .updated,
+#plugin-readme pre {
 	margin-right: 215px;
 }
 
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme pre {
 	padding: 7px;
 	overflow: auto;
 }
Index: wp-admin/css/wp-admin-rtl.dev.css
===================================================================
--- wp-admin/css/wp-admin-rtl.dev.css	(revision 21100)
+++ wp-admin/css/wp-admin-rtl.dev.css	(working copy)
@@ -2239,26 +2239,31 @@
 	float: right;
 }
 
-#plugin-information ul#sidemenu {
+#plugin-information ul#sidemenu,
+#plugin-readme ul#sidemenu {
 	left: auto;
 	right: 0;
 }
 
-#plugin-information h2 {
+#plugin-information h2,
+#plugin-readme h2 {
 	margin-right: 0;
 	margin-left: 200px;
 }
 
-#plugin-information .fyi {
+#plugin-information .fyi,
+#plugin-readme .fyi {
 	margin-left: 5px;
 	margin-right: 20px;
 }
 
-#plugin-information .fyi h2 {
+#plugin-information .fyi h2,
+#plugin-readme .fyi h2 {
 	margin-left: 0;
 }
 
-#plugin-information .fyi ul {
+#plugin-information .fyi ul,
+#plugin-readme .fyi ul {
 	padding: 10px 7px 10px 5px;
 }
 
@@ -2269,13 +2274,17 @@
 
 #plugin-information #section-screenshots ol,
 #plugin-information .updated,
-#plugin-information pre {
+#plugin-information pre,
+#plugin-readme .updated,
+#plugin-readme pre {
 	margin-right: 0;
 	margin-left: 215px;
 }
 
 #plugin-information .updated,
-#plugin-information .error {
+#plugin-information .error,
+#plugin-readme .updated,
+#plugin-readme .error {
 	clear: none;
 	direction: rtl;
 }
Index: wp-admin/plugin-install.php
===================================================================
--- wp-admin/plugin-install.php	(revision 21100)
+++ wp-admin/plugin-install.php	(working copy)
@@ -6,7 +6,7 @@
  * @subpackage Administration
  */
 // TODO route this pages via a specific iframe handler instead of the do_action below
-if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( 'plugin-information' == $_GET['tab'] ) )
+if ( !defined( 'IFRAME_REQUEST' ) && isset( $_GET['tab'] ) && ( in_array( $_GET['tab'], array( 'plugin-information', 'plugin-readme' ) ) ) )
 	define( 'IFRAME_REQUEST', true );
 
 /** WordPress Administration Bootstrap */
@@ -28,7 +28,7 @@
 $parent_file = 'plugins.php';
 
 wp_enqueue_script( 'plugin-install' );
-if ( 'plugin-information' != $tab )
+if ( !in_array( $tab, array('plugin-information', 'plugin-readme') ) )
 	add_thickbox();
 
 $body_id = $tab;
Index: wp-admin/includes/class-wp-plugins-list-table.php
===================================================================
--- wp-admin/includes/class-wp-plugins-list-table.php	(revision 21100)
+++ wp-admin/includes/class-wp-plugins-list-table.php	(working copy)
@@ -420,6 +420,10 @@
 							$author = '<a href="' . $plugin_data['AuthorURI'] . '" title="' . esc_attr__( 'Visit author homepage' ) . '">' . $plugin_data['Author'] . '</a>';
 						$plugin_meta[] = sprintf( __( 'By %s' ), $author );
 					}
+					$slug = basename( $plugin_file, '.php' );
+					$plugin_meta[] = '<a href="' . self_admin_url( 'plugin-install.php?tab=plugin-readme&amp;readme=true&amp;plugin=' . $slug .
+								'&amp;TB_iframe=true&amp;width=600&amp;height=550' ) . '" class="thickbox" title="' .
+								esc_attr( sprintf( __( 'More information about %s' ), "{$plugin_data['Name']} {$plugin_data['Version']}" ) ) . '">' . __( 'Details' ) . '</a>';
 					if ( ! empty( $plugin_data['PluginURI'] ) )
 						$plugin_meta[] = '<a href="' . $plugin_data['PluginURI'] . '" title="' . esc_attr__( 'Visit plugin site' ) . '">' . __( 'Visit plugin site' ) . '</a>';
 
Index: wp-admin/includes/plugin-install.php
===================================================================
--- wp-admin/includes/plugin-install.php	(revision 21100)
+++ wp-admin/includes/plugin-install.php	(working copy)
@@ -380,3 +380,216 @@
 	exit;
 }
 add_action('install_plugins_pre_plugin-information', 'install_plugin_information');
+
+/**
+ * Get information from a plugin locally
+ * @param string $slug
+ * @since 3.4.0
+ */
+function local_plugin_api( $slug ) {
+
+	// Try to find the plugin file from the slug
+	$plugin_data = array();
+	if ( file_exists( ABSPATH . PLUGINDIR . "/$slug.php" ) ) {
+		$plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug.php" );
+	} elseif ( file_exists( ABSPATH . PLUGINDIR . "/$slug/$slug.php" ) ) {
+		$plugin_data = get_plugin_data( ABSPATH . PLUGINDIR . "/$slug/$slug.php" );
+	}
+	
+	// Try to load data from readme.txt
+	$readme_data = false;
+	if ( file_exists( ABSPATH . PLUGINDIR . "/$slug/readme.txt" ) ) {
+
+		// Get markddown library, turn off plugin functionality
+		if ( !defined( 'MARKDOWN_WP_POSTS' ) )
+			define( 'MARKDOWN_WP_POSTS', false );
+		if ( !defined( 'MARKDOWN_WP_COMMENTS' ) )
+			define( 'MARKDOWN_WP_COMMENTS', false );
+		if ( !class_exists( 'Markdown_Parser' ) )
+			include_once( ABSPATH . 'wp-admin/includes/markdown.php' );
+		
+		include_once( ABSPATH . 'wp-admin/includes/class-wp-plugin-readme-parser.php' );
+		$readme_parser = new wp_plugin_readme_parser();
+		$readme_data = $readme_parser->parse_readme_file( ABSPATH . PLUGINDIR . "/$slug/readme.txt" );
+	}
+	
+	// If there's no readme (e.g. hello.php) create a fake structure
+	if ( empty( $readme_data ) ) {
+		$readme_data = array(
+			'contributors'	  => '',
+			'requires_at_least' => '',
+			'tested_up_to'	  => '',
+			'tags'			  => array(),
+			'sections'		  => array(
+				'description'   => !empty( $plugin_data['Description'] ) ? $plugin_data['Description'] : ''
+			)
+		);
+	}
+
+	// Convert to an API-style response
+	$api = array(
+		'name'		   => !empty( $plugin_data['Name'] ) ? $plugin_data['Name'] : $slug,
+		'slug'		   => $slug,
+		'version'		=> !empty( $plugin_data['Version'] ) ? $plugin_data['Version'] : '',
+		'author'		 => !empty( $plugin_data['AuthorName'] ) ? $plugin_data['AuthorName'] : '',
+		'author_profile' => null,
+		'contributors'   => $readme_data['contributors'],
+		'requires'	   => $readme_data['requires_at_least'],
+		'tested'		 => $readme_data['tested_up_to'],
+		'compatibility'  => null,
+		'rating'		 => null,
+		'num_ratings'	=> null,
+		'downloaded'	 => null,
+		'last_updated'   => null,
+		'added'		  => null,
+		'homepage'	   => !empty( $plugin_data['PluginURI'] ) ? $plugin_data['PluginURI'] : '',
+		'sections'	   => $readme_data['sections'],
+		'download_link'  => null,
+		'tags'		   => $readme_data['tags']
+	);
+
+	// Done
+	return (object) $api;
+}
+
+/**
+ * Display local plugin information in dialog box form.
+ * Pull from the readme.txt file and the plugin header
+ * @since 3.4.0
+ */
+function plugin_readme_information() {
+	global $tab;
+
+	$api = local_plugin_api( stripslashes( $_REQUEST['plugin'] ) );
+
+	if ( is_wp_error($api) )
+		wp_die($api);
+
+	$plugins_allowedtags = array(
+		'a'	=> array(
+			'href'   => array(),
+			'title'  => array(),
+			'target' => array()
+		),
+		'abbr' => array(
+			'title' => array()
+		),
+		'acronym' => array(
+			'title' => array()
+		),
+		'code'   => array(),
+		'pre'	=> array(),
+		'em'	 => array(),
+		'strong' => array(),
+		'div'	=> array(),
+		'p'	  => array(),
+		'ul'	 => array(),
+		'ol'	 => array(),
+		'li'	 => array(),
+		'h1'	 => array(),
+		'h2'	 => array(),
+		'h3'	 => array(),
+		'h4'	 => array(),
+		'h5'	 => array(),
+		'h6'	 => array(),
+		'img'	=> array(
+			'src'   => array(),
+			'class' => array(),
+			'alt'   => array()
+		)
+	);
+
+	$plugins_section_titles = array(
+		'description'  => _x('Description',  'Plugin installer section title'),
+		'installation' => _x('Installation', 'Plugin installer section title'),
+		'faq'		  => _x('FAQ',		  'Plugin installer section title'),
+		'changelog'	=> _x('Changelog',	'Plugin installer section title'),
+		'other_notes'  => _x('Other Notes',  'Plugin installer section title')
+	);
+
+	// No screenshots at this time
+	if ( !empty( $api->sections['screenshots'] ) )
+		unset( $api->sections['screenshots'] );
+	
+	// Sanitize HTML
+	foreach ( (array)$api->sections as $section_name => $content )
+		$api->sections[$section_name] = wp_kses( $content, $plugins_allowedtags );
+	foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) {
+		if ( isset( $api->$key ) )
+			$api->$key = wp_kses( $api->$key, $plugins_allowedtags );
+	}
+
+	// Default to the Description tab, Do not translate, API returns English.
+	$section = isset( $_REQUEST['section'] ) ? stripslashes( $_REQUEST['section'] ) : 'description';
+	if ( empty($section) || ! isset($api->sections[ $section ]) )
+		$section = array_shift( $section_titles = array_keys((array)$api->sections) );
+
+	iframe_header( __( 'Plugin Details' ) );
+	?>
+	<div id="<?php echo $tab; ?>-header">
+		<ul id="sidemenu">
+			<?php foreach ( (array) $api->sections as $section_name => $content ) : ?>
+				<?php				
+					if ( isset( $plugins_section_titles[ $section_name ] ) )
+						$title = $plugins_section_titles[ $section_name ];
+					else
+						$title = ucwords( str_replace( '_', ' ', $section_name ) );
+
+					$class = ( $section_name == $section ) ? ' class="current"' : '';
+					$href = add_query_arg( array('tab' => $tab, 'section' => $section_name) );
+					$href = esc_url($href);
+					$san_section = esc_attr( $section_name );
+				?>
+				<li><a name="<?php echo $san_section; ?>" href="<?php echo $href; ?>" <?php echo $class; ?>><?php echo $title; ?></a></li>
+			<?php endforeach ; ?>
+		</ul>
+	</div>
+	<div class="alignright fyi">
+		<h2 class="mainheader"><?php /* translators: For Your Information */ _e('FYI') ?></h2>
+		<ul>
+			<?php if ( !empty( $api->version ) ) : ?>
+				<li><strong><?php _e('Version:') ?></strong> <?php echo $api->version ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->author ) ) : ?>
+				<li><strong><?php _e('Author:') ?></strong> <?php echo $api->author ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->requires ) ) : ?>
+				<li><strong><?php _e('Requires WordPress Version:') ?></strong> <?php printf(__('%s or higher'), $api->requires) ?></li>
+			<?php endif; ?>
+			<?php if ( !empty( $api->tested ) ) : ?>
+				<li><strong><?php _e('Compatible up to:') ?></strong> <?php echo $api->tested ?></li>
+			<?php endif; ?>		
+			<?php if ( !empty( $api->homepage ) ) : ?>
+				<li><a target="_blank" href="<?php echo $api->homepage ?>"><?php _e('Plugin Homepage &#187;') ?></a></li>
+			<?php endif; ?>
+		</ul>
+	</div>
+	<div id="section-holder" class="wrap">
+	<?php
+		if ( !empty( $api->tested ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->tested ) ), $api->tested, '>' ) )
+			echo '<div class="updated"><p>' . __('<strong>Warning:</strong> This plugin has <strong>not been tested</strong> with your current version of WordPress.') . '</p></div>';
+
+		elseif ( !empty( $api->requires ) && version_compare( substr( $GLOBALS['wp_version'], 0, strlen( $api->requires ) ), $api->requires, '<' ) )
+			echo '<div class="updated"><p>' . __('<strong>Warning:</strong> This plugin has <strong>not been marked as compatible</strong> with your version of WordPress.') . '</p></div>';
+
+		foreach ( (array) $api->sections as $section_name => $content ) {
+			if ( isset( $plugins_section_titles[ $section_name ] ) )
+				$title = $plugins_section_titles[ $section_name ];
+			else
+				$title = ucwords( str_replace( '_', ' ', $section_name ) );
+
+			$content = links_add_target($content, '_blank');
+			$san_section = esc_attr( $section_name );
+			$display = ( $section_name == $section ) ? 'block' : 'none';
+			?>
+			<div id="section-<?php echo $san_section; ?>" class="section" style="display: <?php echo $display; ?>;">
+				<h2 class="long-header"><?php echo $title; ?></h2>
+				<?php echo $content; ?>
+			</div>
+			<?php
+		}
+	echo "</div>\n";
+	iframe_footer();
+	exit;
+}
+add_action('install_plugins_pre_plugin-readme', 'plugin_readme_information');
Index: wp-admin/includes/class-wp-plugin-install-list-table.php
===================================================================
--- wp-admin/includes/class-wp-plugin-install-list-table.php	(revision 21100)
+++ wp-admin/includes/class-wp-plugin-install-list-table.php	(working copy)
@@ -34,7 +34,7 @@
 		$tabs['popular']  = _x( 'Popular','Plugin Installer' );
 		$tabs['new']	  = _x( 'Newest','Plugin Installer' );
 
-		$nonmenu_tabs = array( 'plugin-information' ); //Valid actions to perform which do not have a Menu item.
+		$nonmenu_tabs = array( 'plugin-information', 'plugin-readme' ); //Valid actions to perform which do not have a Menu item.
 
 		$tabs = apply_filters( 'install_plugins_tabs', $tabs );
 		$nonmenu_tabs = apply_filters( 'install_plugins_nonmenu_tabs', $nonmenu_tabs );
Index: wp-admin/includes/class-wp-plugin-readme-parser.php
===================================================================
--- wp-admin/includes/class-wp-plugin-readme-parser.php	(revision 0)
+++ wp-admin/includes/class-wp-plugin-readme-parser.php	(working copy)
@@ -0,0 +1,464 @@
+<?php
+
+/**
+ * Parse a plugin's readme.txt file
+ * Based on http://code.svn.wordpress.org/plugin-readme-parser/parse-readme.php
+ * @link http://wordpress.org/extend/plugins/about/readme.txt
+ * @link http://wordpress.org/extend/plugins/about/validator/
+ * @pacakge WordPress
+ * @version 1.0
+ */
+class wp_plugin_readme_parser {
+
+		/**
+		 * Readme.txt file contents
+		 * This string will change with each pass in the process
+		 * @var string
+		 */
+		protected $_readme_contents = '';
+		
+		/**
+		 * Special section names
+		 * @var array
+		 */
+		protected $_special_sections = array(
+				'description',
+				'installation',
+				'frequently_asked_questions',
+				'screenshots',
+				'changelog',
+				'change_log',
+				'upgrade_notice'
+		);
+
+		/**
+		 * Minimum version of WordPress
+		 * @var string
+		 */
+		protected $_requires_at_least = '';
+
+		/**
+		 * Maximum compatible version of WordPress
+		 * @var string
+		 */
+		protected $_tested_up_to = '';
+
+		/**
+		 * Stable tag for the plugin
+		 * @var string
+		 */
+		protected $_stable_tag = '';
+
+		/**
+		 * Keywords for the plugin directory search
+		 * @var array
+		 */
+		protected $_tags = array();
+
+		/**
+		 * Authors who wrote the plugin
+		 * @var array
+		 */
+		protected $_contributors = array();
+
+		/**
+		 * Where to donate
+		 * @var string
+		 */
+		protected $_donate_link = '';
+
+		/**
+		 * License type
+		 * @var string
+		 */
+		protected $_license = '';
+
+		/**
+		 * Recognized keys for records
+		 * @var array
+		 */
+		protected $_keys = array(
+				'requires_at_least' => '/Requires at least:[ \t]*(.+)/i',
+				'tested_up_to'	  => '/Tested up to:[ \t]*(.+)/i',
+				'stable_tag'		=> '/Stable tag:[ \t]*(.+)/i',
+				'tags'			  => '/Tags:[ \t]*(.+)/i',
+				'contributors'	  => '/Contributors:[ \t]*(.+)/i',
+				'donate_link'	   => '/Donate link:[ \t]*(.+)/i',
+				'license'		   => '/License:[ \t]*(.+)/i'
+		);
+		
+		/**
+		 * Is the description a copy of the short description?
+		 * @var bool
+		 */
+		protected $_is_excerpt = false;
+
+		/**
+		 * Was the short description truncated to 150 characters?
+		 * @var bool
+		 */
+		protected $_is_truncated = false;
+
+		/**
+		 * Short description of the plugin
+		 * @var string
+		 */
+		protected $_short_description = '';
+
+		/**
+		 * List of the plugin's screenshots
+		 * @var array
+		 */
+		protected $_screenshots = array();
+		
+		/**
+		 * Content outside of the standard sections
+		 * @var string
+		 */
+		protected $_remaining_content = '';
+		
+		/**
+		 * Upgrade notices for users
+		 * Key = version, value = message.  Example:
+		 * 1.0 => Please upgrade
+		 * 1.1 => Dire security bug
+		 * 1.2 => Minor UI issue
+		 * @var array
+		 */
+		protected $_upgrade_notice = array();
+		
+		/**
+		 * Allowed tags in sections
+		 * @var array
+		 */
+		private $_allowed_tags = array(
+				'a'		  => array(
+						'href'	   => array(),
+						'title'	  => array(),
+						'rel'		=> array()
+				),
+				'blockquote' => array( 'cite' => array() ),
+				'br'		 => array(),
+				'cite'	   => array(),
+				'p'		  => array(),
+				'code'	   => array(),
+				'pre'		=> array(),
+				'em'		 => array(),
+				'strong'	 => array(),
+				'ul'		 => array(),
+				'ol'		 => array(),
+				'li'		 => array(),
+				'h3'		 => array(),
+				'h4'		 => array()
+		);
+
+		/**
+		 * Parse a plugin's readme.txt file.  Expects a path to the file.
+		 * @param string $file
+		 * @return string
+		 */
+		public function parse_readme_file ( $file ) {
+				if ( !file_exists( $file ) ) {
+						throw new Exception('File not found');
+				}
+				$this->_readme_contents = file_get_contents( $file );
+				return $this->_parse_readme();
+		}
+
+		/**
+		 * Parse a plugin's readme.txt.  Expect's the file contents as string.
+		 * @param type $file_contents
+		 * @return type 
+		 */
+		public function parse_readme_contents( $file_contents ) {
+				$this->_readme_contents = $file_contents;
+				return $this->_parse_readme();
+		}
+		
+		/**
+		 * Extract sections
+		 * @return array
+		 */
+		private function _exract_sections( ) {
+				$ret = array();
+
+				$sections = preg_split( '/^[\s]*==[\s]*(.+?)[\s]*==/m', $this->_readme_contents, -1, PREG_SPLIT_DELIM_CAPTURE|PREG_SPLIT_NO_EMPTY );
+
+				// Check if the first element is a short description
+				$this->_short_description = '';
+				$this->_is_truncated	  = false;
+				$this->_is_excerpt		= true;
+				if ( '==' != substr($sections[0][0], 0, 2) ) {
+						$short_description = array_shift( $sections );
+						
+						// If the user doesn't include a full description, this will be used, not truncated, and converted to markdown.  If they
+						// do include a full description, this field will be overwritten
+						$ret['description'] = $this->_markdown( $short_description );
+
+						$short_description = $this->_sanitize_text( $short_description );
+						if ( strlen( $short_description ) > 150 )
+								$this->_is_truncated = true;
+						$this->_short_description = substr( $short_description, 0, 150 );
+				}
+
+				// Sanitize titles / sections
+				for ( $i = 0 ; $i < count( $sections ) ; $i += 2 ) {
+						$title	= $this->_sanitize_text( $sections[$i] );
+						$contents = preg_replace( '/^[\s]*=[\s]+(.+?)[\s]+=/m', '<h4>$1</h4>', $sections[$i+1] );
+						$contents = $this->_markdown( $contents );
+						$ret[$title] = $contents;					   
+						if ( $this->_is_excerpt && 'description' == strtolower( strip_tags( $title ) ) )
+								$this->_is_excerpt = false;
+				}
+
+				// Remove the sections from the readme file
+				$this->_readme_contents = trim( str_replace( $sections, '', $this->_readme_contents ) );
+				
+				// Done
+				return $this->_process_sections( $ret );
+		}
+
+		/**
+		 * Post-process sections
+		 * Any special business logic (e.g. change_log -> changelog) is done here
+		 * @param array $sections
+		 * @return array
+		 */
+		private function _process_sections( $sections ) {
+				
+				// Rename sections to lower-case-underscore notation relegate non-special
+				// content to the "remaining content" section
+				$_sections = array();
+				$this->_remaining_content = '';
+				foreach ( (array) $sections as $k => $v ) {
+						$name = strtolower( preg_replace( '/[^a-zA-Z_]/', '_', $k ) );
+						$_sections[$name] = $v;
+
+						// Is this "remaining content" ?
+						if ( !in_array( $name, $this->_special_sections ) ) {
+								$title_id = esc_attr( $k );
+								$title_id = str_replace( ' ', '-', $title_id );
+								$this->_remaining_content .= sprintf("\n<h3 id=\"%s\">%s</h3>\n%s", $name, $k, $v);
+						}
+				}
+				$sections = $_sections;
+
+				// Upgrade notice section
+				$this->_upgrade_notice = array();
+				if ( array_key_exists( 'upgrade_notice', $sections ) ) {
+						$upgrade_notice = array();
+						$notices = preg_split( '/<h4>(.*?)<\/h4>/', $sections['upgrade_notice'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
+						if ( empty( $notices ) && count( $notices ) > 1 ) {
+								for ( $i = 0 ; $i < count( $notices ) ; $i += 2 )
+										$upgrade_notice[$notices[$i]] = substr( $this->_sanitize_text( @$notices[$i+1] ), 0, 300 );
+						} elseif ( is_array( $notices ) && 1 == count( $notices ) ) {
+								$upgrade_notice[ $this->_stable_tag ] = $notices[0];
+						}
+						$this->_upgrade_notice = $upgrade_notice;
+						unset( $sections['upgrade_notice'] );
+				}
+
+				// Screenshots
+				$this->_screenshots = array();
+				if ( array_key_exists( 'screenshots', $sections ) ) {
+						preg_match_all( '/<li>(.*?)<\/li>/s', $sections['screenshots'], $screenshots, PREG_SET_ORDER);
+						if ( $screenshots ) {
+								foreach ( (array) $screenshots as $screenshot )
+										$this->_screenshots[] = $screenshot[1];
+						}
+				}
+
+				// Fix change_log -> changelog
+				if ( array_key_exists( 'change_log', $sections ) )
+						$sections['changelog'] = $sections['change_log'];
+
+				return $sections;
+		}
+
+		/**
+		 * Post-process records
+		 * Any special business logic (e.g. splitting tags, contributors) is done here
+		 * @param array $records
+		 * @return array
+		 */
+		private function _process_records( $records ) {
+				
+				// Escape donate link URL
+				if ( !empty( $records['donate_link'] ) )
+						$records['donate_link'] = esc_url( $records['donate_link'] );
+				
+				// Split up tags
+				$tags = preg_split('/,[\s]*?/', trim( $records['tags'] ) );
+				foreach ( (array) $tags as $k => $v ) {
+						$v = $this->_sanitize_text( $v );
+						if ( !empty( $v ) )
+								$tags[$k] = $this->_sanitize_text( $v );
+						else
+								unset( $tags[$k] );
+				}
+				$records['tags'] = $tags;
+
+				// Split up contributors
+				$contributors = preg_split( '/,[\s]*/', trim( $records['contributors'] ) );
+				foreach ( (array) $contributors as $k => $v ) {
+						$v = $this->_sanitize_text( $v );
+						if ( !empty( $v ) )
+								$contributors[$k] = sanitize_user( $v );
+						else
+								unset( $contributors[$k] );
+				}
+				$records['contributors'] = $contributors;
+
+				// Assign records to object properties
+				foreach ( (array) $records as $k => $v )
+						if (in_array( $k , array_keys( $this->_keys ) ) )
+								$this->{"_$k"} = $v;
+
+				// Done
+				return $records;
+		}
+
+		/**
+		 * Extract key-value pairs
+		 * @return array
+		 */
+		private function _extract_records( ) {
+				$ret = array();
+				foreach ( (array) $this->_keys as $k => $v ) {
+						if ( preg_match( $v, $this->_readme_contents, $matches ) ) {
+								$ret[$k] = $this->_sanitize_text( $matches[1] );
+								$this->_readme_contents = trim( str_replace( $matches[0], '', $this->_readme_contents ) );
+						}
+				}
+				return $this->_process_records( $ret );
+		}
+		
+		/**
+		 * Parse a readme file into an associative array
+		 * @return string
+		 */
+		protected function _parse_readme() {
+
+				// Normalize white-space
+				$file_contents = str_replace( array( "\r\n", "\r" ), "\n", $this->_readme_contents );
+				$this->_readme_contents = trim( $this->_readme_contents );
+
+				// Remove UTF-8 byte-order-mark
+				if ( 0 === strpos( $this->_readme_contents, "\xEF\xBB\xBF" ) )
+						$this->_readme_contents = substr( $this->_readme_contents, 3 );
+
+				// === Plugin Name ===
+				// Must be the very first thing.				
+				if ( !preg_match( '/^===(.*)===/', $this->_readme_contents, $_name ) )
+						return array(); // require a name
+				$name = trim( $_name[1], '=' );
+				$name = $this->_sanitize_text( $name );
+				$this->_readme_contents = str_replace( $_name[0], '', $this->_readme_contents );
+				
+				// Extract the "key: value" records to the local scope
+				extract( $this->_extract_records() );
+				
+				// Extract the sections (e.g. == HEADING == .... text ...)
+				$sections = $this->_exract_sections();
+				extract( $sections );
+				
+				// Compile the final array
+				return array(
+						'name'			  => isset( $name )			  ? $name			  : '',
+						'tags'			  => isset( $tags )			  ? $tags			  : array(),
+						'requires_at_least' => isset( $requires_at_least ) ? $requires_at_least : '',
+						'tested_up_to'	  => isset( $tested_up_to )	  ? $tested_up_to	  : '',
+						'stable_tag'		=> isset( $stable_tag )		? $stable_tag		: 'trunk',
+						'contributors'	  => isset( $contributors )	  ? $contributors	  : array(),
+						'donate_link'	   => isset( $donate_link )	   ? $donate_link	   : '',
+						'short_description' => $this->_short_description,
+						'screenshots'	   => $this->_screenshots,
+						'is_excerpt'		=> $this->_is_excerpt,
+						'is_truncated'	  => $this->_is_truncated,
+						'sections'		  => $sections,
+						'remaining_content' => $this->_remaining_content,
+						'upgrade_notice'	=> $this->_upgrade_notice
+				);
+		}
+
+		/**
+		 * Make the text safe for use in a browser
+		 * @param string $text
+		 * @return string
+		 */
+		private function _sanitize_text( $text ) {
+				$text = strip_tags( $text );
+				$text = esc_html( $text );
+				$text = trim( $text );
+				return $text;
+		}
+
+		/**
+		 * Format text.  Use markdown, with some extra code -> backtick formatting.
+		 * @param type $text
+		 * @return type 
+		 */
+		private function _markdown( $text ) {
+				$text = trim( $text );
+				$text = $this->_convert_code( $text );
+				$text = Markdown( $text );			  
+				$text = balanceTags( $text );
+				$text = wp_kses( $text, $this->_allowed_tags );
+				$text = trim( $text );
+				return $text;
+		}
+
+		/**
+		 * First take any user formatted code blocks and turn them into backticks so that
+		 * markdown will preserve things like underscores in code blocks
+		 * @param type $text
+		 * @param type $markdown
+		 * @return type 
+		 */
+		private function _convert_code( $text ) {
+				$text = preg_replace_callback( '/(<pre><code>|<code>)(.*?)(<\/code>\/pre>|<\/code>)/s', array( $this, '_decodeit' ), $text );
+				$text = str_replace( array("\r\n", "\r"), "\n", $text );
+				// Markdown seems to be choking on block level stuff too.  Let's just encode it and be done with it.
+				$text = preg_replace_callback( '/(^|\n)`(.*?)`/s', array( $this, '_encodeit' ), $text );
+				return $text;
+		}
+		
+		/**
+		 * Encode Markdown to HTML
+		 * @param array $matches Output from preg_replace_callback
+		 * @return string 
+		 */
+		private function _encodeit( $matches ) {
+				$text = trim( $matches[2] );
+				$text = htmlspecialchars( $text, ENT_QUOTES );
+				$text = str_replace( array( "\r\n", "\r" ), "\n", $text );
+				$text = preg_replace( "|\n\n\n+|", "\n\n", $text );
+				$text = str_replace(
+						array( '&amp;lt;', '&amp;&gt; ' ),
+						array( '&lt;'	, '&gt;'	   ),
+						$text
+				);
+				$text = "<code>$text</code>";
+				if ( "`" != $matches[1] )
+						$text = "<pre>$text</pre>";
+				return $text;
+		}
+
+		/**
+		 * De-code HTML, turn it back into markdown
+		 * @param array $matches Output from preg_replace_callback
+		 * @return string 
+		 */
+		private function _decodeit( $matches ) {
+				$text = $matches[2];
+				$text = html_entity_decode( $text );
+				$text = str_replace(
+						array( '<br />', '&#38;', '&#39;'),
+						array( ''	  , '&'	, "'"	),
+						$text
+				);			  
+				if ( '<pre><code>' == $matches[1] )
+						$text = "\n$text\n";
+				return "`$text`";
+		}
+}
