<?php
/**
 * Handles API calls to BitBucket
 * 
 * @since 2.5.4
 * 
 * @package LearnDash
 */


if ( !class_exists( 'LearnDash_BitBucket_API' ) ) {
	class LearnDash_BitBucket_API {

		// Production
		private $bb_OAuth = null;
				
		private $bb_OAuth_sets = array(
			'fUEfEERS8FdprEzNMX' => 'gnBPjny5yummKe6zGv5MMCa5w9tDGstT',		// ld_updates
			'32uKmTweF7WGUgXC2H' => 'qmUveCQvaZXD9LmAE8GFAd6a6H5SMk6U',		// ld_updates2
			'P2xXBphWPZP8Cbr8Rw' => 'qFvD9zrt9RVVpEJ6j2jEWjZJEgvhpzPZ', 	// ld_updates3
			'8tvevj3YQQnyFJE5a2' => 'C5BuSGZBanNbmaEE7hjeQEnmPLwma3JE', 	// ld_updates4
			'2LdbzTV4u5UhfTPS8t' => 'Y5eMpRVnFAWkvb3dmZuq64jTzNU9x93c', 	// ld_updates5
			'jYyJyBctJ6wkxVVk7h' => 'NeKUN87Npyu8tGwN4eLCh5ZFjjazdu4e',		// ld_updates6
			'bn6Phmn3jVyQ2NfdPa' => 'Ct7MbLpuCyr56s8qvy6Eh7VteBEGshDp',		// ld_updates7
			'bTtudTumX8jQ56aJxc' => 'czwT3Cr75sBDTkDzGzwExwDhHcH6wVbL',		// ld_updates8
			'uAPE6cvgWjbHdEYpJg' => 'butXFuD3QNbX3uXrwf49J8vvDNMsPbpA',		// ld_updates9
			'P6xDsLKNTxGdSfUpnC' => 'XFk6PVJCu5bHmcJB7WN3v5afzEsyqAxL',		// ld_updates10

			'YUKnZucTb2pJchuDZS' => 'W6n5vBVz4vsvF4GQ7xu4zKA2JkA2DVHm',		// ld_updates11
			'YFfyNFaLKtU6cMm9c4' => 'WAnwBKqsj4r2YMU2zMVYSLwqDCt8hc4s',		// ld_updates12
			'asHX3Me2k68jf4ALms' => 'EMR6rEtFv8HKfqKhrzkwmYK6knkGTsq6',		// ld_updates13
			'J94b6KjjBaZDG8Awau' => '9b7nMpTSFBXR7mbc7rmF2hFZduRptjaW',		// ld_updates14
			'VGXFPJcysa83QWgGqY' => 'VUYEtkDG6L3jVCmJARyzRYTZpeWckD8B',		// ld_updates15
			'tKqG3xaLt5dawwLYJu' => 'AEdAJEyHUP9UuTEJ38E36rapvgHG9z3B',		// ld_updates16
			'TxNgg8AkcyhQDLmAaA' => 'usAw3tGPWW6k43KmdqKxZcLRbaVyMWvj',		// ld_updates17
			'r62wGrYYDGz2kTMjcN' => 'C4CWjLFeM2WaJg6egVJcFQK6XZSmZjgZ',		// ld_updates18
			'epKULsTE7DqP5SsJFw' => 'HjYQ8G5Npekq92Xf6vrbt8b79su8r2wb',		// ld_updates19
			'APfwmF6D4VaeAxLprB' => 'U7hyxFwX27mWYzgE5x5DzjHyCaCfqFK4',		// ld_updates20
		);
				
		private $request_method = 'GET';
		private $repo_url_base = 'https://api.bitbucket.org/2.0/repositories/learndash';
		private $download_url_base = 'https://bitbucket.org/learndash';

		function __construct() {
		}

		function init_oauth_key_set() {
			if ( empty( $this->bb_OAuth ) ) {
				$set_key = array_rand( $this->bb_OAuth_sets, 1 );
				if ( !empty( $set_key ) ) {
					$this->bb_OAuth = array(
						'consumer_key' => $set_key,
						'consumer_secret' => $this->bb_OAuth_sets[$set_key],
					);
				}
			}
		}
		
		function get_bb_nonce() {
			$mt = microtime();
			$rand = mt_rand();
	
			return md5($mt . '_' . $rand);
		}
		
		function get_repo_base_url() {
			return $this->repo_url_base;
		}

		function get_download_base_url() {
			return $this->download_url_base;
		}

		function get_readme_base_url() {
			return $this->readme_url_base;
		}
		
		function setup_url_params( $request_url = '' ) {
			if ( !empty( $request_url ) ) {
				$this->init_oauth_key_set();
				
				$request_url_params = array(
					'oauth_consumer_key' 		=> $this->bb_OAuth['consumer_key'],
					'oauth_nonce' 				=> $this->get_bb_nonce(),
					'oauth_signature_method' 	=> 'HMAC-SHA1',
					'oauth_timestamp' 			=> time(),
					'oauth_version' 			=> '1.0',
					'pagelen'					=> 100
				);
				ksort( $request_url_params );
			
				//The most complicated part of the request - generating the signature.
				//The string to sign contains the HTTP method, the URL path, and all of
				//our query parameters. Everything is URL encoded. Then we concatenate
				//them with ampersands into a single string to hash.
				$encodedVerb = urlencode( $this->request_method );
				$encodedUrl = urlencode( $request_url );
				$encodedParams = urlencode( http_build_query( $request_url_params, '', '&' ) );

				$stringToSign = $encodedVerb . '&' . $encodedUrl . '&' . $encodedParams;

				//Since we only have one OAuth token (the consumer secret) we only have
				//to use it as our HMAC key. However, we still have to append an & to it
				//as if we were using it with additional tokens.
				$secret = urlencode( $this->bb_OAuth['consumer_secret'] ) . '&';

				//The signature is a hash of the consumer key and the base string. Note
				//that we have to get the raw output from hash_hmac and base64 encode
				//the binary data result.
				$request_url_params['oauth_signature'] = base64_encode( hash_hmac('sha1', $stringToSign, $secret, true ) );

				$request_url .= '?' . http_build_query( $request_url_params );
			}
			return $request_url;
		}
		
		function get_bitbucket_repositories() {
			// Clear out the existiing plugins array.
			$repository_data = array();
						
			$request_url = $this->repo_url_base .'/learndash-add-ons/src/master/repositories.txt';
			
			$request_url = $this->setup_url_params( $request_url );
			if ( !empty( $request_url ) ) {
				$options = array('timeout' => LEARNDASH_HTTP_REMOTE_GET_TIMEOUT);
				$response = wp_remote_get( $request_url, $options );
				if ( is_wp_error( $response ) ) {
					return $response;
				}

				$code = wp_remote_retrieve_response_code( $response );
				
				$body = wp_remote_retrieve_body( $response );
				if ( ( $code === 200 ) && ( !empty( $body ) ) ) {
					$repository_data = $this->parse_repository_txt( $body );
				}
			}
			
			return $repository_data;
		}

		function get_bitbucket_repositories_ORG() {
			// Clear out the existiing plugins array.
			$repository_data = array();
						
			$request_url = $this->repo_url_base;			
			$request_url = $this->setup_url_params( $request_url );
			if ( !empty( $request_url ) ) {
				$options = array('timeout' => LEARNDASH_HTTP_REMOTE_GET_TIMEOUT);
				$response = wp_remote_get( $request_url, $options );
				if ( is_wp_error( $response ) ) {
					return $response;
				}

				$code = wp_remote_retrieve_response_code( $response );				
				$body = wp_remote_retrieve_body( $response );
				if ( ( $code === 200 ) && ( !empty( $body ) ) ) {
					$repositories = json_decode( $body );
					if ( ( property_exists( $repositories, 'values' ) ) && ( is_array( $repositories->values ) ) && ( !empty( $repositories->values ) ) ) {
						
						foreach( $repositories->values as $plugin ) {
							if ( property_exists( $plugin, 'slug' ) ) {
								//if ( in_array( $plugin->slug, array( 'plugin-test-1', 'plugin-test-2' ) ) ) {
									$repository_data[$plugin->slug] = $plugin;	
								//}
							}
						}
					}
				}
			}

			return $repository_data;
		}

		// The following function is used to pull the readme.txt file from BitBucket. 
		// But this was too costly from a connection standpoint. Plus made editing difficult
		// since a typo or quick change could not be applied to a new update. 
		function get_bitbucket_repository_readme( $plugin_key = '' ) {
			if ( !empty( $plugin_key ) ) {
				$plugin_readme_file = ABSPATH . DIRECTORY_SEPARATOR . $plugin_key . '.txt';
				if ( file_exists( $plugin_readme_file ) ) {
					$code = 200;
					$body = file_get_contents( $plugin_readme_file );
				} else {
					$request_url = $this->repo_url_base .'/'. $plugin_key ."/src/master/readme.txt";
			
					$request_url = $this->setup_url_params( $request_url );
					if ( !empty( $request_url ) ) {
						//$request_url = 'http://local.learndash.com/_readme.txt';

						$options = array('timeout' => LEARNDASH_HTTP_REMOTE_GET_TIMEOUT);
				
						$response = wp_remote_get( $request_url, $options );
						if ( is_wp_error( $response ) ) {
							return $response;
						}

						$code = wp_remote_retrieve_response_code( $response );
						
						$body = wp_remote_retrieve_body( $response );
					}
				}
				if ( ( $code === 200 ) && ( !empty( $body ) ) ) {
					$readme_parser = new LearnDashWPReadmeParser();
					$body_parsed = $readme_parser->parse_readme_contents( $body );

					return $body_parsed;
				}
			}
		}

		// Retreive the readme.txt file from Amazon S3.
		// Filenamess should be in the form format <repo slug>_readme.txt
		// as in learndash-gravity-forms_readme.txt or learndash-edd_readme.txt
		function get_bitbucket_repository_readme_S3( $plugin_key = '' ) {
			if ( !empty( $plugin_key ) ) {
				$request_url = $this->readme_url_base .'/'. $plugin_key ."_readme.txt";
			
				$options = array('timeout' => LEARNDASH_HTTP_REMOTE_GET_TIMEOUT);
		
				$response = wp_remote_get( $request_url, $options );
				if ( is_wp_error( $response ) ) {
					return $response;
				}

				$code = wp_remote_retrieve_response_code( $response );
				$body = wp_remote_retrieve_body( $response );
				if ( ( $code === 200 ) && ( !empty( $body ) ) ) {
					$readme_parser = new LearnDashWPReadmeParser();
					$body_parsed = $readme_parser->parse_readme_contents( $body );

					return $body_parsed;
				}
			}
		}
		
		function get_bitbucket_repository_download_url( $plugin_key = '', $tag_or_version = 'stable' ) {
			if ( !empty( $plugin_key ) ) {
				$request_url = $this->download_url_base ."/". $plugin_key ."/get/". $tag_or_version .".zip";
				return $request_url;
			}
		}
		
		/* The calling function get_bitbucket_repositories() connects to bitbucket and retreives
		 * a file respoitories.txt. Each line of the file represents a repository and contains 
		 * three fields separated by '|'. 
		 * 1. repository slug
		 * 2. current version
		 * 3. last update YYYY-MM-DD hh:mm:ss
		 * 
		 * @ since 2.5.7
		 */
		function parse_repository_txt( $file_contents = '' ) {
			$repositories_array = array();
			
			$file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
			$file_contents = trim($file_contents);
			$file_contents_array = preg_split("/(\r\n|\n|\r)/", $file_contents );
			if ( !empty( $file_contents_array ) ) {
				foreach( $file_contents_array as $repo_item_string ) {
					if ( !empty( $repo_item_string ) ) {
						list( $tmp_array['slug'], $tmp_array['version'], $tmp_array['updated_on'] ) = explode( '|', $repo_item_string );
						$tmp_array = array_map( 'trim', $tmp_array );
						if ( ( isset( $tmp_array['slug'] ) ) && ( !empty( $tmp_array['slug'] ) ) ) {
							$repositories_array[$tmp_array['slug']] = (object)$tmp_array;
						}
					}
				}
			}

			return $repositories_array;
		}
	}
}

