openrat-cms

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

Parsedown.class.php (39279B)


      1 <?php
      2 
      3 #
      4 #
      5 # Parsedown
      6 # http://parsedown.org
      7 #
      8 # (c) Emanuil Rusev
      9 # http://erusev.com
     10 #
     11 # For the full license information, view the LICENSE file that was distributed
     12 # with this source code.
     13 #
     14 #
     15 
     16 namespace util;
     17 class Parsedown
     18 {
     19 	# ~
     20 
     21 	const version = '1.8.0-beta-7';
     22 
     23 	# ~
     24 
     25 	function text($text)
     26 	{
     27 		$Elements = $this->textElements($text);
     28 
     29 		# convert to markup
     30 		$markup = $this->elements($Elements);
     31 
     32 		# trim line breaks
     33 		$markup = trim($markup, "\n");
     34 
     35 		return $markup;
     36 	}
     37 
     38 	protected function textElements($text)
     39 	{
     40 		# make sure no definitions are set
     41 		$this->DefinitionData = array();
     42 
     43 		# standardize line breaks
     44 		$text = str_replace(array("\r\n", "\r"), "\n", $text);
     45 
     46 		# remove surrounding line breaks
     47 		$text = trim($text, "\n");
     48 
     49 		# split text into lines
     50 		$lines = explode("\n", $text);
     51 
     52 		# iterate through lines to identify blocks
     53 		return $this->linesElements($lines);
     54 	}
     55 
     56 	#
     57 	# Setters
     58 	#
     59 
     60 	function setBreaksEnabled($breaksEnabled)
     61 	{
     62 		$this->breaksEnabled = $breaksEnabled;
     63 
     64 		return $this;
     65 	}
     66 
     67 	protected $breaksEnabled;
     68 
     69 	function setMarkupEscaped($markupEscaped)
     70 	{
     71 		$this->markupEscaped = $markupEscaped;
     72 
     73 		return $this;
     74 	}
     75 
     76 	protected $markupEscaped;
     77 
     78 	function setUrlsLinked($urlsLinked)
     79 	{
     80 		$this->urlsLinked = $urlsLinked;
     81 
     82 		return $this;
     83 	}
     84 
     85 	protected $urlsLinked = true;
     86 
     87 	function setSafeMode($safeMode)
     88 	{
     89 		$this->safeMode = (bool)$safeMode;
     90 
     91 		return $this;
     92 	}
     93 
     94 	protected $safeMode;
     95 
     96 	function setStrictMode($strictMode)
     97 	{
     98 		$this->strictMode = (bool)$strictMode;
     99 
    100 		return $this;
    101 	}
    102 
    103 	protected $strictMode;
    104 
    105 	protected $safeLinksWhitelist = array(
    106 		'http://',
    107 		'https://',
    108 		'ftp://',
    109 		'ftps://',
    110 		'mailto:',
    111 		'tel:',
    112 		'data:image/png;base64,',
    113 		'data:image/gif;base64,',
    114 		'data:image/jpeg;base64,',
    115 		'irc:',
    116 		'ircs:',
    117 		'git:',
    118 		'ssh:',
    119 		'news:',
    120 		'steam:',
    121 	);
    122 
    123 	#
    124 	# Lines
    125 	#
    126 
    127 	protected $BlockTypes = array(
    128 		'#' => array('Header'),
    129 		'*' => array('Rule', 'List'),
    130 		'+' => array('List'),
    131 		'-' => array('SetextHeader', 'Table', 'Rule', 'List'),
    132 		'0' => array('List'),
    133 		'1' => array('List'),
    134 		'2' => array('List'),
    135 		'3' => array('List'),
    136 		'4' => array('List'),
    137 		'5' => array('List'),
    138 		'6' => array('List'),
    139 		'7' => array('List'),
    140 		'8' => array('List'),
    141 		'9' => array('List'),
    142 		':' => array('Table'),
    143 		'<' => array('Comment', 'Markup'),
    144 		'=' => array('SetextHeader'),
    145 		'>' => array('Quote'),
    146 		'[' => array('Reference'),
    147 		'_' => array('Rule'),
    148 		'`' => array('FencedCode'),
    149 		'|' => array('Table'),
    150 		'~' => array('FencedCode'),
    151 	);
    152 
    153 	# ~
    154 
    155 	protected $unmarkedBlockTypes = array(
    156 		'Code',
    157 	);
    158 
    159 	#
    160 	# Blocks
    161 	#
    162 
    163 	protected function lines(array $lines)
    164 	{
    165 		return $this->elements($this->linesElements($lines));
    166 	}
    167 
    168 	protected function linesElements(array $lines)
    169 	{
    170 		$Elements = array();
    171 		$CurrentBlock = null;
    172 
    173 		foreach ($lines as $line) {
    174 			if (chop($line) === '') {
    175 				if (isset($CurrentBlock)) {
    176 					$CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
    177 						? $CurrentBlock['interrupted'] + 1 : 1
    178 					);
    179 				}
    180 
    181 				continue;
    182 			}
    183 
    184 			while (($beforeTab = strstr($line, "\t", true)) !== false) {
    185 				$shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
    186 
    187 				$line = $beforeTab
    188 					. str_repeat(' ', $shortage)
    189 					. substr($line, strlen($beforeTab) + 1);
    190 			}
    191 
    192 			$indent = strspn($line, ' ');
    193 
    194 			$text = $indent > 0 ? substr($line, $indent) : $line;
    195 
    196 			# ~
    197 
    198 			$Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
    199 
    200 			# ~
    201 
    202 			if (isset($CurrentBlock['continuable'])) {
    203 				$methodName = 'block' . $CurrentBlock['type'] . 'Continue';
    204 				$Block = $this->$methodName($Line, $CurrentBlock);
    205 
    206 				if (isset($Block)) {
    207 					$CurrentBlock = $Block;
    208 
    209 					continue;
    210 				} else {
    211 					if ($this->isBlockCompletable($CurrentBlock['type'])) {
    212 						$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
    213 						$CurrentBlock = $this->$methodName($CurrentBlock);
    214 					}
    215 				}
    216 			}
    217 
    218 			# ~
    219 
    220 			$marker = $text[0];
    221 
    222 			# ~
    223 
    224 			$blockTypes = $this->unmarkedBlockTypes;
    225 
    226 			if (isset($this->BlockTypes[$marker])) {
    227 				foreach ($this->BlockTypes[$marker] as $blockType) {
    228 					$blockTypes [] = $blockType;
    229 				}
    230 			}
    231 
    232 			#
    233 			# ~
    234 
    235 			foreach ($blockTypes as $blockType) {
    236 				$Block = $this->{"block$blockType"}($Line, $CurrentBlock);
    237 
    238 				if (isset($Block)) {
    239 					$Block['type'] = $blockType;
    240 
    241 					if (!isset($Block['identified'])) {
    242 						if (isset($CurrentBlock)) {
    243 							$Elements[] = $this->extractElement($CurrentBlock);
    244 						}
    245 
    246 						$Block['identified'] = true;
    247 					}
    248 
    249 					if ($this->isBlockContinuable($blockType)) {
    250 						$Block['continuable'] = true;
    251 					}
    252 
    253 					$CurrentBlock = $Block;
    254 
    255 					continue 2;
    256 				}
    257 			}
    258 
    259 			# ~
    260 
    261 			if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph') {
    262 				$Block = $this->paragraphContinue($Line, $CurrentBlock);
    263 			}
    264 
    265 			if (isset($Block)) {
    266 				$CurrentBlock = $Block;
    267 			} else {
    268 				if (isset($CurrentBlock)) {
    269 					$Elements[] = $this->extractElement($CurrentBlock);
    270 				}
    271 
    272 				$CurrentBlock = $this->paragraph($Line);
    273 
    274 				$CurrentBlock['identified'] = true;
    275 			}
    276 		}
    277 
    278 		# ~
    279 
    280 		if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type'])) {
    281 			$methodName = 'block' . $CurrentBlock['type'] . 'Complete';
    282 			$CurrentBlock = $this->$methodName($CurrentBlock);
    283 		}
    284 
    285 		# ~
    286 
    287 		if (isset($CurrentBlock)) {
    288 			$Elements[] = $this->extractElement($CurrentBlock);
    289 		}
    290 
    291 		# ~
    292 
    293 		return $Elements;
    294 	}
    295 
    296 	protected function extractElement(array $Component)
    297 	{
    298 		if (!isset($Component['element'])) {
    299 			if (isset($Component['markup'])) {
    300 				$Component['element'] = array('rawHtml' => $Component['markup']);
    301 			} elseif (isset($Component['hidden'])) {
    302 				$Component['element'] = array();
    303 			}
    304 		}
    305 
    306 		return $Component['element'];
    307 	}
    308 
    309 	protected function isBlockContinuable($Type)
    310 	{
    311 		return method_exists($this, 'block' . $Type . 'Continue');
    312 	}
    313 
    314 	protected function isBlockCompletable($Type)
    315 	{
    316 		return method_exists($this, 'block' . $Type . 'Complete');
    317 	}
    318 
    319 	#
    320 	# Code
    321 
    322 	protected function blockCode($Line, $Block = null)
    323 	{
    324 		if (isset($Block) and $Block['type'] === 'Paragraph' and !isset($Block['interrupted'])) {
    325 			return;
    326 		}
    327 
    328 		if ($Line['indent'] >= 4) {
    329 			$text = substr($Line['body'], 4);
    330 
    331 			$Block = array(
    332 				'element' => array(
    333 					'name' => 'pre',
    334 					'element' => array(
    335 						'name' => 'code',
    336 						'text' => $text,
    337 					),
    338 				),
    339 			);
    340 
    341 			return $Block;
    342 		}
    343 	}
    344 
    345 	protected function blockCodeContinue($Line, $Block)
    346 	{
    347 		if ($Line['indent'] >= 4) {
    348 			if (isset($Block['interrupted'])) {
    349 				$Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
    350 
    351 				unset($Block['interrupted']);
    352 			}
    353 
    354 			$Block['element']['element']['text'] .= "\n";
    355 
    356 			$text = substr($Line['body'], 4);
    357 
    358 			$Block['element']['element']['text'] .= $text;
    359 
    360 			return $Block;
    361 		}
    362 	}
    363 
    364 	protected function blockCodeComplete($Block)
    365 	{
    366 		return $Block;
    367 	}
    368 
    369 	#
    370 	# Comment
    371 
    372 	protected function blockComment($Line)
    373 	{
    374 		if ($this->markupEscaped or $this->safeMode) {
    375 			return;
    376 		}
    377 
    378 		if (strpos($Line['text'], '<!--') === 0) {
    379 			$Block = array(
    380 				'element' => array(
    381 					'rawHtml' => $Line['body'],
    382 					'autobreak' => true,
    383 				),
    384 			);
    385 
    386 			if (strpos($Line['text'], '-->') !== false) {
    387 				$Block['closed'] = true;
    388 			}
    389 
    390 			return $Block;
    391 		}
    392 	}
    393 
    394 	protected function blockCommentContinue($Line, array $Block)
    395 	{
    396 		if (isset($Block['closed'])) {
    397 			return;
    398 		}
    399 
    400 		$Block['element']['rawHtml'] .= "\n" . $Line['body'];
    401 
    402 		if (strpos($Line['text'], '-->') !== false) {
    403 			$Block['closed'] = true;
    404 		}
    405 
    406 		return $Block;
    407 	}
    408 
    409 	#
    410 	# Fenced Code
    411 
    412 	protected function blockFencedCode($Line)
    413 	{
    414 		$marker = $Line['text'][0];
    415 
    416 		$openerLength = strspn($Line['text'], $marker);
    417 
    418 		if ($openerLength < 3) {
    419 			return;
    420 		}
    421 
    422 		$infostring = trim(substr($Line['text'], $openerLength), "\t ");
    423 
    424 		if (strpos($infostring, '`') !== false) {
    425 			return;
    426 		}
    427 
    428 		$Element = array(
    429 			'name' => 'code',
    430 			'text' => '',
    431 		);
    432 
    433 		if ($infostring !== '') {
    434 			/**
    435 			 * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
    436 			 * Every HTML element may have a class attribute specified.
    437 			 * The attribute, if specified, must have a value that is a set
    438 			 * of space-separated tokens representing the various classes
    439 			 * that the element belongs to.
    440 			 * [...]
    441 			 * The space characters, for the purposes of this specification,
    442 			 * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
    443 			 * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
    444 			 * U+000D CARRIAGE RETURN (CR).
    445 			 */
    446 			$language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
    447 
    448 			$Element['attributes'] = array('class' => "language-$language");
    449 		}
    450 
    451 		$Block = array(
    452 			'char' => $marker,
    453 			'openerLength' => $openerLength,
    454 			'element' => array(
    455 				'name' => 'pre',
    456 				'element' => $Element,
    457 			),
    458 		);
    459 
    460 		return $Block;
    461 	}
    462 
    463 	protected function blockFencedCodeContinue($Line, $Block)
    464 	{
    465 		if (isset($Block['complete'])) {
    466 			return;
    467 		}
    468 
    469 		if (isset($Block['interrupted'])) {
    470 			$Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
    471 
    472 			unset($Block['interrupted']);
    473 		}
    474 
    475 		if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
    476 			and chop(substr($Line['text'], $len), ' ') === ''
    477 		) {
    478 			$Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
    479 
    480 			$Block['complete'] = true;
    481 
    482 			return $Block;
    483 		}
    484 
    485 		$Block['element']['element']['text'] .= "\n" . $Line['body'];
    486 
    487 		return $Block;
    488 	}
    489 
    490 	protected function blockFencedCodeComplete($Block)
    491 	{
    492 		return $Block;
    493 	}
    494 
    495 	#
    496 	# Header
    497 
    498 	protected function blockHeader($Line)
    499 	{
    500 		$level = strspn($Line['text'], '#');
    501 
    502 		if ($level > 6) {
    503 			return;
    504 		}
    505 
    506 		$text = trim($Line['text'], '#');
    507 
    508 		if ($this->strictMode and isset($text[0]) and $text[0] !== ' ') {
    509 			return;
    510 		}
    511 
    512 		$text = trim($text, ' ');
    513 
    514 		$Block = array(
    515 			'element' => array(
    516 				'name' => 'h' . $level,
    517 				'handler' => array(
    518 					'function' => 'lineElements',
    519 					'argument' => $text,
    520 					'destination' => 'elements',
    521 				)
    522 			),
    523 		);
    524 
    525 		return $Block;
    526 	}
    527 
    528 	#
    529 	# List
    530 
    531 	protected function blockList($Line, array $CurrentBlock = null)
    532 	{
    533 		list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
    534 
    535 		if (preg_match('/^(' . $pattern . '([ ]++|$))(.*+)/', $Line['text'], $matches)) {
    536 			$contentIndent = strlen($matches[2]);
    537 
    538 			if ($contentIndent >= 5) {
    539 				$contentIndent -= 1;
    540 				$matches[1] = substr($matches[1], 0, -$contentIndent);
    541 				$matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
    542 			} elseif ($contentIndent === 0) {
    543 				$matches[1] .= ' ';
    544 			}
    545 
    546 			$markerWithoutWhitespace = strstr($matches[1], ' ', true);
    547 
    548 			$Block = array(
    549 				'indent' => $Line['indent'],
    550 				'pattern' => $pattern,
    551 				'data' => array(
    552 					'type' => $name,
    553 					'marker' => $matches[1],
    554 					'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
    555 				),
    556 				'element' => array(
    557 					'name' => $name,
    558 					'elements' => array(),
    559 				),
    560 			);
    561 			$Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
    562 
    563 			if ($name === 'ol') {
    564 				$listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
    565 
    566 				if ($listStart !== '1') {
    567 					if (
    568 						isset($CurrentBlock)
    569 						and $CurrentBlock['type'] === 'Paragraph'
    570 						and !isset($CurrentBlock['interrupted'])
    571 					) {
    572 						return;
    573 					}
    574 
    575 					$Block['element']['attributes'] = array('start' => $listStart);
    576 				}
    577 			}
    578 
    579 			$Block['li'] = array(
    580 				'name' => 'li',
    581 				'handler' => array(
    582 					'function' => 'li',
    583 					'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
    584 					'destination' => 'elements'
    585 				)
    586 			);
    587 
    588 			$Block['element']['elements'] [] = &$Block['li'];
    589 
    590 			return $Block;
    591 		}
    592 	}
    593 
    594 	protected function blockListContinue($Line, array $Block)
    595 	{
    596 		if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument'])) {
    597 			return null;
    598 		}
    599 
    600 		$requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
    601 
    602 		if ($Line['indent'] < $requiredIndent
    603 			and (
    604 				(
    605 					$Block['data']['type'] === 'ol'
    606 					and preg_match('/^[0-9]++' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches)
    607 				) or (
    608 					$Block['data']['type'] === 'ul'
    609 					and preg_match('/^' . $Block['data']['markerTypeRegex'] . '(?:[ ]++(.*)|$)/', $Line['text'], $matches)
    610 				)
    611 			)
    612 		) {
    613 			if (isset($Block['interrupted'])) {
    614 				$Block['li']['handler']['argument'] [] = '';
    615 
    616 				$Block['loose'] = true;
    617 
    618 				unset($Block['interrupted']);
    619 			}
    620 
    621 			unset($Block['li']);
    622 
    623 			$text = isset($matches[1]) ? $matches[1] : '';
    624 
    625 			$Block['indent'] = $Line['indent'];
    626 
    627 			$Block['li'] = array(
    628 				'name' => 'li',
    629 				'handler' => array(
    630 					'function' => 'li',
    631 					'argument' => array($text),
    632 					'destination' => 'elements'
    633 				)
    634 			);
    635 
    636 			$Block['element']['elements'] [] = &$Block['li'];
    637 
    638 			return $Block;
    639 		} elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line)) {
    640 			return null;
    641 		}
    642 
    643 		if ($Line['text'][0] === '[' and $this->blockReference($Line)) {
    644 			return $Block;
    645 		}
    646 
    647 		if ($Line['indent'] >= $requiredIndent) {
    648 			if (isset($Block['interrupted'])) {
    649 				$Block['li']['handler']['argument'] [] = '';
    650 
    651 				$Block['loose'] = true;
    652 
    653 				unset($Block['interrupted']);
    654 			}
    655 
    656 			$text = substr($Line['body'], $requiredIndent);
    657 
    658 			$Block['li']['handler']['argument'] [] = $text;
    659 
    660 			return $Block;
    661 		}
    662 
    663 		if (!isset($Block['interrupted'])) {
    664 			$text = preg_replace('/^[ ]{0,' . $requiredIndent . '}+/', '', $Line['body']);
    665 
    666 			$Block['li']['handler']['argument'] [] = $text;
    667 
    668 			return $Block;
    669 		}
    670 	}
    671 
    672 	protected function blockListComplete(array $Block)
    673 	{
    674 		if (isset($Block['loose'])) {
    675 			foreach ($Block['element']['elements'] as &$li) {
    676 				if (end($li['handler']['argument']) !== '') {
    677 					$li['handler']['argument'] [] = '';
    678 				}
    679 			}
    680 		}
    681 
    682 		return $Block;
    683 	}
    684 
    685 	#
    686 	# Quote
    687 
    688 	protected function blockQuote($Line)
    689 	{
    690 		if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
    691 			$Block = array(
    692 				'element' => array(
    693 					'name' => 'blockquote',
    694 					'handler' => array(
    695 						'function' => 'linesElements',
    696 						'argument' => (array)$matches[1],
    697 						'destination' => 'elements',
    698 					)
    699 				),
    700 			);
    701 
    702 			return $Block;
    703 		}
    704 	}
    705 
    706 	protected function blockQuoteContinue($Line, array $Block)
    707 	{
    708 		if (isset($Block['interrupted'])) {
    709 			return;
    710 		}
    711 
    712 		if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches)) {
    713 			$Block['element']['handler']['argument'] [] = $matches[1];
    714 
    715 			return $Block;
    716 		}
    717 
    718 		if (!isset($Block['interrupted'])) {
    719 			$Block['element']['handler']['argument'] [] = $Line['text'];
    720 
    721 			return $Block;
    722 		}
    723 	}
    724 
    725 	#
    726 	# Rule
    727 
    728 	protected function blockRule($Line)
    729 	{
    730 		$marker = $Line['text'][0];
    731 
    732 		if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '') {
    733 			$Block = array(
    734 				'element' => array(
    735 					'name' => 'hr',
    736 				),
    737 			);
    738 
    739 			return $Block;
    740 		}
    741 	}
    742 
    743 	#
    744 	# Setext
    745 
    746 	protected function blockSetextHeader($Line, array $Block = null)
    747 	{
    748 		if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) {
    749 			return;
    750 		}
    751 
    752 		if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '') {
    753 			$Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
    754 
    755 			return $Block;
    756 		}
    757 	}
    758 
    759 	#
    760 	# Markup
    761 
    762 	protected function blockMarkup($Line)
    763 	{
    764 		if ($this->markupEscaped or $this->safeMode) {
    765 			return;
    766 		}
    767 
    768 		if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+(\/)?>/', $Line['text'], $matches)) {
    769 			$element = strtolower($matches[1]);
    770 
    771 			if (in_array($element, $this->textLevelElements)) {
    772 				return;
    773 			}
    774 
    775 			$Block = array(
    776 				'name' => $matches[1],
    777 				'element' => array(
    778 					'rawHtml' => $Line['text'],
    779 					'autobreak' => true,
    780 				),
    781 			);
    782 
    783 			return $Block;
    784 		}
    785 	}
    786 
    787 	protected function blockMarkupContinue($Line, array $Block)
    788 	{
    789 		if (isset($Block['closed']) or isset($Block['interrupted'])) {
    790 			return;
    791 		}
    792 
    793 		$Block['element']['rawHtml'] .= "\n" . $Line['body'];
    794 
    795 		return $Block;
    796 	}
    797 
    798 	#
    799 	# Reference
    800 
    801 	protected function blockReference($Line)
    802 	{
    803 		if (strpos($Line['text'], ']') !== false
    804 			and preg_match('/^\[(.+?)\]:[ ]*+<?(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
    805 		) {
    806 			$id = strtolower($matches[1]);
    807 
    808 			$Data = array(
    809 				'url' => $matches[2],
    810 				'title' => isset($matches[3]) ? $matches[3] : null,
    811 			);
    812 
    813 			$this->DefinitionData['Reference'][$id] = $Data;
    814 
    815 			$Block = array(
    816 				'element' => array(),
    817 			);
    818 
    819 			return $Block;
    820 		}
    821 	}
    822 
    823 	#
    824 	# Table
    825 
    826 	protected function blockTable($Line, array $Block = null)
    827 	{
    828 		if (!isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted'])) {
    829 			return;
    830 		}
    831 
    832 		if (
    833 			strpos($Block['element']['handler']['argument'], '|') === false
    834 			and strpos($Line['text'], '|') === false
    835 			and strpos($Line['text'], ':') === false
    836 			or strpos($Block['element']['handler']['argument'], "\n") !== false
    837 		) {
    838 			return;
    839 		}
    840 
    841 		if (chop($Line['text'], ' -:|') !== '') {
    842 			return;
    843 		}
    844 
    845 		$alignments = array();
    846 
    847 		$divider = $Line['text'];
    848 
    849 		$divider = trim($divider);
    850 		$divider = trim($divider, '|');
    851 
    852 		$dividerCells = explode('|', $divider);
    853 
    854 		foreach ($dividerCells as $dividerCell) {
    855 			$dividerCell = trim($dividerCell);
    856 
    857 			if ($dividerCell === '') {
    858 				return;
    859 			}
    860 
    861 			$alignment = null;
    862 
    863 			if ($dividerCell[0] === ':') {
    864 				$alignment = 'left';
    865 			}
    866 
    867 			if (substr($dividerCell, -1) === ':') {
    868 				$alignment = $alignment === 'left' ? 'center' : 'right';
    869 			}
    870 
    871 			$alignments [] = $alignment;
    872 		}
    873 
    874 		# ~
    875 
    876 		$HeaderElements = array();
    877 
    878 		$header = $Block['element']['handler']['argument'];
    879 
    880 		$header = trim($header);
    881 		$header = trim($header, '|');
    882 
    883 		$headerCells = explode('|', $header);
    884 
    885 		if (count($headerCells) !== count($alignments)) {
    886 			return;
    887 		}
    888 
    889 		foreach ($headerCells as $index => $headerCell) {
    890 			$headerCell = trim($headerCell);
    891 
    892 			$HeaderElement = array(
    893 				'name' => 'th',
    894 				'handler' => array(
    895 					'function' => 'lineElements',
    896 					'argument' => $headerCell,
    897 					'destination' => 'elements',
    898 				)
    899 			);
    900 
    901 			if (isset($alignments[$index])) {
    902 				$alignment = $alignments[$index];
    903 
    904 				$HeaderElement['attributes'] = array(
    905 					'style' => "text-align: $alignment;",
    906 				);
    907 			}
    908 
    909 			$HeaderElements [] = $HeaderElement;
    910 		}
    911 
    912 		# ~
    913 
    914 		$Block = array(
    915 			'alignments' => $alignments,
    916 			'identified' => true,
    917 			'element' => array(
    918 				'name' => 'table',
    919 				'elements' => array(),
    920 			),
    921 		);
    922 
    923 		$Block['element']['elements'] [] = array(
    924 			'name' => 'thead',
    925 		);
    926 
    927 		$Block['element']['elements'] [] = array(
    928 			'name' => 'tbody',
    929 			'elements' => array(),
    930 		);
    931 
    932 		$Block['element']['elements'][0]['elements'] [] = array(
    933 			'name' => 'tr',
    934 			'elements' => $HeaderElements,
    935 		);
    936 
    937 		return $Block;
    938 	}
    939 
    940 	protected function blockTableContinue($Line, array $Block)
    941 	{
    942 		if (isset($Block['interrupted'])) {
    943 			return;
    944 		}
    945 
    946 		if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|')) {
    947 			$Elements = array();
    948 
    949 			$row = $Line['text'];
    950 
    951 			$row = trim($row);
    952 			$row = trim($row, '|');
    953 
    954 			preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
    955 
    956 			$cells = array_slice($matches[0], 0, count($Block['alignments']));
    957 
    958 			foreach ($cells as $index => $cell) {
    959 				$cell = trim($cell);
    960 
    961 				$Element = array(
    962 					'name' => 'td',
    963 					'handler' => array(
    964 						'function' => 'lineElements',
    965 						'argument' => $cell,
    966 						'destination' => 'elements',
    967 					)
    968 				);
    969 
    970 				if (isset($Block['alignments'][$index])) {
    971 					$Element['attributes'] = array(
    972 						'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
    973 					);
    974 				}
    975 
    976 				$Elements [] = $Element;
    977 			}
    978 
    979 			$Element = array(
    980 				'name' => 'tr',
    981 				'elements' => $Elements,
    982 			);
    983 
    984 			$Block['element']['elements'][1]['elements'] [] = $Element;
    985 
    986 			return $Block;
    987 		}
    988 	}
    989 
    990 	#
    991 	# ~
    992 	#
    993 
    994 	protected function paragraph($Line)
    995 	{
    996 		return array(
    997 			'type' => 'Paragraph',
    998 			'element' => array(
    999 				'name' => 'p',
   1000 				'handler' => array(
   1001 					'function' => 'lineElements',
   1002 					'argument' => $Line['text'],
   1003 					'destination' => 'elements',
   1004 				),
   1005 			),
   1006 		);
   1007 	}
   1008 
   1009 	protected function paragraphContinue($Line, array $Block)
   1010 	{
   1011 		if (isset($Block['interrupted'])) {
   1012 			return;
   1013 		}
   1014 
   1015 		$Block['element']['handler']['argument'] .= "\n" . $Line['text'];
   1016 
   1017 		return $Block;
   1018 	}
   1019 
   1020 	#
   1021 	# Inline Elements
   1022 	#
   1023 
   1024 	protected $InlineTypes = array(
   1025 		'!' => array('Image'),
   1026 		'&' => array('SpecialCharacter'),
   1027 		'*' => array('Emphasis'),
   1028 		':' => array('Url'),
   1029 		'<' => array('UrlTag', 'EmailTag', 'Markup'),
   1030 		'[' => array('Link'),
   1031 		'_' => array('Emphasis'),
   1032 		'`' => array('Code'),
   1033 		'~' => array('Strikethrough'),
   1034 		'\\' => array('EscapeSequence'),
   1035 	);
   1036 
   1037 	# ~
   1038 
   1039 	protected $inlineMarkerList = '!*_&[:<`~\\';
   1040 
   1041 	#
   1042 	# ~
   1043 	#
   1044 
   1045 	public function line($text, $nonNestables = array())
   1046 	{
   1047 		return $this->elements($this->lineElements($text, $nonNestables));
   1048 	}
   1049 
   1050 	protected function lineElements($text, $nonNestables = array())
   1051 	{
   1052 		# standardize line breaks
   1053 		$text = str_replace(array("\r\n", "\r"), "\n", $text);
   1054 
   1055 		$Elements = array();
   1056 
   1057 		$nonNestables = (empty($nonNestables)
   1058 			? array()
   1059 			: array_combine($nonNestables, $nonNestables)
   1060 		);
   1061 
   1062 		# $excerpt is based on the first occurrence of a marker
   1063 
   1064 		while ($excerpt = strpbrk($text, $this->inlineMarkerList)) {
   1065 			$marker = $excerpt[0];
   1066 
   1067 			$markerPosition = strlen($text) - strlen($excerpt);
   1068 
   1069 			$Excerpt = array('text' => $excerpt, 'context' => $text);
   1070 
   1071 			foreach ($this->InlineTypes[$marker] as $inlineType) {
   1072 				# check to see if the current inline type is nestable in the current context
   1073 
   1074 				if (isset($nonNestables[$inlineType])) {
   1075 					continue;
   1076 				}
   1077 
   1078 				$Inline = $this->{"inline$inlineType"}($Excerpt);
   1079 
   1080 				if (!isset($Inline)) {
   1081 					continue;
   1082 				}
   1083 
   1084 				# makes sure that the inline belongs to "our" marker
   1085 
   1086 				if (isset($Inline['position']) and $Inline['position'] > $markerPosition) {
   1087 					continue;
   1088 				}
   1089 
   1090 				# sets a default inline position
   1091 
   1092 				if (!isset($Inline['position'])) {
   1093 					$Inline['position'] = $markerPosition;
   1094 				}
   1095 
   1096 				# cause the new element to 'inherit' our non nestables
   1097 
   1098 
   1099 				$Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
   1100 					? array_merge($Inline['element']['nonNestables'], $nonNestables)
   1101 					: $nonNestables;
   1102 
   1103 				# the text that comes before the inline
   1104 				$unmarkedText = substr($text, 0, $Inline['position']);
   1105 
   1106 				# compile the unmarked text
   1107 				$InlineText = $this->inlineText($unmarkedText);
   1108 				$Elements[] = $InlineText['element'];
   1109 
   1110 				# compile the inline
   1111 				$Elements[] = $this->extractElement($Inline);
   1112 
   1113 				# remove the examined text
   1114 				$text = substr($text, $Inline['position'] + $Inline['extent']);
   1115 
   1116 				continue 2;
   1117 			}
   1118 
   1119 			# the marker does not belong to an inline
   1120 
   1121 			$unmarkedText = substr($text, 0, $markerPosition + 1);
   1122 
   1123 			$InlineText = $this->inlineText($unmarkedText);
   1124 			$Elements[] = $InlineText['element'];
   1125 
   1126 			$text = substr($text, $markerPosition + 1);
   1127 		}
   1128 
   1129 		$InlineText = $this->inlineText($text);
   1130 		$Elements[] = $InlineText['element'];
   1131 
   1132 		foreach ($Elements as &$Element) {
   1133 			if (!isset($Element['autobreak'])) {
   1134 				$Element['autobreak'] = false;
   1135 			}
   1136 		}
   1137 
   1138 		return $Elements;
   1139 	}
   1140 
   1141 	#
   1142 	# ~
   1143 	#
   1144 
   1145 	protected function inlineText($text)
   1146 	{
   1147 		$Inline = array(
   1148 			'extent' => strlen($text),
   1149 			'element' => array(),
   1150 		);
   1151 
   1152 		$Inline['element']['elements'] = self::pregReplaceElements(
   1153 			$this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
   1154 			array(
   1155 				array('name' => 'br'),
   1156 				array('text' => "\n"),
   1157 			),
   1158 			$text
   1159 		);
   1160 
   1161 		return $Inline;
   1162 	}
   1163 
   1164 	protected function inlineCode($Excerpt)
   1165 	{
   1166 		$marker = $Excerpt['text'][0];
   1167 
   1168 		if (preg_match('/^([' . $marker . ']++)[ ]*+(.+?)[ ]*+(?<![' . $marker . '])\1(?!' . $marker . ')/s', $Excerpt['text'], $matches)) {
   1169 			$text = $matches[2];
   1170 			$text = preg_replace('/[ ]*+\n/', ' ', $text);
   1171 
   1172 			return array(
   1173 				'extent' => strlen($matches[0]),
   1174 				'element' => array(
   1175 					'name' => 'code',
   1176 					'text' => $text,
   1177 				),
   1178 			);
   1179 		}
   1180 	}
   1181 
   1182 	protected function inlineEmailTag($Excerpt)
   1183 	{
   1184 		$hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
   1185 
   1186 		$commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
   1187 			. $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
   1188 
   1189 		if (strpos($Excerpt['text'], '>') !== false
   1190 			and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
   1191 		) {
   1192 			$url = $matches[1];
   1193 
   1194 			if (!isset($matches[2])) {
   1195 				$url = "mailto:$url";
   1196 			}
   1197 
   1198 			return array(
   1199 				'extent' => strlen($matches[0]),
   1200 				'element' => array(
   1201 					'name' => 'a',
   1202 					'text' => $matches[1],
   1203 					'attributes' => array(
   1204 						'href' => $url,
   1205 					),
   1206 				),
   1207 			);
   1208 		}
   1209 	}
   1210 
   1211 	protected function inlineEmphasis($Excerpt)
   1212 	{
   1213 		if (!isset($Excerpt['text'][1])) {
   1214 			return;
   1215 		}
   1216 
   1217 		$marker = $Excerpt['text'][0];
   1218 
   1219 		if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches)) {
   1220 			$emphasis = 'strong';
   1221 		} elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches)) {
   1222 			$emphasis = 'em';
   1223 		} else {
   1224 			return;
   1225 		}
   1226 
   1227 		return array(
   1228 			'extent' => strlen($matches[0]),
   1229 			'element' => array(
   1230 				'name' => $emphasis,
   1231 				'handler' => array(
   1232 					'function' => 'lineElements',
   1233 					'argument' => $matches[1],
   1234 					'destination' => 'elements',
   1235 				)
   1236 			),
   1237 		);
   1238 	}
   1239 
   1240 	protected function inlineEscapeSequence($Excerpt)
   1241 	{
   1242 		if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters)) {
   1243 			return array(
   1244 				'element' => array('rawHtml' => $Excerpt['text'][1]),
   1245 				'extent' => 2,
   1246 			);
   1247 		}
   1248 	}
   1249 
   1250 	protected function inlineImage($Excerpt)
   1251 	{
   1252 		if (!isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[') {
   1253 			return;
   1254 		}
   1255 
   1256 		$Excerpt['text'] = substr($Excerpt['text'], 1);
   1257 
   1258 		$Link = $this->inlineLink($Excerpt);
   1259 
   1260 		if ($Link === null) {
   1261 			return;
   1262 		}
   1263 
   1264 		$Inline = array(
   1265 			'extent' => $Link['extent'] + 1,
   1266 			'element' => array(
   1267 				'name' => 'img',
   1268 				'attributes' => array(
   1269 					'src' => $Link['element']['attributes']['href'],
   1270 					'alt' => $Link['element']['handler']['argument'],
   1271 				),
   1272 				'autobreak' => true,
   1273 			),
   1274 		);
   1275 
   1276 		$Inline['element']['attributes'] += $Link['element']['attributes'];
   1277 
   1278 		unset($Inline['element']['attributes']['href']);
   1279 
   1280 		return $Inline;
   1281 	}
   1282 
   1283 	protected function inlineLink($Excerpt)
   1284 	{
   1285 		$Element = array(
   1286 			'name' => 'a',
   1287 			'handler' => array(
   1288 				'function' => 'lineElements',
   1289 				'argument' => null,
   1290 				'destination' => 'elements',
   1291 			),
   1292 			'nonNestables' => array('Url', 'Link'),
   1293 			'attributes' => array(
   1294 				'href' => null,
   1295 				'title' => null,
   1296 			),
   1297 		);
   1298 
   1299 		$extent = 0;
   1300 
   1301 		$remainder = $Excerpt['text'];
   1302 
   1303 		if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches)) {
   1304 			$Element['handler']['argument'] = $matches[1];
   1305 
   1306 			$extent += strlen($matches[0]);
   1307 
   1308 			$remainder = substr($remainder, $extent);
   1309 		} else {
   1310 			return;
   1311 		}
   1312 
   1313 		if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches)) {
   1314 			$Element['attributes']['href'] = $matches[1];
   1315 
   1316 			if (isset($matches[2])) {
   1317 				$Element['attributes']['title'] = substr($matches[2], 1, -1);
   1318 			}
   1319 
   1320 			$extent += strlen($matches[0]);
   1321 		} else {
   1322 			if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches)) {
   1323 				$definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
   1324 				$definition = strtolower($definition);
   1325 
   1326 				$extent += strlen($matches[0]);
   1327 			} else {
   1328 				$definition = strtolower($Element['handler']['argument']);
   1329 			}
   1330 
   1331 			if (!isset($this->DefinitionData['Reference'][$definition])) {
   1332 				return;
   1333 			}
   1334 
   1335 			$Definition = $this->DefinitionData['Reference'][$definition];
   1336 
   1337 			$Element['attributes']['href'] = $Definition['url'];
   1338 			$Element['attributes']['title'] = $Definition['title'];
   1339 		}
   1340 
   1341 		return array(
   1342 			'extent' => $extent,
   1343 			'element' => $Element,
   1344 		);
   1345 	}
   1346 
   1347 	protected function inlineMarkup($Excerpt)
   1348 	{
   1349 		if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false) {
   1350 			return;
   1351 		}
   1352 
   1353 		if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches)) {
   1354 			return array(
   1355 				'element' => array('rawHtml' => $matches[0]),
   1356 				'extent' => strlen($matches[0]),
   1357 			);
   1358 		}
   1359 
   1360 		if ($Excerpt['text'][1] === '!' and preg_match('/^<!---?[^>-](?:-?+[^-])*-->/s', $Excerpt['text'], $matches)) {
   1361 			return array(
   1362 				'element' => array('rawHtml' => $matches[0]),
   1363 				'extent' => strlen($matches[0]),
   1364 			);
   1365 		}
   1366 
   1367 		if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+' . $this->regexHtmlAttribute . ')*+[ ]*+\/?>/s', $Excerpt['text'], $matches)) {
   1368 			return array(
   1369 				'element' => array('rawHtml' => $matches[0]),
   1370 				'extent' => strlen($matches[0]),
   1371 			);
   1372 		}
   1373 	}
   1374 
   1375 	protected function inlineSpecialCharacter($Excerpt)
   1376 	{
   1377 		if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
   1378 			and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
   1379 		) {
   1380 			return array(
   1381 				'element' => array('rawHtml' => '&' . $matches[1] . ';'),
   1382 				'extent' => strlen($matches[0]),
   1383 			);
   1384 		}
   1385 
   1386 		return;
   1387 	}
   1388 
   1389 	protected function inlineStrikethrough($Excerpt)
   1390 	{
   1391 		if (!isset($Excerpt['text'][1])) {
   1392 			return;
   1393 		}
   1394 
   1395 		if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches)) {
   1396 			return array(
   1397 				'extent' => strlen($matches[0]),
   1398 				'element' => array(
   1399 					'name' => 'del',
   1400 					'handler' => array(
   1401 						'function' => 'lineElements',
   1402 						'argument' => $matches[1],
   1403 						'destination' => 'elements',
   1404 					)
   1405 				),
   1406 			);
   1407 		}
   1408 	}
   1409 
   1410 	protected function inlineUrl($Excerpt)
   1411 	{
   1412 		if ($this->urlsLinked !== true or !isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/') {
   1413 			return;
   1414 		}
   1415 
   1416 		if (strpos($Excerpt['context'], 'http') !== false
   1417 			and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
   1418 		) {
   1419 			$url = $matches[0][0];
   1420 
   1421 			$Inline = array(
   1422 				'extent' => strlen($matches[0][0]),
   1423 				'position' => $matches[0][1],
   1424 				'element' => array(
   1425 					'name' => 'a',
   1426 					'text' => $url,
   1427 					'attributes' => array(
   1428 						'href' => $url,
   1429 					),
   1430 				),
   1431 			);
   1432 
   1433 			return $Inline;
   1434 		}
   1435 	}
   1436 
   1437 	protected function inlineUrlTag($Excerpt)
   1438 	{
   1439 		if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches)) {
   1440 			$url = $matches[1];
   1441 
   1442 			return array(
   1443 				'extent' => strlen($matches[0]),
   1444 				'element' => array(
   1445 					'name' => 'a',
   1446 					'text' => $url,
   1447 					'attributes' => array(
   1448 						'href' => $url,
   1449 					),
   1450 				),
   1451 			);
   1452 		}
   1453 	}
   1454 
   1455 	# ~
   1456 
   1457 	protected function unmarkedText($text)
   1458 	{
   1459 		$Inline = $this->inlineText($text);
   1460 		return $this->element($Inline['element']);
   1461 	}
   1462 
   1463 	#
   1464 	# Handlers
   1465 	#
   1466 
   1467 	protected function handle(array $Element)
   1468 	{
   1469 		if (isset($Element['handler'])) {
   1470 			if (!isset($Element['nonNestables'])) {
   1471 				$Element['nonNestables'] = array();
   1472 			}
   1473 
   1474 			if (is_string($Element['handler'])) {
   1475 				$function = $Element['handler'];
   1476 				$argument = $Element['text'];
   1477 				unset($Element['text']);
   1478 				$destination = 'rawHtml';
   1479 			} else {
   1480 				$function = $Element['handler']['function'];
   1481 				$argument = $Element['handler']['argument'];
   1482 				$destination = $Element['handler']['destination'];
   1483 			}
   1484 
   1485 			$Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
   1486 
   1487 			if ($destination === 'handler') {
   1488 				$Element = $this->handle($Element);
   1489 			}
   1490 
   1491 			unset($Element['handler']);
   1492 		}
   1493 
   1494 		return $Element;
   1495 	}
   1496 
   1497 	protected function handleElementRecursive(array $Element)
   1498 	{
   1499 		return $this->elementApplyRecursive(array($this, 'handle'), $Element);
   1500 	}
   1501 
   1502 	protected function handleElementsRecursive(array $Elements)
   1503 	{
   1504 		return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
   1505 	}
   1506 
   1507 	protected function elementApplyRecursive($closure, array $Element)
   1508 	{
   1509 		$Element = call_user_func($closure, $Element);
   1510 
   1511 		if (isset($Element['elements'])) {
   1512 			$Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
   1513 		} elseif (isset($Element['element'])) {
   1514 			$Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
   1515 		}
   1516 
   1517 		return $Element;
   1518 	}
   1519 
   1520 	protected function elementApplyRecursiveDepthFirst($closure, array $Element)
   1521 	{
   1522 		if (isset($Element['elements'])) {
   1523 			$Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
   1524 		} elseif (isset($Element['element'])) {
   1525 			$Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
   1526 		}
   1527 
   1528 		$Element = call_user_func($closure, $Element);
   1529 
   1530 		return $Element;
   1531 	}
   1532 
   1533 	protected function elementsApplyRecursive($closure, array $Elements)
   1534 	{
   1535 		foreach ($Elements as &$Element) {
   1536 			$Element = $this->elementApplyRecursive($closure, $Element);
   1537 		}
   1538 
   1539 		return $Elements;
   1540 	}
   1541 
   1542 	protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
   1543 	{
   1544 		foreach ($Elements as &$Element) {
   1545 			$Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
   1546 		}
   1547 
   1548 		return $Elements;
   1549 	}
   1550 
   1551 	protected function element(array $Element)
   1552 	{
   1553 		if ($this->safeMode) {
   1554 			$Element = $this->sanitiseElement($Element);
   1555 		}
   1556 
   1557 		# identity map if element has no handler
   1558 		$Element = $this->handle($Element);
   1559 
   1560 		$hasName = isset($Element['name']);
   1561 
   1562 		$markup = '';
   1563 
   1564 		if ($hasName) {
   1565 			$markup .= '<' . $Element['name'];
   1566 
   1567 			if (isset($Element['attributes'])) {
   1568 				foreach ($Element['attributes'] as $name => $value) {
   1569 					if ($value === null) {
   1570 						continue;
   1571 					}
   1572 
   1573 					$markup .= " $name=\"" . self::escape($value) . '"';
   1574 				}
   1575 			}
   1576 		}
   1577 
   1578 		$permitRawHtml = false;
   1579 
   1580 		if (isset($Element['text'])) {
   1581 			$text = $Element['text'];
   1582 		}
   1583 		// very strongly consider an alternative if you're writing an
   1584 		// extension
   1585 		elseif (isset($Element['rawHtml'])) {
   1586 			$text = $Element['rawHtml'];
   1587 
   1588 			$allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
   1589 			$permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
   1590 		}
   1591 
   1592 		$hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
   1593 
   1594 		if ($hasContent) {
   1595 			$markup .= $hasName ? '>' : '';
   1596 
   1597 			if (isset($Element['elements'])) {
   1598 				$markup .= $this->elements($Element['elements']);
   1599 			} elseif (isset($Element['element'])) {
   1600 				$markup .= $this->element($Element['element']);
   1601 			} else {
   1602 				if (!$permitRawHtml) {
   1603 					$markup .= self::escape($text, true);
   1604 				} else {
   1605 					$markup .= $text;
   1606 				}
   1607 			}
   1608 
   1609 			$markup .= $hasName ? '</' . $Element['name'] . '>' : '';
   1610 		} elseif ($hasName) {
   1611 			$markup .= ' />';
   1612 		}
   1613 
   1614 		return $markup;
   1615 	}
   1616 
   1617 	protected function elements(array $Elements)
   1618 	{
   1619 		$markup = '';
   1620 
   1621 		$autoBreak = true;
   1622 
   1623 		foreach ($Elements as $Element) {
   1624 			if (empty($Element)) {
   1625 				continue;
   1626 			}
   1627 
   1628 			$autoBreakNext = (isset($Element['autobreak'])
   1629 				? $Element['autobreak'] : isset($Element['name'])
   1630 			);
   1631 			// (autobreak === false) covers both sides of an element
   1632 			$autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
   1633 
   1634 			$markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
   1635 			$autoBreak = $autoBreakNext;
   1636 		}
   1637 
   1638 		$markup .= $autoBreak ? "\n" : '';
   1639 
   1640 		return $markup;
   1641 	}
   1642 
   1643 	# ~
   1644 
   1645 	protected function li($lines)
   1646 	{
   1647 		$Elements = $this->linesElements($lines);
   1648 
   1649 		if (!in_array('', $lines)
   1650 			and isset($Elements[0]) and isset($Elements[0]['name'])
   1651 			and $Elements[0]['name'] === 'p'
   1652 		) {
   1653 			unset($Elements[0]['name']);
   1654 		}
   1655 
   1656 		return $Elements;
   1657 	}
   1658 
   1659 	#
   1660 	# AST Convenience
   1661 	#
   1662 
   1663 	/**
   1664 	 * Replace occurrences $regexp with $Elements in $text. Return an array of
   1665 	 * elements representing the replacement.
   1666 	 */
   1667 	protected static function pregReplaceElements($regexp, $Elements, $text)
   1668 	{
   1669 		$newElements = array();
   1670 
   1671 		while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE)) {
   1672 			$offset = $matches[0][1];
   1673 			$before = substr($text, 0, $offset);
   1674 			$after = substr($text, $offset + strlen($matches[0][0]));
   1675 
   1676 			$newElements[] = array('text' => $before);
   1677 
   1678 			foreach ($Elements as $Element) {
   1679 				$newElements[] = $Element;
   1680 			}
   1681 
   1682 			$text = $after;
   1683 		}
   1684 
   1685 		$newElements[] = array('text' => $text);
   1686 
   1687 		return $newElements;
   1688 	}
   1689 
   1690 	#
   1691 	# Deprecated Methods
   1692 	#
   1693 
   1694 	function parse($text)
   1695 	{
   1696 		$markup = $this->text($text);
   1697 
   1698 		return $markup;
   1699 	}
   1700 
   1701 	protected function sanitiseElement(array $Element)
   1702 	{
   1703 		static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
   1704 		static $safeUrlNameToAtt = array(
   1705 			'a' => 'href',
   1706 			'img' => 'src',
   1707 		);
   1708 
   1709 		if (!isset($Element['name'])) {
   1710 			unset($Element['attributes']);
   1711 			return $Element;
   1712 		}
   1713 
   1714 		if (isset($safeUrlNameToAtt[$Element['name']])) {
   1715 			$Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
   1716 		}
   1717 
   1718 		if (!empty($Element['attributes'])) {
   1719 			foreach ($Element['attributes'] as $att => $val) {
   1720 				# filter out badly parsed attribute
   1721 				if (!preg_match($goodAttribute, $att)) {
   1722 					unset($Element['attributes'][$att]);
   1723 				} # dump onevent attribute
   1724 				elseif (self::striAtStart($att, 'on')) {
   1725 					unset($Element['attributes'][$att]);
   1726 				}
   1727 			}
   1728 		}
   1729 
   1730 		return $Element;
   1731 	}
   1732 
   1733 	protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
   1734 	{
   1735 		foreach ($this->safeLinksWhitelist as $scheme) {
   1736 			if (self::striAtStart($Element['attributes'][$attribute], $scheme)) {
   1737 				return $Element;
   1738 			}
   1739 		}
   1740 
   1741 		$Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
   1742 
   1743 		return $Element;
   1744 	}
   1745 
   1746 	#
   1747 	# Static Methods
   1748 	#
   1749 
   1750 	protected static function escape($text, $allowQuotes = false)
   1751 	{
   1752 		return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
   1753 	}
   1754 
   1755 	protected static function striAtStart($string, $needle)
   1756 	{
   1757 		$len = strlen($needle);
   1758 
   1759 		if ($len > strlen($string)) {
   1760 			return false;
   1761 		} else {
   1762 			return strtolower(substr($string, 0, $len)) === strtolower($needle);
   1763 		}
   1764 	}
   1765 
   1766 	static function instance($name = 'default')
   1767 	{
   1768 		if (isset(self::$instances[$name])) {
   1769 			return self::$instances[$name];
   1770 		}
   1771 
   1772 		$instance = new static();
   1773 
   1774 		self::$instances[$name] = $instance;
   1775 
   1776 		return $instance;
   1777 	}
   1778 
   1779 	private static $instances = array();
   1780 
   1781 	#
   1782 	# Fields
   1783 	#
   1784 
   1785 	protected $DefinitionData;
   1786 
   1787 	#
   1788 	# Read-Only
   1789 
   1790 	protected $specialCharacters = array(
   1791 		'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
   1792 	);
   1793 
   1794 	protected $StrongRegex = array(
   1795 		'*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
   1796 		'_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
   1797 	);
   1798 
   1799 	protected $EmRegex = array(
   1800 		'*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
   1801 		'_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
   1802 	);
   1803 
   1804 	protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
   1805 
   1806 	protected $voidElements = array(
   1807 		'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
   1808 	);
   1809 
   1810 	protected $textLevelElements = array(
   1811 		'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
   1812 		'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
   1813 		'i', 'rp', 'del', 'code', 'strike', 'marquee',
   1814 		'q', 'rt', 'ins', 'font', 'strong',
   1815 		's', 'tt', 'kbd', 'mark',
   1816 		'u', 'xm', 'sub', 'nobr',
   1817 		'sup', 'ruby',
   1818 		'var', 'span',
   1819 		'wbr', 'time',
   1820 	);
   1821 }