PageRenderTime 78ms CodeModel.GetById 42ms app.highlight 27ms RepoModel.GetById 1ms app.codeStats 1ms

/sources/ext/markdownify/markdownify.php

https://github.com/Arantor/Elkarte
PHP | 1494 lines | 941 code | 82 blank | 471 comment | 157 complexity | e4dfece984a91f2cebf15ed4631b36c1 MD5 | raw file
   1<?php
   2
   3/**
   4 * Markdownify converts HTML Markup to [Markdown][1] (by [John Gruber][2]. It
   5 * also supports [Markdown Extra][3] by [Michel Fortin][4] via Markdownify_Extra.
   6 *
   7 * It all started as `html2text.php` - a port of [Aaron Swartz'][5] [`html2text.py`][6] - but
   8 * got a long way since. This is far more than a mere port now!
   9 * Starting with version 2.0.0 this is a complete rewrite and cannot be
  10 * compared to Aaron Swatz' `html2text.py` anylonger. I'm now using a HTML parser
  11 * (see `parsehtml.php` which I also wrote) which makes most of the evil
  12 * RegEx magic go away and additionally it gives a much cleaner class
  13 * structure. Also notably is the fact that I now try to prevent regressions by
  14 * utilizing testcases of Michel Fortin's [MDTest][7].
  15 *
  16 * [1]: http://daringfireball.com/projects/markdown
  17 * [2]: http://daringfireball.com/
  18 * [3]: http://www.michelf.com/projects/php-markdown/extra/
  19 * [4]: http://www.michelf.com/
  20 * [5]: http://www.aaronsw.com/
  21 * [6]: http://www.aaronsw.com/2002/html2text/
  22 * [7]: http://article.gmane.org/gmane.text.markdown.general/2540
  23 *
  24 * @version 2.0.0 alpha
  25 * @author Milian Wolff (<mail@milianw.de>, <http://milianw.de>)
  26 * @license LGPL, see LICENSE_LGPL.txt and the summary below
  27 * @copyright (C) 2007  Milian Wolff
  28 *
  29 * This library is free software; you can redistribute it and/or
  30 * modify it under the terms of the GNU Lesser General Public
  31 * License as published by the Free Software Foundation; either
  32 * version 2.1 of the License, or (at your option) any later version.
  33 *
  34 * This library is distributed in the hope that it will be useful,
  35 * but WITHOUT ANY WARRANTY; without even the implied warranty of
  36 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  37 * Lesser General Public License for more details.
  38 *
  39 * You should have received a copy of the GNU Lesser General Public
  40 * License along with this library; if not, write to the Free Software
  41 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  42 */
  43/**
  44 * HTML Parser, see http://sf.net/projects/parseHTML
  45 */
  46require_once dirname(__FILE__) . '/parsehtml.php';
  47
  48/**
  49 * default configuration
  50 */
  51define('MDFY_LINKS_EACH_PARAGRAPH', false);
  52define('MDFY_BODYWIDTH', false);
  53define('MDFY_KEEPHTML', true);
  54
  55/**
  56 * HTML to Markdown converter class
  57 */
  58class Markdownify
  59{
  60	/**
  61	 * html parser object
  62	 *
  63	 * @var parseHTML
  64	 */
  65	var $parser;
  66
  67	/**
  68	 * markdown output
  69	 *
  70	 * @var string
  71	 */
  72	var $output;
  73
  74	/**
  75	 * stack with tags which where not converted to html
  76	 *
  77	 * @var array<string>
  78	 */
  79	var $notConverted = array();
  80
  81	/**
  82	 * skip conversion to markdown
  83	 *
  84	 * @var bool
  85	 */
  86	var $skipConversion = false;
  87	/* options */
  88
  89	/**
  90	 * keep html tags which cannot be converted to markdown
  91	 *
  92	 * @var bool
  93	 */
  94	var $keepHTML = false;
  95
  96	/**
  97	 * wrap output, set to 0 to skip wrapping
  98	 *
  99	 * @var int
 100	 */
 101	var $bodyWidth = 0;
 102
 103	/**
 104	 * minimum body width
 105	 *
 106	 * @var int
 107	 */
 108	var $minBodyWidth = 25;
 109
 110	/**
 111	 * display links after each paragraph
 112	 *
 113	 * @var bool
 114	 */
 115	var $linksAfterEachParagraph = false;
 116
 117	/**
 118	 * constructor, set options, setup parser
 119	 *
 120	 * @param bool $linksAfterEachParagraph wether or not to flush stacked links after each paragraph
 121	 *             defaults to false
 122	 * @param int $bodyWidth wether or not to wrap the output to the given width
 123	 *             defaults to false
 124	 * @param bool $keepHTML wether to keep non markdownable HTML or to discard it
 125	 *             defaults to true (HTML will be kept)
 126	 * @return void
 127	 */
 128	function Markdownify($linksAfterEachParagraph = MDFY_LINKS_EACH_PARAGRAPH, $bodyWidth = MDFY_BODYWIDTH, $keepHTML = MDFY_KEEPHTML)
 129	{
 130		$this->linksAfterEachParagraph = $linksAfterEachParagraph;
 131		$this->keepHTML = $keepHTML;
 132
 133		if ($bodyWidth > $this->minBodyWidth)
 134		{
 135			$this->bodyWidth = intval($bodyWidth);
 136		}
 137		else
 138		{
 139			$this->bodyWidth = false;
 140		}
 141
 142		$this->parser = new parseHTML;
 143		$this->parser->noTagsInCode = true;
 144
 145		# we don't have to do this every time
 146		$search = array();
 147		$replace = array();
 148		foreach ($this->escapeInText as $s => $r)
 149		{
 150			array_push($search, '#(?<!\\\)' . $s . '#U');
 151			array_push($replace, $r);
 152		}
 153		$this->escapeInText = array(
 154			'search' => $search,
 155			'replace' => $replace
 156		);
 157	}
 158
 159	/**
 160	 * parse a HTML string
 161	 *
 162	 * @param string $html
 163	 * @return string markdown formatted
 164	 */
 165	function parseString($html)
 166	{
 167		$this->parser->html = $html;
 168		$this->parse();
 169		return $this->output;
 170	}
 171	/**
 172	 * tags with elements which can be handled by markdown
 173	 *
 174	 * @var array<string>
 175	 */
 176	var $isMarkdownable = array(
 177		'p' => array(),
 178		'ul' => array(),
 179		'ol' => array(),
 180		'li' => array(),
 181		'br' => array(),
 182		'blockquote' => array(),
 183		'code' => array(),
 184		'pre' => array(),
 185		'a' => array(
 186			'href' => 'required',
 187			'title' => 'optional',
 188		),
 189		'strong' => array(),
 190		'b' => array(),
 191		'em' => array(),
 192		'i' => array(),
 193		'img' => array(
 194			'src' => 'required',
 195			'alt' => 'optional',
 196			'title' => 'optional',
 197		),
 198		'h1' => array(),
 199		'h2' => array(),
 200		'h3' => array(),
 201		'h4' => array(),
 202		'h5' => array(),
 203		'h6' => array(),
 204		'hr' => array(),
 205	);
 206
 207	/**
 208	 * html tags to be ignored (contents will be parsed)
 209	 *
 210	 * @var array<string>
 211	 */
 212	var $ignore = array(
 213		'html',
 214		'body',
 215	);
 216
 217	/**
 218	 * html tags to be dropped (contents will not be parsed!)
 219	 *
 220	 * @var array<string>
 221	 */
 222	var $drop = array(
 223		'script',
 224		'head',
 225		'style',
 226		'form',
 227		'area',
 228		'object',
 229		'param',
 230		'iframe',
 231	);
 232
 233	/**
 234	 * Markdown indents which could be wrapped
 235	 * @note: use strings in regex format
 236	 *
 237	 * @var array<string>
 238	 */
 239	var $wrappableIndents = array(
 240		'\*   ', # ul
 241		'\d.  ', # ol
 242		'\d\d. ', # ol
 243		'> ', # blockquote
 244		'', # p
 245	);
 246
 247	/**
 248	 * list of chars which have to be escaped in normal text
 249	 * @note: use strings in regex format
 250	 *
 251	 * @var array
 252	 *
 253	 * TODO: what's with block chars / sequences at the beginning of a block?
 254	 */
 255	var $escapeInText = array(
 256		'([-*_])([ ]{0,2}\1){2,}' => '\\\\$0|', # hr
 257		'\*\*([^*\s]+)\*\*' => '\*\*$1\*\*', # strong
 258		'\*([^*\s]+)\*' => '\*$1\*', # em
 259		'__(?! |_)(.+)(?!<_| )__' => '\_\_$1\_\_', # em
 260		'_(?! |_)(.+)(?!<_| )_' => '\_$1\_', # em
 261		'`(.+)`' => '\`$1\`', # code
 262		'\[(.+)\](\s*\()' => '\[$1\]$2', # links: [text] (url) => [text\] (url)
 263		'\[(.+)\](\s*)\[(.*)\]' => '\[$1\]$2\[$3\]', # links: [text][id] => [text\][id\]
 264	);
 265
 266	/**
 267	 * wether last processed node was a block tag or not
 268	 *
 269	 * @var bool
 270	 */
 271	var $lastWasBlockTag = false;
 272
 273	/**
 274	 * name of last closed tag
 275	 *
 276	 * @var string
 277	 */
 278	var $lastClosedTag = '';
 279
 280	/**
 281	 * iterate through the nodes and decide what we
 282	 * shall do with the current node
 283	 *
 284	 * @param void
 285	 * @return void
 286	 */
 287	function parse()
 288	{
 289		$this->output = '';
 290		# drop tags
 291		$this->parser->html = preg_replace('#<(' . implode('|', $this->drop) . ')[^>]*>.*</\\1>#sU', '', $this->parser->html);
 292		while ($this->parser->nextNode())
 293		{
 294			switch ($this->parser->nodeType)
 295			{
 296				case 'doctype':
 297					break;
 298				case 'pi':
 299				case 'comment':
 300					if ($this->keepHTML)
 301					{
 302						$this->flushLinebreaks();
 303						$this->out($this->parser->node);
 304						$this->setLineBreaks(2);
 305					}
 306					# else drop
 307					break;
 308				case 'text':
 309					$this->handleText();
 310					break;
 311				case 'tag':
 312					if (in_array($this->parser->tagName, $this->ignore))
 313					{
 314						break;
 315					}
 316					if ($this->parser->isStartTag)
 317					{
 318						$this->flushLinebreaks();
 319					}
 320					if ($this->skipConversion)
 321					{
 322						$this->isMarkdownable(); # update notConverted
 323						$this->handleTagToText();
 324						continue;
 325					}
 326					if (!$this->parser->keepWhitespace && $this->parser->isBlockElement && $this->parser->isStartTag)
 327					{
 328						$this->parser->html = ltrim($this->parser->html);
 329					}
 330					if ($this->isMarkdownable())
 331					{
 332						if ($this->parser->isBlockElement && $this->parser->isStartTag && !$this->lastWasBlockTag && !empty($this->output))
 333						{
 334							if (!empty($this->buffer))
 335							{
 336								$str = & $this->buffer[count($this->buffer) - 1];
 337							}
 338							else
 339							{
 340								$str = & $this->output;
 341							}
 342							if (substr($str, -strlen($this->indent) - 1) != "\n" . $this->indent)
 343							{
 344								$str .= "\n" . $this->indent;
 345							}
 346						}
 347						$func = 'handleTag_' . $this->parser->tagName;
 348						$this->$func();
 349						if ($this->linksAfterEachParagraph && $this->parser->isBlockElement && !$this->parser->isStartTag && empty($this->parser->openTags))
 350						{
 351							$this->flushStacked();
 352						}
 353						if (!$this->parser->isStartTag)
 354						{
 355							$this->lastClosedTag = $this->parser->tagName;
 356						}
 357					}
 358					else
 359					{
 360						$this->handleTagToText();
 361						$this->lastClosedTag = '';
 362					}
 363					break;
 364				default:
 365					trigger_error('invalid node type', E_USER_ERROR);
 366					break;
 367			}
 368			$this->lastWasBlockTag = $this->parser->nodeType == 'tag' && $this->parser->isStartTag && $this->parser->isBlockElement;
 369		}
 370		if (!empty($this->buffer))
 371		{
 372			trigger_error('buffer was not flushed, this is a bug. please report!', E_USER_WARNING);
 373			while (!empty($this->buffer))
 374			{
 375				$this->out($this->unbuffer());
 376			}
 377		}
 378		### cleanup
 379		$this->output = rtrim(str_replace('&amp;', '&', str_replace('&lt;', '<', str_replace('&gt;', '>', $this->output))));
 380		# end parsing, flush stacked tags
 381		$this->flushStacked();
 382		$this->stack = array();
 383	}
 384
 385	/**
 386	 * check if current tag can be converted to Markdown
 387	 *
 388	 * @param void
 389	 * @return bool
 390	 */
 391	function isMarkdownable()
 392	{
 393		if (!isset($this->isMarkdownable[$this->parser->tagName]))
 394		{
 395			# simply not markdownable
 396			return false;
 397		}
 398		if ($this->parser->isStartTag)
 399		{
 400			$return = true;
 401			if ($this->keepHTML)
 402			{
 403				$diff = array_diff(array_keys($this->parser->tagAttributes), array_keys($this->isMarkdownable[$this->parser->tagName]));
 404				if (!empty($diff))
 405				{
 406					# non markdownable attributes given
 407					$return = false;
 408				}
 409			}
 410			if ($return)
 411			{
 412				foreach ($this->isMarkdownable[$this->parser->tagName] as $attr => $type)
 413				{
 414					if ($type == 'required' && !isset($this->parser->tagAttributes[$attr]))
 415					{
 416						# required markdown attribute not given
 417						$return = false;
 418						break;
 419					}
 420				}
 421			}
 422			if (!$return)
 423			{
 424				array_push($this->notConverted, $this->parser->tagName . '::' . implode('/', $this->parser->openTags));
 425			}
 426			return $return;
 427		}
 428		else
 429		{
 430			if (!empty($this->notConverted) && end($this->notConverted) === $this->parser->tagName . '::' . implode('/', $this->parser->openTags))
 431			{
 432				array_pop($this->notConverted);
 433				return false;
 434			}
 435			return true;
 436		}
 437	}
 438
 439	/**
 440	 * output all stacked tags
 441	 *
 442	 * @param void
 443	 * @return void
 444	 */
 445	function flushStacked()
 446	{
 447		# links
 448		foreach ($this->stack as $tag => $a)
 449		{
 450			if (!empty($a))
 451			{
 452				call_user_func(array(&$this, 'flushStacked_' . $tag));
 453			}
 454		}
 455	}
 456
 457	/**
 458	 * output link references (e.g. [1]: http://example.com "title");
 459	 *
 460	 * @param void
 461	 * @return void
 462	 */
 463	function flushStacked_a()
 464	{
 465		$out = false;
 466		foreach ($this->stack['a'] as $k => $tag)
 467		{
 468			if (!isset($tag['unstacked']))
 469			{
 470				if (!$out)
 471				{
 472					$out = true;
 473					$this->out("\n\n", true);
 474				}
 475				else
 476				{
 477					$this->out("\n", true);
 478				}
 479				$this->out(' [' . $tag['linkID'] . ']: ' . $tag['href'] . (isset($tag['title']) ? ' "' . $tag['title'] . '"' : ''), true);
 480				$tag['unstacked'] = true;
 481				$this->stack['a'][$k] = $tag;
 482			}
 483		}
 484	}
 485
 486	/**
 487	 * flush enqued linebreaks
 488	 *
 489	 * @param void
 490	 * @return void
 491	 */
 492	function flushLinebreaks()
 493	{
 494		if ($this->lineBreaks && !empty($this->output))
 495		{
 496			$this->out(str_repeat("\n" . $this->indent, $this->lineBreaks), true);
 497		}
 498		$this->lineBreaks = 0;
 499	}
 500
 501	/**
 502	 * handle non Markdownable tags
 503	 *
 504	 * @param void
 505	 * @return void
 506	 */
 507	function handleTagToText()
 508	{
 509		if (!$this->keepHTML)
 510		{
 511			if (!$this->parser->isStartTag && $this->parser->isBlockElement)
 512			{
 513				$this->setLineBreaks(2);
 514			}
 515		}
 516		else
 517		{
 518			# dont convert to markdown inside this tag
 519			/** TODO: markdown extra * */
 520			if (!$this->parser->isEmptyTag)
 521			{
 522				if ($this->parser->isStartTag)
 523				{
 524					if (!$this->skipConversion)
 525					{
 526						$this->skipConversion = $this->parser->tagName . '::' . implode('/', $this->parser->openTags);
 527					}
 528				}
 529				else
 530				{
 531					if ($this->skipConversion == $this->parser->tagName . '::' . implode('/', $this->parser->openTags))
 532					{
 533						$this->skipConversion = false;
 534					}
 535				}
 536			}
 537
 538			if ($this->parser->isBlockElement)
 539			{
 540				if ($this->parser->isStartTag)
 541				{
 542					if (in_array($this->parent(), array('ins', 'del')))
 543					{
 544						# looks like ins or del are block elements now
 545						$this->out("\n", true);
 546						$this->indent('  ');
 547					}
 548					if ($this->parser->tagName != 'pre')
 549					{
 550						$this->out($this->parser->node . "\n" . $this->indent);
 551						if (!$this->parser->isEmptyTag)
 552						{
 553							$this->indent('  ');
 554						}
 555						else
 556						{
 557							$this->setLineBreaks(1);
 558						}
 559						$this->parser->html = ltrim($this->parser->html);
 560					}
 561					else
 562					{
 563						# don't indent inside <pre> tags
 564						$this->out($this->parser->node);
 565						static $indent;
 566						$indent = $this->indent;
 567						$this->indent = '';
 568					}
 569				}
 570				else
 571				{
 572					if (!$this->parser->keepWhitespace)
 573					{
 574						$this->output = rtrim($this->output);
 575					}
 576					if ($this->parser->tagName != 'pre')
 577					{
 578						$this->indent('  ');
 579						$this->out("\n" . $this->indent . $this->parser->node);
 580					}
 581					else
 582					{
 583						# reset indentation
 584						$this->out($this->parser->node);
 585						static $indent;
 586						$this->indent = $indent;
 587					}
 588
 589					if (in_array($this->parent(), array('ins', 'del')))
 590					{
 591						# ins or del was block element
 592						$this->out("\n");
 593						$this->indent('  ');
 594					}
 595					if ($this->parser->tagName == 'li')
 596					{
 597						$this->setLineBreaks(1);
 598					}
 599					else
 600					{
 601						$this->setLineBreaks(2);
 602					}
 603				}
 604			}
 605			else
 606			{
 607				$this->out($this->parser->node);
 608			}
 609			if (in_array($this->parser->tagName, array('code', 'pre')))
 610			{
 611				if ($this->parser->isStartTag)
 612				{
 613					$this->buffer();
 614				}
 615				else
 616				{
 617					# add stuff so cleanup just reverses this
 618					$this->out(str_replace('&lt;', '&amp;lt;', str_replace('&gt;', '&amp;gt;', $this->unbuffer())));
 619				}
 620			}
 621		}
 622	}
 623
 624	/**
 625	 * handle plain text
 626	 *
 627	 * @param void
 628	 * @return void
 629	 */
 630	function handleText()
 631	{
 632		if ($this->hasParent('pre') && strpos($this->parser->node, "\n") !== false)
 633		{
 634			$this->parser->node = str_replace("\n", "\n" . $this->indent, $this->parser->node);
 635		}
 636		if (!$this->hasParent('code') && !$this->hasParent('pre'))
 637		{
 638			# entity decode
 639			$this->parser->node = $this->decode($this->parser->node);
 640			if (!$this->skipConversion)
 641			{
 642				# escape some chars in normal Text
 643				$this->parser->node = preg_replace($this->escapeInText['search'], $this->escapeInText['replace'], $this->parser->node);
 644			}
 645		}
 646		else
 647		{
 648			$this->parser->node = str_replace(array('&quot;', '&apos'), array('"', '\''), $this->parser->node);
 649		}
 650		$this->out($this->parser->node);
 651		$this->lastClosedTag = '';
 652	}
 653
 654	/**
 655	 * handle <em> and <i> tags
 656	 *
 657	 * @param void
 658	 * @return void
 659	 */
 660	function handleTag_em()
 661	{
 662		$this->out('*', true);
 663	}
 664
 665	function handleTag_i()
 666	{
 667		$this->handleTag_em();
 668	}
 669
 670	/**
 671	 * handle <strong> and <b> tags
 672	 *
 673	 * @param void
 674	 * @return void
 675	 */
 676	function handleTag_strong()
 677	{
 678		$this->out('**', true);
 679	}
 680
 681	function handleTag_b()
 682	{
 683		$this->handleTag_strong();
 684	}
 685
 686	/**
 687	 * handle <h1> tags
 688	 *
 689	 * @param void
 690	 * @return void
 691	 */
 692	function handleTag_h1()
 693	{
 694		$this->handleHeader(1);
 695	}
 696
 697	/**
 698	 * handle <h2> tags
 699	 *
 700	 * @param void
 701	 * @return void
 702	 */
 703	function handleTag_h2()
 704	{
 705		$this->handleHeader(2);
 706	}
 707
 708	/**
 709	 * handle <h3> tags
 710	 *
 711	 * @param void
 712	 * @return void
 713	 */
 714	function handleTag_h3()
 715	{
 716		$this->handleHeader(3);
 717	}
 718
 719	/**
 720	 * handle <h4> tags
 721	 *
 722	 * @param void
 723	 * @return void
 724	 */
 725	function handleTag_h4()
 726	{
 727		$this->handleHeader(4);
 728	}
 729
 730	/**
 731	 * handle <h5> tags
 732	 *
 733	 * @param void
 734	 * @return void
 735	 */
 736	function handleTag_h5()
 737	{
 738		$this->handleHeader(5);
 739	}
 740
 741	/**
 742	 * handle <h6> tags
 743	 *
 744	 * @param void
 745	 * @return void
 746	 */
 747	function handleTag_h6()
 748	{
 749		$this->handleHeader(6);
 750	}
 751	
 752	/**
 753	 * number of line breaks before next inline output
 754	 */
 755	var $lineBreaks = 0;
 756
 757	/**
 758	 * handle header tags (<h1> - <h6>)
 759	 *
 760	 * @param int $level 1-6
 761	 * @return void
 762	 */
 763	function handleHeader($level)
 764	{
 765		if ($this->parser->isStartTag)
 766		{
 767			$this->out(str_repeat('#', $level) . ' ', true);
 768		}
 769		else
 770		{
 771			$this->setLineBreaks(2);
 772		}
 773	}
 774
 775	/**
 776	 * handle <p> tags
 777	 *
 778	 * @param void
 779	 * @return void
 780	 */
 781	function handleTag_p()
 782	{
 783		if (!$this->parser->isStartTag)
 784		{
 785			$this->setLineBreaks(2);
 786		}
 787	}
 788
 789	/**
 790	 * handle <a> tags
 791	 *
 792	 * @param void
 793	 * @return void
 794	 */
 795	function handleTag_a()
 796	{
 797		if ($this->parser->isStartTag)
 798		{
 799			$this->buffer();
 800			if (isset($this->parser->tagAttributes['title']))
 801			{
 802				$this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']);
 803			}
 804			else
 805			{
 806				$this->parser->tagAttributes['title'] = null;
 807			}
 808			$this->parser->tagAttributes['href'] = $this->decode(trim($this->parser->tagAttributes['href']));
 809			$this->stack();
 810		}
 811		else
 812		{
 813			$tag = $this->unstack();
 814			$buffer = $this->unbuffer();
 815
 816			if (empty($tag['href']) && empty($tag['title']))
 817			{
 818				# empty links... testcase mania, who would possibly do anything like that?!
 819				$this->out('[' . $buffer . ']()', true);
 820				return;
 821			}
 822
 823			if ($buffer == $tag['href'] && empty($tag['title']))
 824			{
 825				# <http://example.com>
 826				$this->out('<' . $buffer . '>', true);
 827				return;
 828			}
 829
 830			$bufferDecoded = $this->decode(trim($buffer));
 831			if (substr($tag['href'], 0, 7) == 'mailto:' && 'mailto:' . $bufferDecoded == $tag['href'])
 832			{
 833				if (is_null($tag['title']))
 834				{
 835					# <mail@example.com>
 836					$this->out('<' . $bufferDecoded . '>', true);
 837					return;
 838				}
 839				# [mail@example.com][1]
 840				# ...
 841				#  [1]: mailto:mail@example.com Title
 842				$tag['href'] = 'mailto:' . $bufferDecoded;
 843			}
 844			# [This link][id]
 845			foreach ($this->stack['a'] as $tag2)
 846			{
 847				if ($tag2['href'] == $tag['href'] && $tag2['title'] === $tag['title'])
 848				{
 849					$tag['linkID'] = $tag2['linkID'];
 850					break;
 851				}
 852			}
 853			
 854			// Inline Style for our bbc links
 855			if (isset($tag['class']) && $tag['class'] == 'bbc_link')
 856			{
 857				$tag['linkID'] = $tag['href'];
 858				$this->out('[' . $buffer . '](' . $tag['href'] . ')', true);
 859				return;
 860			}
 861			
 862			if (!isset($tag['linkID']))
 863			{
 864				$tag['linkID'] = count($this->stack['a']) + 1;
 865				array_push($this->stack['a'], $tag);
 866			}
 867
 868			$this->out('[' . $buffer . '][' . $tag['linkID'] . ']', true);
 869		}
 870	}
 871
 872	/**
 873	 * handle <img /> tags
 874	 *
 875	 * @param void
 876	 * @return void
 877	 */
 878	function handleTag_img()
 879	{
 880		if (!$this->parser->isStartTag)
 881		{
 882			return; # just to be sure this is really an empty tag...
 883		}
 884
 885		if (isset($this->parser->tagAttributes['title']))
 886		{
 887			$this->parser->tagAttributes['title'] = $this->decode($this->parser->tagAttributes['title']);
 888		}
 889		else
 890		{
 891			$this->parser->tagAttributes['title'] = null;
 892		}
 893		
 894		if (isset($this->parser->tagAttributes['alt']))
 895		{
 896			$this->parser->tagAttributes['alt'] = $this->decode($this->parser->tagAttributes['alt']);
 897		}
 898		else
 899		{
 900			$this->parser->tagAttributes['alt'] = null;
 901		}
 902
 903		if (empty($this->parser->tagAttributes['src']))
 904		{
 905			# support for "empty" images... dunno if this is really needed
 906			# but there are some testcases which do that...
 907			if (!empty($this->parser->tagAttributes['title']))
 908			{
 909				$this->parser->tagAttributes['title'] = ' ' . $this->parser->tagAttributes['title'] . ' ';
 910			}
 911			$this->out('![' . $this->parser->tagAttributes['alt'] . '](' . $this->parser->tagAttributes['title'] . ')', true);
 912			return;
 913		}
 914		else
 915		{
 916			$this->parser->tagAttributes['src'] = $this->decode($this->parser->tagAttributes['src']);
 917		}
 918
 919		# [This link][id]
 920		$link_id = false;
 921		if (!empty($this->stack['a']))
 922		{
 923			foreach ($this->stack['a'] as $tag)
 924			{
 925				if ($tag['href'] == $this->parser->tagAttributes['src']
 926						&& $tag['title'] === $this->parser->tagAttributes['title'])
 927				{
 928					$link_id = $tag['linkID'];
 929					break;
 930				}
 931			}
 932		}
 933		else
 934		{
 935			$this->stack['a'] = array();
 936		}
 937		if (!$link_id)
 938		{
 939			$link_id = count($this->stack['a']) + 1;
 940			$tag = array(
 941				'href' => $this->parser->tagAttributes['src'],
 942				'linkID' => $link_id,
 943				'title' => $this->parser->tagAttributes['title']
 944			);
 945			array_push($this->stack['a'], $tag);
 946		}
 947
 948		$this->out('![' . $this->parser->tagAttributes['alt'] . '][' . $link_id . ']', true);
 949	}
 950
 951	/**
 952	 * handle <code> tags
 953	 *
 954	 * @param void
 955	 * @return void
 956	 */
 957	function handleTag_code()
 958	{
 959		if ($this->hasParent('pre'))
 960		{
 961			# ignore code blocks inside <pre>
 962			return;
 963		}
 964		if ($this->parser->isStartTag)
 965		{
 966			$this->buffer();
 967		}
 968		else
 969		{
 970			$buffer = $this->unbuffer();
 971			# use as many backticks as needed
 972			preg_match_all('#`+#', $buffer, $matches);
 973			if (!empty($matches[0]))
 974			{
 975				rsort($matches[0]);
 976
 977				$ticks = '`';
 978				while (true)
 979				{
 980					if (!in_array($ticks, $matches[0]))
 981					{
 982						break;
 983					}
 984					$ticks .= '`';
 985				}
 986			}
 987			else
 988			{
 989				$ticks = '`';
 990			}
 991			if ($buffer[0] == '`' || substr($buffer, -1) == '`')
 992			{
 993				$buffer = ' ' . $buffer . ' ';
 994			}
 995			$this->out($ticks . $buffer . $ticks, true);
 996		}
 997	}
 998
 999	/**
1000	 * handle <pre> tags
1001	 *
1002	 * @param void
1003	 * @return void
1004	 */
1005	function handleTag_pre()
1006	{
1007		if ($this->keepHTML && $this->parser->isStartTag)
1008		{
1009			# check if a simple <code> follows
1010			if (!preg_match('#^\s*<code\s*>#Us', $this->parser->html))
1011			{
1012				# this is no standard markdown code block
1013				$this->handleTagToText();
1014				return;
1015			}
1016		}
1017		$this->indent('    ');
1018		if (!$this->parser->isStartTag)
1019		{
1020			$this->setLineBreaks(2);
1021		}
1022		else
1023		{
1024			$this->parser->html = ltrim($this->parser->html);
1025		}
1026	}
1027
1028	/**
1029	 * handle <blockquote> tags
1030	 *
1031	 * @param void
1032	 * @return void
1033	 */
1034	function handleTag_blockquote()
1035	{
1036		$this->indent('> ');
1037	}
1038
1039	/**
1040	 * handle <ul> tags
1041	 *
1042	 * @param void
1043	 * @return void
1044	 */
1045	function handleTag_ul()
1046	{
1047		if ($this->parser->isStartTag)
1048		{
1049			$this->stack();
1050			if (!$this->keepHTML && $this->lastClosedTag == $this->parser->tagName)
1051			{
1052				$this->out("\n" . $this->indent . '<!-- -->' . "\n" . $this->indent . "\n" . $this->indent);
1053			}
1054		}
1055		else
1056		{
1057			$this->unstack();
1058			if ($this->parent() != 'li' || preg_match('#^\s*(</li\s*>\s*<li\s*>\s*)?<(p|blockquote)\s*>#sU', $this->parser->html))
1059			{
1060				# dont make Markdown add unneeded paragraphs
1061				$this->setLineBreaks(2);
1062			}
1063		}
1064	}
1065
1066	/**
1067	 * handle <ul> tags
1068	 *
1069	 * @param void
1070	 * @return void
1071	 */
1072	function handleTag_ol()
1073	{
1074		# same as above
1075		$this->parser->tagAttributes['num'] = 0;
1076		$this->handleTag_ul();
1077	}
1078
1079	/**
1080	 * handle <li> tags
1081	 *
1082	 * @param void
1083	 * @return void
1084	 */
1085	function handleTag_li()
1086	{
1087		if ($this->parent() == 'ol')
1088		{
1089			$parent = & $this->getStacked('ol');
1090			if ($this->parser->isStartTag)
1091			{
1092				$parent['num']++;
1093				$this->out($parent['num'] . '.' . str_repeat(' ', 3 - strlen($parent['num'])), true);
1094			}
1095			$this->indent('    ', false);
1096		}
1097		else
1098		{
1099			if ($this->parser->isStartTag)
1100			{
1101				$this->out('*   ', true);
1102			}
1103			$this->indent('    ', false);
1104		}
1105		if (!$this->parser->isStartTag)
1106		{
1107			$this->setLineBreaks(1);
1108		}
1109	}
1110
1111	/**
1112	 * handle <hr /> tags
1113	 *
1114	 * @param void
1115	 * @return void
1116	 */
1117	function handleTag_hr()
1118	{
1119		if (!$this->parser->isStartTag)
1120		{
1121			return; # just to be sure this really is an empty tag
1122		}
1123		$this->out('* * *', true);
1124		$this->setLineBreaks(2);
1125	}
1126
1127	/**
1128	 * handle <br /> tags
1129	 *
1130	 * @param void
1131	 * @return void
1132	 */
1133	function handleTag_br()
1134	{
1135		$this->out("  \n" . $this->indent, true);
1136		$this->parser->html = ltrim($this->parser->html);
1137	}
1138	/**
1139	 * node stack, e.g. for <a> and <abbr> tags
1140	 *
1141	 * @var array<array>
1142	 */
1143	var $stack = array();
1144
1145	/**
1146	 * add current node to the stack
1147	 * this only stores the attributes
1148	 *
1149	 * @param void
1150	 * @return void
1151	 */
1152	function stack()
1153	{
1154		if (!isset($this->stack[$this->parser->tagName]))
1155		{
1156			$this->stack[$this->parser->tagName] = array();
1157		}
1158		array_push($this->stack[$this->parser->tagName], $this->parser->tagAttributes);
1159	}
1160
1161	/**
1162	 * remove current tag from stack
1163	 *
1164	 * @param void
1165	 * @return array
1166	 */
1167	function unstack()
1168	{
1169		if (!isset($this->stack[$this->parser->tagName]) || !is_array($this->stack[$this->parser->tagName]))
1170		{
1171			trigger_error('Trying to unstack from empty stack. This must not happen.', E_USER_ERROR);
1172		}
1173		return array_pop($this->stack[$this->parser->tagName]);
1174	}
1175
1176	/**
1177	 * get last stacked element of type $tagName
1178	 *
1179	 * @param string $tagName
1180	 * @return array
1181	 */
1182	function & getStacked($tagName)
1183	{
1184		// no end() so it can be referenced
1185		return $this->stack[$tagName][count($this->stack[$tagName]) - 1];
1186	}
1187
1188	/**
1189	 * set number of line breaks before next start tag
1190	 *
1191	 * @param int $number
1192	 * @return void
1193	 */
1194	function setLineBreaks($number)
1195	{
1196		if ($this->lineBreaks < $number)
1197		{
1198			$this->lineBreaks = $number;
1199		}
1200	}
1201	/**
1202	 * stores current buffers
1203	 *
1204	 * @var array<string>
1205	 */
1206	var $buffer = array();
1207
1208	/**
1209	 * buffer next parser output until unbuffer() is called
1210	 *
1211	 * @param void
1212	 * @return void
1213	 */
1214	function buffer()
1215	{
1216		array_push($this->buffer, '');
1217	}
1218
1219	/**
1220	 * end current buffer and return buffered output
1221	 *
1222	 * @param void
1223	 * @return string
1224	 */
1225	function unbuffer()
1226	{
1227		return array_pop($this->buffer);
1228	}
1229
1230	/**
1231	 * append string to the correct var, either
1232	 * directly to $this->output or to the current
1233	 * buffers
1234	 *
1235	 * @param string $put
1236	 * @return void
1237	 */
1238	function out($put, $nowrap = false)
1239	{
1240		if (empty($put))
1241		{
1242			return;
1243		}
1244		if (!empty($this->buffer))
1245		{
1246			$this->buffer[count($this->buffer) - 1] .= $put;
1247		}
1248		else
1249		{
1250			if ($this->bodyWidth && !$this->parser->keepWhitespace)
1251			{ # wrap lines
1252				// get last line
1253				$pos = strrpos($this->output, "\n");
1254				if ($pos === false)
1255				{
1256					$line = $this->output;
1257				}
1258				else
1259				{
1260					$line = substr($this->output, $pos);
1261				}
1262
1263				if ($nowrap)
1264				{
1265					if ($put[0] != "\n" && $this->strlen($line) + $this->strlen($put) > $this->bodyWidth)
1266					{
1267						$this->output .= "\n" . $this->indent . $put;
1268					}
1269					else
1270					{
1271						$this->output .= $put;
1272					}
1273					return;
1274				}
1275				else
1276				{
1277					$put .= "\n"; # make sure we get all lines in the while below
1278					$lineLen = $this->strlen($line);
1279					while ($pos = strpos($put, "\n"))
1280					{
1281						$putLine = substr($put, 0, $pos + 1);
1282						$put = substr($put, $pos + 1);
1283						$putLen = $this->strlen($putLine);
1284						if ($lineLen + $putLen < $this->bodyWidth)
1285						{
1286							$this->output .= $putLine;
1287							$lineLen = $putLen;
1288						}
1289						else
1290						{
1291							$split = preg_split('#^(.{0,' . ($this->bodyWidth - $lineLen) . '})\b#', $putLine, 2, PREG_SPLIT_OFFSET_CAPTURE | PREG_SPLIT_DELIM_CAPTURE);
1292							$this->output .= rtrim($split[1][0]) . "\n" . $this->indent . $this->wordwrap(ltrim($split[2][0]), $this->bodyWidth, "\n" . $this->indent, false);
1293							$this->output = rtrim($this->output, $this->indent);
1294						}
1295					}
1296					$this->output = substr($this->output, 0, -1);
1297					return;
1298				}
1299			}
1300			else
1301			{
1302				$this->output .= $put;
1303			}
1304		}
1305	}
1306	/**
1307	 * current indentation
1308	 *
1309	 * @var string
1310	 */
1311	var $indent = '';
1312
1313	/**
1314	 * indent next output (start tag) or unindent (end tag)
1315	 *
1316	 * @param string $str indentation
1317	 * @param bool $output add indendation to output
1318	 * @return void
1319	 */
1320	function indent($str, $output = true)
1321	{
1322		if ($this->parser->isStartTag)
1323		{
1324			$this->indent .= $str;
1325			if ($output)
1326			{
1327				$this->out($str, true);
1328			}
1329		}
1330		else
1331		{
1332			$this->indent = substr($this->indent, 0, -strlen($str));
1333		}
1334	}
1335
1336	/**
1337	 * decode email addresses
1338	 *
1339	 * @author derernst@gmx.ch <http://www.php.net/manual/en/function.html-entity-decode.php#68536>
1340	 * @author Milian Wolff <http://milianw.de>
1341	 */
1342	function decode($text, $quote_style = ENT_QUOTES)
1343	{
1344		if (version_compare(PHP_VERSION, '5', '>='))
1345		{
1346			# UTF-8 is only supported in PHP 5.x.x and above
1347			$text = html_entity_decode($text, $quote_style, 'UTF-8');
1348		}
1349		else
1350		{
1351			if (function_exists('html_entity_decode'))
1352			{
1353				$text = html_entity_decode($text, $quote_style, 'ISO-8859-1');
1354			}
1355			else
1356			{
1357				static $trans_tbl;
1358				if (!isset($trans_tbl))
1359				{
1360					$trans_tbl = array_flip(get_html_translation_table(HTML_ENTITIES, $quote_style));
1361				}
1362				$text = strtr($text, $trans_tbl);
1363			}
1364			$text = preg_replace_callback('~&#x([0-9a-f]+);~i', array(&$this, '_decode_hex'), $text);
1365			$text = preg_replace_callback('~&#(\d{2,5});~', array(&$this, '_decode_numeric'), $text);
1366		}
1367		return $text;
1368	}
1369
1370	/**
1371	 * callback for decode() which converts a hexadecimal entity to UTF-8
1372	 *
1373	 * @param array $matches
1374	 * @return string UTF-8 encoded
1375	 */
1376	function _decode_hex($matches)
1377	{
1378		return $this->unichr(hexdec($matches[1]));
1379	}
1380
1381	/**
1382	 * callback for decode() which converts a numerical entity to UTF-8
1383	 *
1384	 * @param array $matches
1385	 * @return string UTF-8 encoded
1386	 */
1387	function _decode_numeric($matches)
1388	{
1389		return $this->unichr($matches[1]);
1390	}
1391
1392	/**
1393	 * UTF-8 chr() which supports numeric entities
1394	 *
1395	 * @author grey - greywyvern - com <http://www.php.net/manual/en/function.chr.php#55978>
1396	 * @param array $matches
1397	 * @return string UTF-8 encoded
1398	 */
1399	function unichr($dec)
1400	{
1401		if ($dec < 128)
1402		{
1403			$utf = chr($dec);
1404		}
1405		else if ($dec < 2048)
1406		{
1407			$utf = chr(192 + (($dec - ($dec % 64)) / 64));
1408			$utf .= chr(128 + ($dec % 64));
1409		}
1410		else
1411		{
1412			$utf = chr(224 + (($dec - ($dec % 4096)) / 4096));
1413			$utf .= chr(128 + ((($dec % 4096) - ($dec % 64)) / 64));
1414			$utf .= chr(128 + ($dec % 64));
1415		}
1416		return $utf;
1417	}
1418
1419	/**
1420	 * UTF-8 strlen()
1421	 *
1422	 * @param string $str
1423	 * @return int
1424	 *
1425	 * @author dtorop 932 at hotmail dot com <http://www.php.net/manual/en/function.strlen.php#37975>
1426	 * @author Milian Wolff <http://milianw.de>
1427	 */
1428	function strlen($str)
1429	{
1430		if (function_exists('mb_strlen'))
1431		{
1432			return mb_strlen($str, 'UTF-8');
1433		}
1434		else
1435		{
1436			return preg_match_all('/[\x00-\x7F\xC0-\xFD]/', $str, $var_empty);
1437		}
1438	}
1439
1440	/**
1441	 * wordwrap for utf8 encoded strings
1442	 *
1443	 * @param string $str
1444	 * @param integer $len
1445	 * @param string $what
1446	 * @return string
1447	 */
1448	function wordwrap($str, $width, $break, $cut = false)
1449	{
1450		if (!$cut)
1451		{
1452			$regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){1,' . $width . '}\b#';
1453		}
1454		else
1455		{
1456			$regexp = '#^(?:[\x00-\x7F]|[\xC0-\xFF][\x80-\xBF]+){' . $width . '}#';
1457		}
1458		$return = '';
1459		while (preg_match($regexp, $str, $matches))
1460		{
1461			$string = $matches[0];
1462			$str = ltrim(substr($str, strlen($string)));
1463			if (!$cut && isset($str[0]) && in_array($str[0], array('.', '!', ';', ':', '?', ',')))
1464			{
1465				$string .= $str[0];
1466				$str = ltrim(substr($str, 1));
1467			}
1468			$return .= $string . $break;
1469		}
1470		return $return . ltrim($str);
1471	}
1472
1473	/**
1474	 * check if current node has a $tagName as parent (somewhere, not only the direct parent)
1475	 *
1476	 * @param string $tagName
1477	 * @return bool
1478	 */
1479	function hasParent($tagName)
1480	{
1481		return in_array($tagName, $this->parser->openTags);
1482	}
1483
1484	/**
1485	 * get tagName of direct parent tag
1486	 *
1487	 * @param void
1488	 * @return string $tagName
1489	 */
1490	function parent()
1491	{
1492		return end($this->parser->openTags);
1493	}
1494}