if ( !class_exists( 'LearnDashWPReadmeParser' ) ) {

	class LearnDashWPReadmeParser {

		function __construct() {
				// This space intentially blank
			}

			function parse_readme( $file ) {
				$file_contents = @implode('', @file($file));
				return $this->parse_readme_contents( $file_contents );
			}

			function parse_readme_contents( $file_contents ) {
				global $wpdb;

				$readme_sections = array();
				
				if ( ! empty( $file_contents ) ) {
					$file_contents = $wpdb->strip_invalid_text_for_column( $wpdb->options, 'option_value', $file_contents );
				}

				if ( ! empty( $file_contents ) ) {
					$file_contents = str_replace(array("\r\n", "\r"), "\n", $file_contents);
					$file_contents = trim($file_contents);
					if ( 0 === strpos( $file_contents, "\xEF\xBB\xBF" ) )
						$file_contents = substr( $file_contents, 3 );

					// Markdown transformations
					$file_contents = preg_replace( "|^###([^#]+)#*?\s*?\n|im", '=$1='."\n",     $file_contents );
					$file_contents = preg_replace( "|^##([^#]+)#*?\s*?\n|im",  '==$1=='."\n",   $file_contents );
					$file_contents = preg_replace( "|^#([^#]+)#*?\s*?\n|im",   '===$1==='."\n", $file_contents );
				

					$file_contents_array = preg_split("/(\r\n|\n|\r)/", $file_contents );
					if ( !empty( $file_contents_array ) ) {
						$section_current = '';
						$_is_short_description = false;
						foreach( $file_contents_array as $line ) {
							if ( substr( $line, 0, 3 ) == '===' ) {
								$section_current = 'header';
								if ( preg_match('|^===(.*)===|', $line, $_name) ) {
									$readme_sections['name'] = $this->sanitize_text( trim( $_name[1] ) );
								}
								continue;
								
							} else if ( substr( $line, 0, 2 ) == '==' ) {
								$section_current = '';
								if ( preg_match('|^==(.*)==|', $line, $_name) ) {
									$title = $this->sanitize_text( trim( $_name[1] ) );
									$section_current = str_replace(' ', '_', strtolower( $title ) );
									$readme_sections[$section_current] = array('title' => $title, 'content_raw' => '', 'content' => '');
									continue;
								}
							}
							
							if ( !empty( $section_current ) ) {
								if ( $section_current == 'header' ) {
									if ( !empty( $line ) ) {
										if ( $_is_short_description == true ) {
											$short_desc_filtered = $this->sanitize_text( $line );
											$short_desc_length = strlen($short_desc_filtered);
											$short_description = substr($short_desc_filtered, 0, 150);
											$readme_sections['short_description'] = $short_description;
												
											if ( $short_desc_length > strlen($short_description) )
												$truncated = true;
											else
												$truncated = false;
											$readme_sections['is_truncated'] = $truncated;
											
										} else {
											$line_parts = explode(':', $line, 2 );
											if ( count( $line_parts ) > 1 ) {
												$title = $this->sanitize_text( trim( $line_parts[0] ) );
												$title = str_replace(' ', '_', strtolower($title));
												$readme_sections[$title] = 	trim( $line_parts[1] );
											}
											
										}
									} else {
										$_is_short_description = true;
									}
								} else if ( isset( $readme_sections[$section_current]['content_raw'] ) ) {
									if ( ( empty( $line ) ) && ( empty( $readme_sections[$section_current]['content_raw'] ) ) )
										continue;
									else	
										$readme_sections[$section_current]['content_raw'] .= $line ."\r\n"; 
								}
							}
						}
					}
					
					if ( ( isset( $readme_sections['tags'] ) ) && ( !empty( $readme_sections['tags'] ) ) ) {
						$tags_str = $readme_sections['tags'];
						$readme_sections['tags'] = array();
						
						$tags_array = preg_split('|,[\s]*?|', trim( $tags_str ) );
						if ( count( $tags_array ) > 1 ) {
							//foreach ( array_keys( $tags_array ) as $t ) {
							//	$readme_sections['tags'][$t] = $this->sanitize_text( $tags_array[$t] );
							//}
							$readme_sections['tags'] = array_map( array( $this, 'sanitize_text'), $tags_array );
							
						} 
					} else {
						$readme_sections['tags'] = array();
					}

					/*
					if ( ( isset( $readme_sections['contributors'] ) ) && ( !empty( $readme_sections['contributors'] ) ) ) {
						$contributors_str = $readme_sections['contributors'];
						$readme_sections['contributors'] = array();
						
						$contributors_array = preg_split('|,[\s]*?|', trim( $contributors_str ) );
						if ( count( $contributors_array ) > 1 ) {
							//foreach ( array_keys( $contributors_array ) as $t ) {
							//	$readme_sections['contributors'][$t] = $this->user_sanitize( $contributors_array[$t] );
							//}
							$readme_sections['contributors'] = array_map( array( $this, 'user_sanitize'), $contributors_array );
						} 
					} else {
					*/
						$readme_sections['contributors'] = array();
					//}
					
					foreach( array( 'changelog', 'upgrade_notice', 'upgrade_notice_admin' ) as $section_key ) {
						if ( ( isset( $readme_sections[$section_key]['content_raw'] ) ) && ( !empty( $readme_sections[$section_key]['content_raw'] ) ) ) {
							$split = preg_split( '#=(.*?)=#', $readme_sections[$section_key]['content_raw'], -1, PREG_SPLIT_DELIM_CAPTURE | PREG_SPLIT_NO_EMPTY );
							$readme_sections[$section_key]['content'] = array();
							for ( $i = 0; $i < count( $split ); $i += 2 ) {
								$readme_sections[$section_key]['content'][$this->sanitize_text( $split[$i] )] = $this->filter_text( $this->sanitize_text( $split[$i + 1] ), true );
							}
						}
					}

					foreach( array( 'icons', 'banners' ) as $section_key ) {
						if ( ( isset( $readme_sections[$section_key]['content_raw'] ) ) && ( !empty( $readme_sections[$section_key]['content_raw'] ) ) ) {
							$items = explode( PHP_EOL, $readme_sections[$section_key]['content_raw'] );
							if ( !empty( $items ) ) {
								$readme_sections[$section_key] = array();
								foreach( $items as $item ) {
									$item = trim( $item );
									if ( !empty( $item ) ) {
										$item_split = explode( ':', $item, 2 );
										$readme_sections[$section_key][$item_split[0]] = $item_split[1];
									}
								}
							}
						}
					}

					
					$readme_sections['sections'] = array();
									
					foreach( array( 'description', 'installation', 'frequently_asked_questions', 'changelog', 'arbitrary_section' ) as $section_key ) {
						if ( ( isset( $readme_sections[$section_key]['content_raw'] ) ) && ( !empty( $readme_sections[$section_key]['content_raw'] ) ) ) {
							$readme_sections[$section_key]['content'] = preg_replace('/^[\s]*=[\s]+(.+?)[\s]+=/m', '<h4>$1</h4>', $readme_sections[$section_key]['content_raw'] );
							$readme_sections[$section_key]['content'] = $this->filter_text( $readme_sections[$section_key]['content'], true );
							
							$readme_sections['sections'][$section_key] = $readme_sections[$section_key]['content'];
						}
					}
				}	
				return $readme_sections;
			}

			function sanitize_text( $text ) { // not fancy
				$text = strip_tags($text);
				$text = esc_html($text);
				$text = trim($text);
				return $text;
			}

			function filter_text( $text, $markdown = false ) { // fancy, Markdown
				$text = trim($text);

			        $text = call_user_func( array( __CLASS__, 'code_trick' ), $text, $markdown ); // A better parser than Markdown's for: backticks -> CODE

				//if ( $markdown ) { // Parse markdown.
				//	if ( !function_exists('Markdown') )
				//		require( WORDPRESS_README_MARKDOWN );
				//	$text = Markdown($text);
				//}
				if ( $markdown ) { // Parse markdown.
					//if ( !class_exists('LeanDashParsedown', false) ) {
					//	/** @noinspection PhpIncludeInspection */
					//	require_once(dirname(__FILE__) . '/Parsedown' . (version_compare(PHP_VERSION, '5.3.0', '>=') ? '' : 'Legacy') . '.php');
					//}
					$instance = LeanDashParsedown::instance();
					$text = $instance->text($text);
				}
				

				$allowed = array(
					'a' => array(
						'href' => array(),
						'title' => array(),
						'rel' => array()),
					'blockquote' => array('cite' => array()),
					'br' => array(),
					'p' => array(),
					'code' => array(),
					'pre' => array(),
					'em' => array(),
					'strong' => array(),
					'ul' => array(),
					'ol' => array(),
					'li' => array(),
					'h3' => array(),
					'h4' => array()
				);

				$text = balanceTags($text);
		
				$text = wp_kses( $text, $allowed );
				$text = trim($text);
				return $text;
			}

			function code_trick( $text, $markdown ) { // Don't use bbPress native function - it's incompatible with Markdown
				// If doing markdown, first take any user formatted code blocks and turn them into backticks so that
				// markdown will preserve things like underscores in code blocks
				if ( $markdown )
					$text = preg_replace_callback("!(<pre><code>|<code>)(.*?)(</code></pre>|</code>)!s", array( __CLASS__,'decodeit'), $text);

				$text = str_replace(array("\r\n", "\r"), "\n", $text);
				if ( !$markdown ) {
					// This gets the "inline" code blocks, but can't be used with Markdown.
					$text = preg_replace_callback("|(`)(.*?)`|", array( __CLASS__, 'encodeit'), $text);
					// This gets the "block level" code blocks and converts them to PRE CODE
					$text = preg_replace_callback("!(^|\n)`(.*?)`!s", array( __CLASS__, 'encodeit'), $text);
				} else {
					// Markdown can do inline code, we convert bbPress style block level code to Markdown style
					$text = preg_replace_callback("!(^|\n)([ \t]*?)`(.*?)`!s", array( __CLASS__, 'indent'), $text);
				}
				return $text;
			}

			function indent( $matches ) {
				$text = $matches[3];
				$text = preg_replace('|^|m', $matches[2] . '    ', $text);
				return $matches[1] . $text;
			}

			function encodeit( $matches ) {
				if ( function_exists('encodeit') ) // bbPress native
					return 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('&amp;lt;', '&lt;', $text);
				$text = str_replace('&amp;gt;', '&gt;', $text);
				$text = "<code>$text</code>";
				if ( "`" != $matches[1] )
					$text = "<pre>$text</pre>";
				return $text;
			}

			function decodeit( $matches ) {
				if ( function_exists('decodeit') ) // bbPress native
					return decodeit( $matches );

				$text = $matches[2];
				$trans_table = array_flip(get_html_translation_table(HTML_ENTITIES));
				$text = strtr($text, $trans_table);
				$text = str_replace('<br />', '', $text);
				$text = str_replace('&#38;', '&', $text);
				$text = str_replace('&#39;', "'", $text);
				if ( '<pre><code>' == $matches[1] )
					$text = "\n$text\n";
				return "`$text`";
			}
		}		
	#
	#
	# LeanDashParsedown
	# http://parsedown.org
	#
	# (c) Emanuil Rusev
	# http://erusev.com
	#
	# For the full license information, view the LICENSE file that was distributed
	# with this source code.
	#
	#

	class LeanDashParsedown
	{
	    # ~

	    const version = '1.6.0';

	    # ~

	    function text($text)
	    {
	        # make sure no definitions are set
	        $this->DefinitionData = array();

	        # standardize line breaks
	        $text = str_replace(array("\r\n", "\r"), "\n", $text);

	        # remove surrounding line breaks
	        $text = trim($text, "\n");

	        # split text into lines
	        $lines = explode("\n", $text);

	        # iterate through lines to identify blocks
	        $markup = $this->lines($lines);

	        # trim line breaks
	        $markup = trim($markup, "\n");

	        return $markup;
	    }

	    #
	    # Setters
	    #

	    function setBreaksEnabled($breaksEnabled)
	    {
	        $this->breaksEnabled = $breaksEnabled;

	        return $this;
	    }

	    protected $breaksEnabled;

	    function setMarkupEscaped($markupEscaped)
	    {
	        $this->markupEscaped = $markupEscaped;

	        return $this;
	    }

	    protected $markupEscaped;

	    function setUrlsLinked($urlsLinked)
	    {
	        $this->urlsLinked = $urlsLinked;

	        return $this;
	    }

	    protected $urlsLinked = true;

	    #
	    # Lines
	    #

	    protected $BlockTypes = array(
	        '#' => array('Header'),
	        '*' => array('Rule', 'List'),
	        '+' => array('List'),
	        '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
	        '0' => array('List'),
	        '1' => array('List'),
	        '2' => array('List'),
	        '3' => array('List'),
	        '4' => array('List'),
	        '5' => array('List'),
	        '6' => array('List'),
	        '7' => array('List'),
	        '8' => array('List'),
	        '9' => array('List'),
	        ':' => array('Table'),
	        '<' => array('Comment', 'Markup'),
	        '=' => array('SetextHeader'),
	        '>' => array('Quote'),
	        '[' => array('Reference'),
	        '_' => array('Rule'),
	        '`' => array('FencedCode'),
	        '|' => array('Table'),
	        '~' => array('FencedCode'),
	    );

	    # ~

	    protected $unmarkedBlockTypes = array(
	        'Code',
	    );

	    #
	    # Blocks
	    #

	    protected function lines(array $lines)
	    {
	        $CurrentBlock = null;

	        foreach ($lines as $line)
	        {
	            if (chop($line) === '')
	            {
	                if (isset($CurrentBlock))
	                {
	                    $CurrentBlock['interrupted'] = true;
	                }

	                continue;
	            }

	            if (strpos($line, "\t") !== false)
	            {
	                $parts = explode("\t", $line);

	                $line = $parts[0];

	                unset($parts[0]);

	                foreach ($parts as $part)
	                {
	                    $shortage = 4 - mb_strlen($line, 'utf-8') % 4;

	                    $line .= str_repeat(' ', $shortage);
	                    $line .= $part;
	                }
	            }

	            $indent = 0;

	            while (isset($line[$indent]) and $line[$indent] === ' ')
	            {
	                $indent ++;
	            }

	            $text = $indent > 0 ? substr($line, $indent) : $line;

	            # ~

	            $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);

	            # ~

	            if (isset($CurrentBlock['continuable']))
	            {
	                $Block = $this->{'block'.$CurrentBlock['type'].'Continue'}($Line, $CurrentBlock);

	                if (isset($Block))
	                {
	                    $CurrentBlock = $Block;

	                    continue;
	                }
	                else
	                {
	                    if ($this->isBlockCompletable($CurrentBlock['type']))
	                    {
	                        $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
	                    }
	                }
	            }

	            # ~

	            $marker = $text[0];

	            # ~

	            $blockTypes = $this->unmarkedBlockTypes;

	            if (isset($this->BlockTypes[$marker]))
	            {
	                foreach ($this->BlockTypes[$marker] as $blockType)
	                {
	                    $blockTypes []= $blockType;
	                }
	            }

	            #
	            # ~

	            foreach ($blockTypes as $blockType)
	            {
	                $Block = $this->{'block'.$blockType}($Line, $CurrentBlock);

	                if (isset($Block))
	                {
	                    $Block['type'] = $blockType;

	                    if ( ! isset($Block['identified']))
	                    {
	                        $Blocks []= $CurrentBlock;

	                        $Block['identified'] = true;
	                    }

	                    if ($this->isBlockContinuable($blockType))
	                    {
	                        $Block['continuable'] = true;
	                    }

	                    $CurrentBlock = $Block;

	                    continue 2;
	                }
	            }

	            # ~

	            if (isset($CurrentBlock) and ! isset($CurrentBlock['type']) and ! isset($CurrentBlock['interrupted']))
	            {
	                $CurrentBlock['element']['text'] .= "\n".$text;
	            }
	            else
	            {
	                $Blocks []= $CurrentBlock;

	                $CurrentBlock = $this->paragraph($Line);

	                $CurrentBlock['identified'] = true;
	            }
	        }

	        # ~

	        if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
	        {
	            $CurrentBlock = $this->{'block'.$CurrentBlock['type'].'Complete'}($CurrentBlock);
	        }

	        # ~

	        $Blocks []= $CurrentBlock;

	        unset($Blocks[0]);

	        # ~

	        $markup = '';

	        foreach ($Blocks as $Block)
	        {
	            if (isset($Block['hidden']))
	            {
	                continue;
	            }

	            $markup .= "\n";
	            $markup .= isset($Block['markup']) ? $Block['markup'] : $this->element($Block['element']);
	        }

	        $markup .= "\n";

	        # ~

	        return $markup;
	    }

	    protected function isBlockContinuable($Type)
	    {
	        return method_exists($this, 'block'.$Type.'Continue');
	    }

	    protected function isBlockCompletable($Type)
	    {
	        return method_exists($this, 'block'.$Type.'Complete');
	    }

	    #
	    # Code

	    protected function blockCode($Line, $Block = null)
	    {
	        if (isset($Block) and ! isset($Block['type']) and ! isset($Block['interrupted']))
	        {
	            return;
	        }

	        if ($Line['indent'] >= 4)
	        {
	            $text = substr($Line['body'], 4);

	            $Block = array(
	                'element' => array(
	                    'name' => 'pre',
	                    'handler' => 'element',
	                    'text' => array(
	                        'name' => 'code',
	                        'text' => $text,
	                    ),
	                ),
	            );

	            return $Block;
	        }
	    }

	    protected function blockCodeContinue($Line, $Block)
	    {
	        if ($Line['indent'] >= 4)
	        {
	            if (isset($Block['interrupted']))
	            {
	                $Block['element']['text']['text'] .= "\n";

	                unset($Block['interrupted']);
	            }

	            $Block['element']['text']['text'] .= "\n";

	            $text = substr($Line['body'], 4);

	            $Block['element']['text']['text'] .= $text;

	            return $Block;
	        }
	    }

	    protected function blockCodeComplete($Block)
	    {
	        $text = $Block['element']['text']['text'];

	        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');

	        $Block['element']['text']['text'] = $text;

	        return $Block;
	    }

	    #
	    # Comment

	    protected function blockComment($Line)
	    {
	        if ($this->markupEscaped)
	        {
	            return;
	        }

	        if (isset($Line['text'][3]) and $Line['text'][3] === '-' and $Line['text'][2] === '-' and $Line['text'][1] === '!')
	        {
	            $Block = array(
	                'markup' => $Line['body'],
	            );

	            if (preg_match('/-->$/', $Line['text']))
	            {
	                $Block['closed'] = true;
	            }

	            return $Block;
	        }
	    }

	    protected function blockCommentContinue($Line, array $Block)
	    {
	        if (isset($Block['closed']))
	        {
	            return;
	        }

	        $Block['markup'] .= "\n" . $Line['body'];

	        if (preg_match('/-->$/', $Line['text']))
	        {
	            $Block['closed'] = true;
	        }

	        return $Block;
	    }

	    #
	    # Fenced Code

	    protected function blockFencedCode($Line)
	    {
	        if (preg_match('/^['.$Line['text'][0].']{3,}[ ]*([\w-]+)?[ ]*$/', $Line['text'], $matches))
	        {
	            $Element = array(
	                'name' => 'code',
	                'text' => '',
	            );

	            if (isset($matches[1]))
	            {
	                $class = 'language-'.$matches[1];

	                $Element['attributes'] = array(
	                    'class' => $class,
	                );
	            }

	            $Block = array(
	                'char' => $Line['text'][0],
	                'element' => array(
	                    'name' => 'pre',
	                    'handler' => 'element',
	                    'text' => $Element,
	                ),
	            );

	            return $Block;
	        }
	    }

	    protected function blockFencedCodeContinue($Line, $Block)
	    {
	        if (isset($Block['complete']))
	        {
	            return;
	        }

	        if (isset($Block['interrupted']))
	        {
	            $Block['element']['text']['text'] .= "\n";

	            unset($Block['interrupted']);
	        }

	        if (preg_match('/^'.$Block['char'].'{3,}[ ]*$/', $Line['text']))
	        {
	            $Block['element']['text']['text'] = substr($Block['element']['text']['text'], 1);

	            $Block['complete'] = true;

	            return $Block;
	        }

	        $Block['element']['text']['text'] .= "\n".$Line['body'];;

	        return $Block;
	    }

	    protected function blockFencedCodeComplete($Block)
	    {
	        $text = $Block['element']['text']['text'];

	        $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');

	        $Block['element']['text']['text'] = $text;

	        return $Block;
	    }

	    #
	    # Header

	    protected function blockHeader($Line)
	    {
	        if (isset($Line['text'][1]))
	        {
	            $level = 1;

	            while (isset($Line['text'][$level]) and $Line['text'][$level] === '#')
	            {
	                $level ++;
	            }

	            if ($level > 6)
	            {
	                return;
	            }

	            $text = trim($Line['text'], '# ');

	            $Block = array(
	                'element' => array(
	                    'name' => 'h' . min(6, $level),
	                    'text' => $text,
	                    'handler' => 'line',
	                ),
	            );

	            return $Block;
	        }
	    }

	    #
	    # List

	    protected function blockList($Line)
	    {
	        list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]+[.]');

	        if (preg_match('/^('.$pattern.'[ ]+)(.*)/', $Line['text'], $matches))
	        {
	            $Block = array(
	                'indent' => $Line['indent'],
	                'pattern' => $pattern,
	                'element' => array(
	                    'name' => $name,
	                    'handler' => 'elements',
	                ),
	            );

	            $Block['li'] = array(
	                'name' => 'li',
	                'handler' => 'li',
	                'text' => array(
	                    $matches[2],
	                ),
	            );

	            $Block['element']['text'] []= & $Block['li'];

	            return $Block;
	        }
	    }

	    protected function blockListContinue($Line, array $Block)
	    {
	        if ($Block['indent'] === $Line['indent'] and preg_match('/^'.$Block['pattern'].'(?:[ ]+(.*)|$)/', $Line['text'], $matches))
	        {
	            if (isset($Block['interrupted']))
	            {
	                $Block['li']['text'] []= '';

	                unset($Block['interrupted']);
	            }

	            unset($Block['li']);

	            $text = isset($matches[1]) ? $matches[1] : '';

	            $Block['li'] = array(
	                'name' => 'li',
	                'handler' => 'li',
	                'text' => array(
	                    $text,
	                ),
	            );

	            $Block['element']['text'] []= & $Block['li'];

	            return $Block;
	        }

	        if ($Line['text'][0] === '[' and $this->blockReference($Line))
	        {
	            return $Block;
	        }

	        if ( ! isset($Block['interrupted']))
	        {
	            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);

	            $Block['li']['text'] []= $text;

	            return $Block;
	        }

	        if ($Line['indent'] > 0)
	        {
	            $Block['li']['text'] []= '';

	            $text = preg_replace('/^[ ]{0,4}/', '', $Line['body']);

	            $Block['li']['text'] []= $text;

	            unset($Block['interrupted']);

	            return $Block;
	        }
	    }

	    #
	    # Quote

	    protected function blockQuote($Line)
	    {
	        if (preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
	        {
	            $Block = array(
	                'element' => array(
	                    'name' => 'blockquote',
	                    'handler' => 'lines',
	                    'text' => (array) $matches[1],
	                ),
	            );

	            return $Block;
	        }
	    }

	    protected function blockQuoteContinue($Line, array $Block)
	    {
	        if ($Line['text'][0] === '>' and preg_match('/^>[ ]?(.*)/', $Line['text'], $matches))
	        {
	            if (isset($Block['interrupted']))
	            {
	                $Block['element']['text'] []= '';

	                unset($Block['interrupted']);
	            }

	            $Block['element']['text'] []= $matches[1];

	            return $Block;
	        }

	        if ( ! isset($Block['interrupted']))
	        {
	            $Block['element']['text'] []= $Line['text'];

	            return $Block;
	        }
	    }

	    #
	    # Rule

	    protected function blockRule($Line)
	    {
	        if (preg_match('/^(['.$Line['text'][0].'])([ ]*\1){2,}[ ]*$/', $Line['text']))
	        {
	            $Block = array(
	                'element' => array(
	                    'name' => 'hr'
	                ),
	            );

	            return $Block;
	        }
	    }

	    #
	    # Setext

	    protected function blockSetextHeader($Line, array $Block = null)
	    {
	        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
	        {
	            return;
	        }

	        if (chop($Line['text'], $Line['text'][0]) === '')
	        {
	            $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';

	            return $Block;
	        }
	    }

	    #
	    # Markup

	    protected function blockMarkup($Line)
	    {
	        if ($this->markupEscaped)
	        {
	            return;
	        }

	        if (preg_match('/^<(\w*)(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*(\/)?>/', $Line['text'], $matches))
	        {
	            $element = strtolower($matches[1]);

	            if (in_array($element, $this->textLevelElements))
	            {
	                return;
	            }

	            $Block = array(
	                'name' => $matches[1],
	                'depth' => 0,
	                'markup' => $Line['text'],
	            );

	            $length = strlen($matches[0]);

	            $remainder = substr($Line['text'], $length);

	            if (trim($remainder) === '')
	            {
	                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
	                {
	                    $Block['closed'] = true;

	                    $Block['void'] = true;
	                }
	            }
	            else
	            {
	                if (isset($matches[2]) or in_array($matches[1], $this->voidElements))
	                {
	                    return;
	                }

	                if (preg_match('/<\/'.$matches[1].'>[ ]*$/i', $remainder))
	                {
	                    $Block['closed'] = true;
	                }
	            }

	            return $Block;
	        }
	    }

	    protected function blockMarkupContinue($Line, array $Block)
	    {
	        if (isset($Block['closed']))
	        {
	            return;
	        }

	        if (preg_match('/^<'.$Block['name'].'(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*>/i', $Line['text'])) # open
	        {
	            $Block['depth'] ++;
	        }

	        if (preg_match('/(.*?)<\/'.$Block['name'].'>[ ]*$/i', $Line['text'], $matches)) # close
	        {
	            if ($Block['depth'] > 0)
	            {
	                $Block['depth'] --;
	            }
	            else
	            {
	                $Block['closed'] = true;
	            }
	        }

	        if (isset($Block['interrupted']))
	        {
	            $Block['markup'] .= "\n";

	            unset($Block['interrupted']);
	        }

	        $Block['markup'] .= "\n".$Line['body'];

	        return $Block;
	    }

	    #
	    # Reference

	    protected function blockReference($Line)
	    {
	        if (preg_match('/^\[(.+?)\]:[ ]*<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*$/', $Line['text'], $matches))
	        {
	            $id = strtolower($matches[1]);

	            $Data = array(
	                'url' => $matches[2],
	                'title' => null,
	            );

	            if (isset($matches[3]))
	            {
	                $Data['title'] = $matches[3];
	            }

	            $this->DefinitionData['Reference'][$id] = $Data;

	            $Block = array(
	                'hidden' => true,
	            );

	            return $Block;
	        }
	    }

	    #
	    # Table

	    protected function blockTable($Line, array $Block = null)
	    {
	        if ( ! isset($Block) or isset($Block['type']) or isset($Block['interrupted']))
	        {
	            return;
	        }

	        if (strpos($Block['element']['text'], '|') !== false and chop($Line['text'], ' -:|') === '')
	        {
	            $alignments = array();

	            $divider = $Line['text'];

	            $divider = trim($divider);
	            $divider = trim($divider, '|');

	            $dividerCells = explode('|', $divider);

	            foreach ($dividerCells as $dividerCell)
	            {
	                $dividerCell = trim($dividerCell);

	                if ($dividerCell === '')
	                {
	                    continue;
	                }

	                $alignment = null;

	                if ($dividerCell[0] === ':')
	                {
	                    $alignment = 'left';
	                }

	                if (substr($dividerCell, - 1) === ':')
	                {
	                    $alignment = $alignment === 'left' ? 'center' : 'right';
	                }

	                $alignments []= $alignment;
	            }

	            # ~

	            $HeaderElements = array();

	            $header = $Block['element']['text'];

	            $header = trim($header);
	            $header = trim($header, '|');

	            $headerCells = explode('|', $header);

	            foreach ($headerCells as $index => $headerCell)
	            {
	                $headerCell = trim($headerCell);

	                $HeaderElement = array(
	                    'name' => 'th',
	                    'text' => $headerCell,
	                    'handler' => 'line',
	                );

	                if (isset($alignments[$index]))
	                {
	                    $alignment = $alignments[$index];

	                    $HeaderElement['attributes'] = array(
	                        'style' => 'text-align: '.$alignment.';',
	                    );
	                }

	                $HeaderElements []= $HeaderElement;
	            }

	            # ~

	            $Block = array(
	                'alignments' => $alignments,
	                'identified' => true,
	                'element' => array(
	                    'name' => 'table',
	                    'handler' => 'elements',
	                ),
	            );

	            $Block['element']['text'] []= array(
	                'name' => 'thead',
	                'handler' => 'elements',
	            );

	            $Block['element']['text'] []= array(
	                'name' => 'tbody',
	                'handler' => 'elements',
	                'text' => array(),
	            );

	            $Block['element']['text'][0]['text'] []= array(
	                'name' => 'tr',
	                'handler' => 'elements',
	                'text' => $HeaderElements,
	            );

	            return $Block;
	        }
	    }

	    protected function blockTableContinue($Line, array $Block)
	    {
	        if (isset($Block['interrupted']))
	        {
	            return;
	        }

	        if ($Line['text'][0] === '|' or strpos($Line['text'], '|'))
	        {
	            $Elements = array();

	            $row = $Line['text'];

	            $row = trim($row);
	            $row = trim($row, '|');

	            preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]+`|`)+/', $row, $matches);

	            foreach ($matches[0] as $index => $cell)
	            {
	                $cell = trim($cell);

	                $Element = array(
	                    'name' => 'td',
	                    'handler' => 'line',
	                    'text' => $cell,
	                );

	                if (isset($Block['alignments'][$index]))
	                {
	                    $Element['attributes'] = array(
	                        'style' => 'text-align: '.$Block['alignments'][$index].';',
	                    );
	                }

	                $Elements []= $Element;
	            }

	            $Element = array(
	                'name' => 'tr',
	                'handler' => 'elements',
	                'text' => $Elements,
	            );

	            $Block['element']['text'][1]['text'] []= $Element;

	            return $Block;
	        }
	    }

	    #
	    # ~
	    #

	    protected function paragraph($Line)
	    {
	        $Block = array(
	            'element' => array(
	                'name' => 'p',
	                'text' => $Line['text'],
	                'handler' => 'line',
	            ),
	        );

	        return $Block;
	    }

	    #
	    # Inline Elements
	    #

	    protected $InlineTypes = array(
	        '"' => array('SpecialCharacter'),
	        '!' => array('Image'),
	        '&' => array('SpecialCharacter'),
	        '*' => array('Emphasis'),
	        ':' => array('Url'),
	        '<' => array('UrlTag', 'EmailTag', 'Markup', 'SpecialCharacter'),
	        '>' => array('SpecialCharacter'),
	        '[' => array('Link'),
	        '_' => array('Emphasis'),
	        '`' => array('Code'),
	        '~' => array('Strikethrough'),
	        '\\' => array('EscapeSequence'),
	    );

	    # ~

	    protected $inlineMarkerList = '!"*_&[:<>`~\\';

	    #
	    # ~
	    #

	    public function line($text)
	    {
	        $markup = '';

	        # $excerpt is based on the first occurrence of a marker

	        while ($excerpt = strpbrk($text, $this->inlineMarkerList))
	        {
	            $marker = $excerpt[0];

	            $markerPosition = strpos($text, $marker);

	            $Excerpt = array('text' => $excerpt, 'context' => $text);

	            foreach ($this->InlineTypes[$marker] as $inlineType)
	            {
	                $Inline = $this->{'inline'.$inlineType}($Excerpt);

	                if ( ! isset($Inline))
	                {
	                    continue;
	                }

	                # makes sure that the inline belongs to "our" marker

	                if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
	                {
	                    continue;
	                }

	                # sets a default inline position

	                if ( ! isset($Inline['position']))
	                {
	                    $Inline['position'] = $markerPosition;
	                }

	                # the text that comes before the inline
	                $unmarkedText = substr($text, 0, $Inline['position']);

	                # compile the unmarked text
	                $markup .= $this->unmarkedText($unmarkedText);

	                # compile the inline
	                $markup .= isset($Inline['markup']) ? $Inline['markup'] : $this->element($Inline['element']);

	                # remove the examined text
	                $text = substr($text, $Inline['position'] + $Inline['extent']);

	                continue 2;
	            }

	            # the marker does not belong to an inline

	            $unmarkedText = substr($text, 0, $markerPosition + 1);

	            $markup .= $this->unmarkedText($unmarkedText);

	            $text = substr($text, $markerPosition + 1);
	        }

	        $markup .= $this->unmarkedText($text);

	        return $markup;
	    }

	    #
	    # ~
	    #

	    protected function inlineCode($Excerpt)
	    {
	        $marker = $Excerpt['text'][0];

	        if (preg_match('/^('.$marker.'+)[ ]*(.+?)[ ]*(?<!'.$marker.')\1(?!'.$marker.')/s', $Excerpt['text'], $matches))
	        {
	            $text = $matches[2];
	            $text = htmlspecialchars($text, ENT_NOQUOTES, 'UTF-8');
	            $text = preg_replace("/[ ]*\n/", ' ', $text);

	            return array(
	                'extent' => strlen($matches[0]),
	                'element' => array(
	                    'name' => 'code',
	                    'text' => $text,
	                ),
	            );
	        }
	    }

	    protected function inlineEmailTag($Excerpt)
	    {
	        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<((mailto:)?\S+?@\S+?)>/i', $Excerpt['text'], $matches))
	        {
	            $url = $matches[1];

	            if ( ! isset($matches[2]))
	            {
	                $url = 'mailto:' . $url;
	            }

	            return array(
	                'extent' => strlen($matches[0]),
	                'element' => array(
	                    'name' => 'a',
	                    'text' => $matches[1],
	                    'attributes' => array(
	                        'href' => $url,
	                    ),
	                ),
	            );
	        }
	    }

	    protected function inlineEmphasis($Excerpt)
	    {
	        if ( ! isset($Excerpt['text'][1]))
	        {
	            return;
	        }

	        $marker = $Excerpt['text'][0];

	        if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
	        {
	            $emphasis = 'strong';
	        }
	        elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
	        {
	            $emphasis = 'em';
	        }
	        else
	        {
	            return;
	        }

	        return array(
	            'extent' => strlen($matches[0]),
	            'element' => array(
	                'name' => $emphasis,
	                'handler' => 'line',
	                'text' => $matches[1],
	            ),
	        );
	    }

	    protected function inlineEscapeSequence($Excerpt)
	    {
	        if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
	        {
	            return array(
	                'markup' => $Excerpt['text'][1],
	                'extent' => 2,
	            );
	        }
	    }

	    protected function inlineImage($Excerpt)
	    {
	        if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
	        {
	            return;
	        }

	        $Excerpt['text']= substr($Excerpt['text'], 1);

	        $Link = $this->inlineLink($Excerpt);

	        if ($Link === null)
	        {
	            return;
	        }

	        $Inline = array(
	            'extent' => $Link['extent'] + 1,
	            'element' => array(
	                'name' => 'img',
	                'attributes' => array(
	                    'src' => $Link['element']['attributes']['href'],
	                    'alt' => $Link['element']['text'],
	                ),
	            ),
	        );

	        $Inline['element']['attributes'] += $Link['element']['attributes'];

	        unset($Inline['element']['attributes']['href']);

	        return $Inline;
	    }

	    protected function inlineLink($Excerpt)
	    {
	        $Element = array(
	            'name' => 'a',
	            'handler' => 'line',
	            'text' => null,
	            'attributes' => array(
	                'href' => null,
	                'title' => null,
	            ),
	        );

	        $extent = 0;

	        $remainder = $Excerpt['text'];

	        if (preg_match('/\[((?:[^][]|(?R))*)\]/', $remainder, $matches))
	        {
	            $Element['text'] = $matches[1];

	            $extent += strlen($matches[0]);

	            $remainder = substr($remainder, $extent);
	        }
	        else
	        {
	            return;
	        }

	        if (preg_match('/^[(]((?:[^ ()]|[(][^ )]+[)])+)(?:[ ]+("[^"]*"|\'[^\']*\'))?[)]/', $remainder, $matches))
	        {
	            $Element['attributes']['href'] = $matches[1];

	            if (isset($matches[2]))
	            {
	                $Element['attributes']['title'] = substr($matches[2], 1, - 1);
	            }

	            $extent += strlen($matches[0]);
	        }
	        else
	        {
	            if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
	            {
	                $definition = strlen($matches[1]) ? $matches[1] : $Element['text'];
	                $definition = strtolower($definition);

	                $extent += strlen($matches[0]);
	            }
	            else
	            {
	                $definition = strtolower($Element['text']);
	            }

	            if ( ! isset($this->DefinitionData['Reference'][$definition]))
	            {
	                return;
	            }

	            $Definition = $this->DefinitionData['Reference'][$definition];

	            $Element['attributes']['href'] = $Definition['url'];
	            $Element['attributes']['title'] = $Definition['title'];
	        }

	        $Element['attributes']['href'] = str_replace(array('&', '<'), array('&amp;', '&lt;'), $Element['attributes']['href']);

	        return array(
	            'extent' => $extent,
	            'element' => $Element,
	        );
	    }

	    protected function inlineMarkup($Excerpt)
	    {
	        if ($this->markupEscaped or strpos($Excerpt['text'], '>') === false)
	        {
	            return;
	        }

	        if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w*[ ]*>/s', $Excerpt['text'], $matches))
	        {
	            return array(
	                'markup' => $matches[0],
	                'extent' => strlen($matches[0]),
	            );
	        }

	        if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?[^-])*-->/s', $Excerpt['text'], $matches))
	        {
	            return array(
	                'markup' => $matches[0],
	                'extent' => strlen($matches[0]),
	            );
	        }

	        if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w*(?:[ ]*'.$this->regexHtmlAttribute.')*[ ]*\/?>/s', $Excerpt['text'], $matches))
	        {
	            return array(
	                'markup' => $matches[0],
	                'extent' => strlen($matches[0]),
	            );
	        }
	    }

	    protected function inlineSpecialCharacter($Excerpt)
	    {
	        if ($Excerpt['text'][0] === '&' and ! preg_match('/^&#?\w+;/', $Excerpt['text']))
	        {
	            return array(
	                'markup' => '&amp;',
	                'extent' => 1,
	            );
	        }

	        $SpecialCharacter = array('>' => 'gt', '<' => 'lt', '"' => 'quot');

	        if (isset($SpecialCharacter[$Excerpt['text'][0]]))
	        {
	            return array(
	                'markup' => '&'.$SpecialCharacter[$Excerpt['text'][0]].';',
	                'extent' => 1,
	            );
	        }
	    }

	    protected function inlineStrikethrough($Excerpt)
	    {
	        if ( ! isset($Excerpt['text'][1]))
	        {
	            return;
	        }

	        if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
	        {
	            return array(
	                'extent' => strlen($matches[0]),
	                'element' => array(
	                    'name' => 'del',
	                    'text' => $matches[1],
	                    'handler' => 'line',
	                ),
	            );
	        }
	    }

	    protected function inlineUrl($Excerpt)
	    {
	        if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
	        {
	            return;
	        }

	        if (preg_match('/\bhttps?:[\/]{2}[^\s<]+\b\/*/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE))
	        {
	            $Inline = array(
	                'extent' => strlen($matches[0][0]),
	                'position' => $matches[0][1],
	                'element' => array(
	                    'name' => 'a',
	                    'text' => $matches[0][0],
	                    'attributes' => array(
	                        'href' => $matches[0][0],
	                    ),
	                ),
	            );

	            return $Inline;
	        }
	    }

	    protected function inlineUrlTag($Excerpt)
	    {
	        if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w+:\/{2}[^ >]+)>/i', $Excerpt['text'], $matches))
	        {
	            $url = str_replace(array('&', '<'), array('&amp;', '&lt;'), $matches[1]);

	            return array(
	                'extent' => strlen($matches[0]),
	                'element' => array(
	                    'name' => 'a',
	                    'text' => $url,
	                    'attributes' => array(
	                        'href' => $url,
	                    ),
	                ),
	            );
	        }
	    }

	    # ~

	    protected function unmarkedText($text)
	    {
	        if ($this->breaksEnabled)
	        {
	            $text = preg_replace('/[ ]*\n/', "<br />\n", $text);
	        }
	        else
	        {
	            $text = preg_replace('/(?:[ ][ ]+|[ ]*\\\\)\n/', "<br />\n", $text);
	            $text = str_replace(" \n", "\n", $text);
	        }

	        return $text;
	    }

	    #
	    # Handlers
	    #

	    protected function element(array $Element)
	    {
	        $markup = '<'.$Element['name'];

	        if (isset($Element['attributes']))
	        {
	            foreach ($Element['attributes'] as $name => $value)
	            {
	                if ($value === null)
	                {
	                    continue;
	                }

	                $markup .= ' '.$name.'="'.$value.'"';
	            }
	        }

	        if (isset($Element['text']))
	        {
	            $markup .= '>';

	            if (isset($Element['handler']))
	            {
	                $markup .= $this->{$Element['handler']}($Element['text']);
	            }
	            else
	            {
	                $markup .= $Element['text'];
	            }

	            $markup .= '</'.$Element['name'].'>';
	        }
	        else
	        {
	            $markup .= ' />';
	        }

	        return $markup;
	    }

	    protected function elements(array $Elements)
	    {
	        $markup = '';

	        foreach ($Elements as $Element)
	        {
	            $markup .= "\n" . $this->element($Element);
	        }

	        $markup .= "\n";

	        return $markup;
	    }

	    # ~

	    protected function li($lines)
	    {
	        $markup = $this->lines($lines);

	        $trimmedMarkup = trim($markup);

	        if ( ! in_array('', $lines) and substr($trimmedMarkup, 0, 3) === '<p>')
	        {
	            $markup = $trimmedMarkup;
	            $markup = substr($markup, 3);

	            $position = strpos($markup, "</p>");

	            $markup = substr_replace($markup, '', $position, 4);
	        }

	        return $markup;
	    }

	    #
	    # Deprecated Methods
	    #

	    function parse($text)
	    {
	        $markup = $this->text($text);

	        return $markup;
	    }

	    #
	    # Static Methods
	    #

	    static function instance($name = 'default')
	    {
	        if (isset(self::$instances[$name]))
	        {
	            return self::$instances[$name];
	        }

	        $instance = new static();

	        self::$instances[$name] = $instance;

	        return $instance;
	    }

	    private static $instances = array();

	    #
	    # Fields
	    #

	    protected $DefinitionData;

	    #
	    # Read-Only

	    protected $specialCharacters = array(
	        '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|',
	    );

	    protected $StrongRegex = array(
	        '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*[*])+?)[*]{2}(?![*])/s',
	        '_' => '/^__((?:\\\\_|[^_]|_[^_]*_)+?)__(?!_)/us',
	    );

	    protected $EmRegex = array(
	        '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
	        '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
	    );

	    protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*(?:\s*=\s*(?:[^"\'=<>`\s]+|"[^"]*"|\'[^\']*\'))?';

	    protected $voidElements = array(
	        'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
	    );

	    protected $textLevelElements = array(
	        'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
	        'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
	        'i', 'rp', 'del', 'code',          'strike', 'marquee',
	        'q', 'rt', 'ins', 'font',          'strong',
	        's', 'tt', 'sub', 'mark',
	        'u', 'xm', 'sup', 'nobr',
	                   'var', 'ruby',
	                   'wbr', 'span',
	                          'time',
	    );
	}
}