code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Texy! - Convert plain text to XHTML format using {@link process()}.
 *
 * <code>
 * $texy = new Texy();
 * $html = $texy->process($text);
 * </code>
 */
class Texy extends TexyObject
{
	// configuration directives
	const ALL = TRUE;
	const NONE = FALSE;

	// Texy version
	const VERSION = '2.7.1';
	const REVISION = 'released on 2015-10-06';

	// types of protection marks
	const CONTENT_MARKUP = "\x17";
	const CONTENT_REPLACED = "\x16";
	const CONTENT_TEXTUAL = "\x15";
	const CONTENT_BLOCK = "\x14";

	// url filters
	const FILTER_ANCHOR = 'anchor';
	const FILTER_IMAGE = 'image';

	// HTML minor-modes
	const XML = 2;

	// HTML modes
	const HTML4_TRANSITIONAL = 0;
	const HTML4_STRICT = 1;
	const HTML5 = 4;
	const XHTML1_TRANSITIONAL = 2; // Texy::HTML4_TRANSITIONAL | Texy::XML;
	const XHTML1_STRICT = 3; // Texy::HTML4_STRICT | Texy::XML;
	const XHTML5 = 6; // Texy::HTML5 | Texy::XML;

	/** @var string  input & output text encoding */
	public $encoding = 'utf-8';

	/** @var array  Texy! syntax configuration */
	public $allowed = array();

	/** @var TRUE|FALSE|array  Allowed HTML tags */
	public $allowedTags;

	/** @var TRUE|FALSE|array  Allowed classes */
	public $allowedClasses = self::ALL; // all classes and id are allowed

	/** @var TRUE|FALSE|array  Allowed inline CSS style */
	public $allowedStyles = self::ALL;  // all inline styles are allowed

	/** @var int  TAB width (for converting tabs to spaces) */
	public $tabWidth = 8;

	/** @var bool  Do obfuscate e-mail addresses? */
	public $obfuscateEmail = TRUE;

	/** @var array  regexps to check URL schemes */
	public $urlSchemeFilters = NULL; // disable URL scheme filter

	/** @var bool  Paragraph merging mode */
	public $mergeLines = TRUE;

	/** @var array  Parsing summary */
	public $summary = array(
		'images' => array(),
		'links' => array(),
		'preload' => array(),
	);

	/** @var string  Generated stylesheet */
	public $styleSheet = '';

	/** @var array  CSS classes for align modifiers */
	public $alignClasses = array(
		'left' => NULL,
		'right' => NULL,
		'center' => NULL,
		'justify' => NULL,
		'top' => NULL,
		'middle' => NULL,
		'bottom' => NULL,
	);

	/** @var bool  remove soft hyphens (SHY)? */
	public $removeSoftHyphens = TRUE;

	/** @deprecated */
	public static $advertisingNotice = FALSE;

	/** @var string */
	public $nontextParagraph = 'div';

	/** @var TexyScriptModule */
	// public $scriptModule;

	/** @var TexyParagraphModule */
	public $paragraphModule;

	/** @var TexyHtmlModule */
	// public $htmlModule;

	/** @var TexyImageModule */
	// public $imageModule;

	/** @var TexyLinkModule */
	public $linkModule;

	/** @var TexyPhraseModule */
	public $phraseModule;

	/** @var TexyEmoticonModule */
	// public $emoticonModule;

	/** @var TexyBlockModule */
	public $blockModule;

	/** @var TexyHeadingModule */
	public $headingModule;

	/** @var TexyHorizLineModule */
	public $horizLineModule;

	/** @var TexyBlockQuoteModule */
	public $blockQuoteModule;

	/** @var TexyListModule */
	public $listModule;

	/** @var TexyTableModule */
	// public $tableModule;

	/** @var TexyFigureModule */
	// public $figureModule;

	/** @var TexyTypographyModule */
	public $typographyModule;

	/** @var TexyLongWordsModule */
	public $longWordsModule;

	/** @var TexyHtmlOutputModule */
	public $htmlOutputModule;

	/**
	 * Registered regexps and associated handlers for inline parsing.
	 * @var array of ('handler' => callback, 'pattern' => regular expression)
	 */
	private $linePatterns = array();
	private $_linePatterns;

	/**
	 * Registered regexps and associated handlers for block parsing.
	 * @var array of ('handler' => callback, 'pattern' => regular expression)
	 */
	private $blockPatterns = array();
	private $_blockPatterns;

	/** @var array */
	private $postHandlers = array();

	/** @var TexyHtml  DOM structure for parsed text */
	private $DOM;

	/** @var array  Texy protect markup table */
	private $marks = array();

	/** @var array  for internal usage */
	public $_classes, $_styles;

	/** @var bool */
	private $processing;

	/** @var array of events and registered handlers */
	private $handlers = array();

	/**
	 * DTD descriptor.
	 *   $dtd[element][0] - allowed attributes (as array keys)
	 *   $dtd[element][1] - allowed content for an element (content model) (as array keys)
	 *                    - array of allowed elements (as keys)
	 *                    - FALSE - empty element
	 *                    - 0 - special case for ins & del
	 * @var array
	 */
	public $dtd;

	/** @var array */
	private static $dtdCache;

	/** @var int  HTML mode */
	private $mode;

	/** DEPRECATED */
	public static $strictDTD;
	public $cleaner;
	public $xhtml;

	public function __construct()
	{
		if (defined('PCRE_VERSION') && PCRE_VERSION == 8.34 && defined('PHP_VERSION_ID') && PHP_VERSION_ID < 50513) {
			trigger_error('Texy: PCRE 8.34 is not supported due to bug #1451', E_USER_WARNING);
		}

		if (extension_loaded('mbstring') && mb_get_info('func_overload') & 2 && substr(mb_get_info('internal_encoding'), 0, 1) === 'U') {
			mb_internal_encoding('pass');
			trigger_error("Texy: mb_internal_encoding changed to 'pass'", E_USER_WARNING);
		}

		// load all modules
		$this->loadModules();

		// DEPRECATED
		if (self::$strictDTD !== NULL) {
			$this->setOutputMode(self::$strictDTD ? self::XHTML1_STRICT : self::XHTML1_TRANSITIONAL);
		} else {
			$this->setOutputMode(self::XHTML1_TRANSITIONAL);
		}

		// DEPRECATED
		$this->cleaner = & $this->htmlOutputModule;

		// examples of link references ;-)
		$link = new TexyLink('http://texy.info/');
		$link->modifier->title = 'The best text -> HTML converter and formatter';
		$link->label = 'Texy!';
		$this->linkModule->addReference('texy', $link);

		$link = new TexyLink('https://www.google.com/search?q=%s');
		$this->linkModule->addReference('google', $link);

		$link = new TexyLink('https://en.wikipedia.org/wiki/Special:Search?search=%s');
		$this->linkModule->addReference('wikipedia', $link);
	}

	/**
	 * Set HTML/XHTML output mode (overwrites self::$allowedTags)
	 * @param  int
	 * @return void
	 */
	public function setOutputMode($mode)
	{
		if (!in_array($mode, array(self::HTML4_TRANSITIONAL, self::HTML4_STRICT,
			self::HTML5, self::XHTML1_TRANSITIONAL, self::XHTML1_STRICT, self::XHTML5), TRUE)
		) {
			throw new InvalidArgumentException('Invalid mode.');
		}

		if (!isset(self::$dtdCache[$mode])) {
			require dirname(__FILE__) . '/DTD.php';
			self::$dtdCache[$mode] = $dtd;
		}

		$this->mode = $mode;
		$this->dtd = self::$dtdCache[$mode];
		TexyHtml::$xhtml = (bool) ($mode & self::XML); // TODO: remove?

		// accept all valid HTML tags and attributes by default
		$this->allowedTags = array();
		foreach ($this->dtd as $tag => $dtd) {
			$this->allowedTags[$tag] = self::ALL;
		}
	}

	/**
	 * Get HTML/XHTML output mode
	 * @return int
	 */
	public function getOutputMode()
	{
		return $this->mode;
	}

	/**
	 * Create array of all used modules ($this->modules).
	 * This array can be changed by overriding this method (by subclasses)
	 */
	protected function loadModules()
	{
		// line parsing
		// $this->scriptModule = new TexyScriptModule($this);
		// $this->htmlModule = new TexyHtmlModule($this);
		// $this->imageModule = new TexyImageModule($this);
		$this->phraseModule = new TexyPhraseModule($this);
		$this->linkModule = new TexyLinkModule($this);
		// $this->emoticonModule = new TexyEmoticonModule($this);

		// block parsing
		$this->paragraphModule = new TexyParagraphModule($this);
		$this->blockModule = new TexyBlockModule($this);
		// $this->figureModule = new TexyFigureModule($this);
		$this->horizLineModule = new TexyHorizLineModule($this);
		$this->blockQuoteModule = new TexyBlockQuoteModule($this);
		// $this->tableModule = new TexyTableModule($this);
		$this->headingModule = new TexyHeadingModule($this);
		$this->listModule = new TexyListModule($this);

		// post process
		$this->typographyModule = new TexyTypographyModule($this);
		$this->longWordsModule = new TexyLongWordsModule($this);
		$this->htmlOutputModule = new TexyHtmlOutputModule($this);
	}

	final public function registerLinePattern($handler, $pattern, $name, $againTest = NULL)
	{
		if (!is_callable($handler)) {
			$able = is_callable($handler, TRUE, $textual);
			throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.'));
		}

		if (!isset($this->allowed[$name])) {
			$this->allowed[$name] = TRUE;
		}

		$this->linePatterns[$name] = array(
			'handler' => $handler,
			'pattern' => $pattern,
			'again' => $againTest,
		);
	}

	final public function registerBlockPattern($handler, $pattern, $name)
	{
		if (!is_callable($handler)) {
			$able = is_callable($handler, TRUE, $textual);
			throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.'));
		}

		// if (!preg_match('#(.)\^.*\$\\1[a-z]*#is', $pattern)) die("Texy: Not a block pattern $name");
		if (!isset($this->allowed[$name])) {
			$this->allowed[$name] = TRUE;
		}

		$this->blockPatterns[$name] = array(
			'handler' => $handler,
			'pattern' => $pattern . 'm', // force multiline
		);
	}

	final public function registerPostLine($handler, $name)
	{
		if (!is_callable($handler)) {
			$able = is_callable($handler, TRUE, $textual);
			throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.'));
		}

		if (!isset($this->allowed[$name])) {
			$this->allowed[$name] = TRUE;
		}

		$this->postHandlers[$name] = $handler;
	}

	/**
	 * Converts document in Texy! to (X)HTML code.
	 *
	 * @param  string   input text
	 * @param  bool     is single line?
	 * @return string   output HTML code
	 */
	public function process($text, $singleLine = FALSE)
	{
		if ($this->processing) {
			throw new RuntimeException('Processing is in progress yet.');
		}

		// initialization
		$this->marks = array();
		$this->processing = TRUE;

		// speed-up
		if (is_array($this->allowedClasses)) {
			$this->_classes = array_flip($this->allowedClasses);
		} else {
			$this->_classes = $this->allowedClasses;
		}
		if (is_array($this->allowedStyles)) {
			$this->_styles = array_flip($this->allowedStyles);
		} else {
			$this->_styles = $this->allowedStyles;
		}

		// convert to UTF-8 (and check source encoding)
		$text = TexyUtf::toUtf($text, $this->encoding);

		if ($this->removeSoftHyphens) {
			$text = str_replace("\xC2\xAD", '', $text);
		}

		// standardize line endings and spaces
		$text = self::normalize($text);

		// replace tabs with spaces
		$this->tabWidth = max(1, (int) $this->tabWidth);
		while (strpos($text, "\t") !== FALSE) {
			$text = TexyRegexp::replace($text, '#^([^\t\n]*+)\t#mU', array($this, 'tabCb'));
		}

		// user before handler
		$this->invokeHandlers('beforeParse', array($this, & $text, $singleLine));

		// select patterns
		$this->_linePatterns = $this->linePatterns;
		$this->_blockPatterns = $this->blockPatterns;
		foreach ($this->_linePatterns as $name => $foo) {
			if (empty($this->allowed[$name])) {
				unset($this->_linePatterns[$name]);
			}
		}
		foreach ($this->_blockPatterns as $name => $foo) {
			if (empty($this->allowed[$name])) {
				unset($this->_blockPatterns[$name]);
			}
		}

		// parse Texy! document into internal DOM structure
		$this->DOM = TexyHtml::el();
		if ($singleLine) {
			$this->DOM->parseLine($this, $text);
		} else {
			$this->DOM->parseBlock($this, $text);
		}

		// user after handler
		$this->invokeHandlers('afterParse', array($this, $this->DOM, $singleLine));

		// converts internal DOM structure to final HTML code
		$html = $this->DOM->toHtml($this);

		// created by TexyParagraphModule and then protected
		$html = str_replace("\r", "\n", $html);

		$this->processing = FALSE;

		return TexyUtf::utf2html($html, $this->encoding);
	}

	/**
	 * Converts single line in Texy! to (X)HTML code.
	 *
	 * @param  string   input text
	 * @return string   output HTML code
	 */
	public function processLine($text)
	{
		return $this->process($text, TRUE);
	}

	/**
	 * Makes only typographic corrections.
	 * @param  string   input text (in encoding defined by Texy::$encoding)
	 * @return string   output text (in UTF-8)
	 */
	public function processTypo($text)
	{
		// convert to UTF-8 (and check source encoding)
		$text = TexyUtf::toUtf($text, $this->encoding);

		// standardize line endings and spaces
		$text = self::normalize($text);

		$this->typographyModule->beforeParse($this, $text);
		$text = $this->typographyModule->postLine($text, TRUE);

		if (!empty($this->allowed['longwords'])) {
			$text = $this->longWordsModule->postLine($text);
		}

		return TexyUtf::utf2html($text, $this->encoding);
	}

	/**
	 * Converts DOM structure to pure text.
	 * @return string
	 */
	public function toText()
	{
		if (!$this->DOM) {
			throw new RuntimeException('Call $texy->process() first.');
		}

		return TexyUtf::utfTo($this->DOM->toText($this), $this->encoding);
	}

	/**
	 * Converts internal string representation to final HTML code in UTF-8.
	 * @return string
	 */
	final public function stringToHtml($s)
	{
		// decode HTML entities to UTF-8
		$s = self::unescapeHtml($s);

		// line-postprocessing
		$blocks = explode(self::CONTENT_BLOCK, $s);
		foreach ($this->postHandlers as $name => $handler) {
			if (empty($this->allowed[$name])) {
				continue;
			}
			foreach ($blocks as $n => $s) {
				if ($n % 2 === 0 && $s !== '') {
					$blocks[$n] = call_user_func($handler, $s);
				}
			}
		}
		$s = implode(self::CONTENT_BLOCK, $blocks);

		// encode < > &
		$s = self::escapeHtml($s);

		// replace protected marks
		$s = $this->unProtect($s);

		// wellform and reformat HTML
		$this->invokeHandlers('postProcess', array($this, & $s));

		// unfreeze spaces
		$s = self::unfreezeSpaces($s);
		$s = ltrim($s, "\n");

		return $s;
	}

	/**
	 * Converts internal string representation to final HTML code in UTF-8.
	 * @return string
	 */
	final public function stringToText($s)
	{
		$save = $this->htmlOutputModule->lineWrap;
		$this->htmlOutputModule->lineWrap = FALSE;
		$s = $this->stringToHtml($s);
		$this->htmlOutputModule->lineWrap = $save;

		// remove tags
		$s = TexyRegexp::replace($s, '#<(script|style)(.*)</\\1>#Uis', '');
		$s = strip_tags($s);
		$s = TexyRegexp::replace($s, '#\n\s*\n\s*\n[\n\s]*\n#', "\n\n");

		// entities -> chars
		$s = self::unescapeHtml($s);

		// convert nbsp to normal space and remove shy
		$s = strtr($s, array(
			"\xC2\xAD" => '', // shy
			"\xC2\xA0" => ' ', // nbsp
		));

		return $s;
	}

	/**
	 * Add new event handler.
	 *
	 * @param  string   event name
	 * @param  callback
	 * @return void
	 */
	final public function addHandler($event, $callback)
	{
		if (!is_callable($callback)) {
			$able = is_callable($callback, TRUE, $textual);
			throw new InvalidArgumentException("Handler '$textual' is not " . ($able ? 'callable.' : 'valid PHP callback.'));
		}

		$this->handlers[$event][] = $callback;
	}

	/**
	 * Invoke registered around-handlers.
	 *
	 * @param  string   event name
	 * @param  TexyParser  actual parser object
	 * @param  array    arguments passed into handler
	 * @return mixed
	 */
	final public function invokeAroundHandlers($event, $parser, $args)
	{
		if (!isset($this->handlers[$event])) {
			return FALSE;
		}

		$invocation = new TexyHandlerInvocation($this->handlers[$event], $parser, $args);
		$res = $invocation->proceed();
		$invocation->free();
		return $res;
	}

	/**
	 * Invoke registered after-handlers.
	 *
	 * @param  string   event name
	 * @param  array    arguments passed into handler
	 * @return void
	 */
	final public function invokeHandlers($event, $args)
	{
		if (!isset($this->handlers[$event])) {
			return;
		}

		foreach ($this->handlers[$event] as $handler) {
			call_user_func_array($handler, $args);
		}
	}

	/**
	 * Translate all white spaces (\t \n \r space) to meta-spaces \x01-\x04.
	 * which are ignored by TexyHtmlOutputModule routine
	 * @param  string
	 * @return string
	 */
	final public static function freezeSpaces($s)
	{
		return strtr($s, " \t\r\n", "\x01\x02\x03\x04");
	}

	/**
	 * Reverts meta-spaces back to normal spaces.
	 * @param  string
	 * @return string
	 */
	final public static function unfreezeSpaces($s)
	{
		return strtr($s, "\x01\x02\x03\x04", " \t\r\n");
	}

	/**
	 * Removes special controls characters and normalizes line endings and spaces.
	 * @param  string
	 * @return string
	 */
	final public static function normalize($s)
	{
		// standardize line endings to unix-like
		$s = str_replace("\r\n", "\n", $s); // DOS
		$s = strtr($s, "\r", "\n"); // Mac

		// remove special chars; leave \t + \n
		$s = TexyRegexp::replace($s, '#[\x00-\x08\x0B-\x1F]+#', '');

		// right trim
		$s = TexyRegexp::replace($s, "#[\t ]+$#m", '');

		// trailing spaces
		$s = trim($s, "\n");

		return $s;
	}

	/**
	 * Converts to web safe characters [a-z0-9-] text.
	 * @param  string
	 * @param  string
	 * @return string
	 */
	final public static function webalize($s, $charlist = NULL)
	{
		$s = TexyUtf::utf2ascii($s);
		$s = strtolower($s);
		$s = TexyRegexp::replace($s, '#[^a-z0-9'.preg_quote($charlist, '#').']+#', '-');
		$s = trim($s, '-');
		return $s;
	}

	/**
	 * Texy! version of htmlSpecialChars (much faster than htmlSpecialChars!).
	 * note: " is not encoded!
	 * @param  string
	 * @return string
	 */
	final public static function escapeHtml($s)
	{
		return str_replace(array('&', '<', '>'), array('&', '<', '>'), $s);
	}

	/**
	 * Texy! version of html_entity_decode (always UTF-8, much faster than original!).
	 * @param  string
	 * @return string
	 */
	final public static function unescapeHtml($s)
	{
		if (strpos($s, '&') === FALSE) {
			return $s;
		}
		return html_entity_decode($s, ENT_QUOTES, 'UTF-8');
	}

	/**
	 * Outdents text block.
	 * @param  string
	 * @return string
	 */
	final public static function outdent($s)
	{
		$s = trim($s, "\n");
		$min = strlen($s);
		foreach (TexyRegexp::match($s, '#^ *\S#m', TexyRegexp::ALL) as $m) {
			$min = min($min, strlen($m[0]) - 1);
		}
		if ($min) {
			$s = TexyRegexp::replace($s, "#^ {{$min}}#m", '');
		}
		return $s;
	}

	/**
	 * Generate unique mark - useful for freezing (folding) some substrings.
	 * @param  string   any string to froze
	 * @param  int      Texy::CONTENT_* constant
	 * @return string  internal mark
	 */
	final public function protect($child, $contentType)
	{
		if ($child === '') {
			return '';
		}

		$key = $contentType
			. strtr(base_convert(count($this->marks), 10, 8), '01234567', "\x18\x19\x1A\x1B\x1C\x1D\x1E\x1F")
			. $contentType;

		$this->marks[$key] = $child;

		return $key;
	}

	final public function unProtect($html)
	{
		return strtr($html, $this->marks);
	}

	/**
	 * Filters bad URLs.
	 * @param  string   user URL
	 * @param  string   FILTER_ANCHOR | FILTER_IMAGE
	 * @return bool
	 */
	final public function checkURL($URL, $type)
	{
		// absolute URL with scheme? check scheme!
		return empty($this->urlSchemeFilters[$type])
			|| !preg_match('#\s*[a-z][a-z0-9+.-]{0,20}:#Ai', $URL) // http: | mailto:
			|| preg_match($this->urlSchemeFilters[$type], $URL);
	}

	/**
	 * Is given URL relative?
	 * @param  string  URL
	 * @return bool
	 */
	final public static function isRelative($URL)
	{
		// check for scheme, or absolute path, or absolute URL
		return !preg_match('#[a-z][a-z0-9+.-]{0,20}:|[\#/?]#Ai', $URL);
	}

	/**
	 * Prepends root to URL, if possible.
	 * @param  string  URL
	 * @param  string  root
	 * @return string
	 */
	final public static function prependRoot($URL, $root)
	{
		if ($root == NULL || !self::isRelative($URL)) {
			return $URL;
		}
		return rtrim($root, '/\\') . '/' . $URL;
	}

	final public function getLinePatterns()
	{
		return $this->_linePatterns;
	}

	final public function getBlockPatterns()
	{
		return $this->_blockPatterns;
	}

	final public function getDOM()
	{
		return $this->DOM;
	}

	/** @internal */
	public function tabCb($m)
	{
		return $m[1] . str_repeat(' ', $this->tabWidth - strlen($m[1]) % $this->tabWidth);
	}

	/**
	 * PHP garbage collector helper.
	 */
	final public function free()
	{
		if (version_compare(PHP_VERSION, '5.3', '<')) {
			foreach (array_keys(get_object_vars($this)) as $key) {
				$this->$key = NULL;
			}
		}
	}

	final public function __clone()
	{
		throw new Exception('Clone is not supported.');
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Parser for block structures.
 */
class TexyBlockParser extends TexyParser
{
	/** @var string */
	private $text;

	/** @var int */
	private $offset;

	/** @var bool */
	private $indented;

	/**
	 * @param  Texy
	 * @param  TexyHtml
	 */
	public function __construct(Texy $texy, TexyHtml $element, $indented)
	{
		$this->texy = $texy;
		$this->element = $element;
		$this->indented = (bool) $indented;
		$this->patterns = $texy->getBlockPatterns();
	}

	public function isIndented()
	{
		return $this->indented;
	}

	// match current line against RE.
	// if succesfull, increments current position and returns TRUE
	public function next($pattern, & $matches)
	{
		if ($this->offset > strlen($this->text)) {
			return FALSE;
		}
		$matches = TexyRegexp::match(
			$this->text,
			$pattern . 'Am', // anchored & multiline
			TexyRegexp::OFFSET_CAPTURE,
			$this->offset
		);

		if ($matches) {
			$this->offset += strlen($matches[0][0]) + 1; // 1 = "\n"
			foreach ($matches as $key => $value) {
				$matches[$key] = $value[0];
			}
			return TRUE;
		}
	}

	public function moveBackward($linesCount = 1)
	{
		while (--$this->offset > 0) {
			if ($this->text[$this->offset - 1] === "\n") {
				$linesCount--;
				if ($linesCount < 1) {
					break;
				}
			}
		}

		$this->offset = max($this->offset, 0);
	}

	public static function cmp($a, $b)
	{
		if ($a[0] === $b[0]) {
			return $a[3] < $b[3] ? -1 : 1;
		}
		if ($a[0] < $b[0]) {
			return -1;
		}
		return 1;
	}

	/**
	 * @param  string
	 * @return void
	 */
	public function parse($text)
	{
		$tx = $this->texy;

		$tx->invokeHandlers('beforeBlockParse', array($this, & $text));

		// parser initialization
		$this->text = $text;
		$this->offset = 0;

		// parse loop
		$matches = array();
		$priority = 0;
		foreach ($this->patterns as $name => $pattern) {
			$ms = TexyRegexp::match(
				$text,
				$pattern['pattern'],
				TexyRegexp::OFFSET_CAPTURE | TexyRegexp::ALL
			);

			foreach ((array) $ms as $m) {
				$offset = $m[0][1];
				foreach ($m as $k => $v) {
					$m[$k] = $v[0];
				}
				$matches[] = array($offset, $name, $m, $priority);
			}
			$priority++;
		}
		unset($name, $pattern, $ms, $m, $k, $v);

		usort($matches, array(__CLASS__, 'cmp')); // generates strict error in PHP 5.1.2
		$matches[] = array(strlen($text), NULL, NULL); // terminal cap

		// process loop
		$el = $this->element;
		$cursor = 0;
		do {
			do {
				list($mOffset, $mName, $mMatches) = $matches[$cursor];
				$cursor++;
				if ($mName === NULL || $mOffset >= $this->offset) {
					break;
				}
			} while (1);

			// between-matches content
			if ($mOffset > $this->offset) {
				$s = trim(substr($text, $this->offset, $mOffset - $this->offset));
				if ($s !== '') {
					$tx->paragraphModule->process($this, $s, $el);
				}
			}

			if ($mName === NULL) {
				break; // finito
			}

			$this->offset = $mOffset + strlen($mMatches[0]) + 1; // 1 = \n

			$res = call_user_func_array(
				$this->patterns[$mName]['handler'],
				array($this, $mMatches, $mName)
			);

			if ($res === FALSE || $this->offset <= $mOffset) { // module rejects text
				// asi by se nemelo stat, rozdeli generic block
				$this->offset = $mOffset; // turn offset back
				continue;

			} elseif ($res instanceof TexyHtml) {
				$el->insert(NULL, $res);

			} elseif (is_string($res)) {
				$el->insert(NULL, $res);
			}

		} while (1);
	}

}

repo_name: devarped/pierwszykanal

license: mit


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Texy basic configurators.
 *
 * <code>
 * $texy = new Texy();
 * TexyConfigurator::safeMode($texy);
 * </code>
 */
class TexyConfigurator
{
	public static $safeTags = array(
		'a' => array('href', 'title'),
		'acronym' => array('title'),
		'b' => array(),
		'br' => array(),
		'cite' => array(),
		'code' => array(),
		'em' => array(),
		'i' => array(),
		'strong' => array(),
		'sub' => array(),
		'sup' => array(),
		'q' => array(),
		'small' => array(),
	);

	/**
	 * static class.
	 */
	final public function __construct()
	{
		throw new LogicException('Cannot instantiate static class ' . get_class($this));
	}

	/**
	 * Configure Texy! for web comments and other usages, where input text may insert attacker.
	 *
	 * @param  Texy  object to configure
	 * @return void
	 */
	public static function safeMode(Texy $texy)
	{
		$texy->allowedClasses = Texy::NONE; // no class or ID are allowed
		$texy->allowedStyles = Texy::NONE; // style modifiers are disabled
		$texy->allowedTags = self::$safeTags; // only some "safe" HTML tags and attributes are allowed
		$texy->urlSchemeFilters[Texy::FILTER_ANCHOR] = '#https?:|ftp:|mailto:#A';
		$texy->urlSchemeFilters[Texy::FILTER_IMAGE] = '#https?:#A';
		$texy->allowed['image'] = FALSE; // disable images
		$texy->allowed['link/definition'] = FALSE; // disable [ref]: URL reference definitions
		$texy->allowed['html/comment'] = FALSE; // disable HTML comments
		$texy->linkModule->forceNoFollow = TRUE; // force rel="nofollow"
	}

	/**
	 * Disable all links.
	 *
	 * @param  Texy  object to configure
	 * @return void
	 */
	public static function disableLinks(Texy $texy)
	{
		$texy->allowed['link/reference'] = FALSE;
		$texy->allowed['link/email'] = FALSE;
		$texy->allowed['link/url'] = FALSE;
		$texy->allowed['link/definition'] = FALSE;
		$texy->phraseModule->linksAllowed = FALSE;

		if (is_array($texy->allowedTags)) {
			unset($texy->allowedTags['a']);
		} // TODO: else...
	}

	/**
	 * Disable all images.
	 *
	 * @param  Texy  object to configure
	 * @return void
	 */
	public static function disableImages(Texy $texy)
	{
		$texy->allowed['image'] = FALSE;
		$texy->allowed['figure'] = FALSE;
		$texy->allowed['image/definition'] = FALSE;

		if (is_array($texy->allowedTags)) {
			unset($texy->allowedTags['img'], $texy->allowedTags['object'], $texy->allowedTags['embed'], $texy->allowedTags['applet']);
		} // TODO: else...
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Around advice handlers.
 */
final class TexyHandlerInvocation extends TexyObject
{
	/** @var array of callbacks */
	private $handlers;

	/** @var int  callback counter */
	private $pos;

	/** @var array */
	private $args;

	/** @var TexyParser */
	private $parser;

	/**
	 * @param  array    array of callbacks
	 * @param  TexyParser
	 * @param  array    arguments
	 */
	public function __construct($handlers, TexyParser $parser, $args)
	{
		$this->handlers = $handlers;
		$this->pos = count($handlers);
		$this->parser = $parser;
		array_unshift($args, $this);
		$this->args = $args;
	}

	/**
	 * @param  mixed
	 * @return mixed
	 */
	public function proceed()
	{
		if ($this->pos === 0) {
			throw new RuntimeException('No more handlers.');
		}

		if (func_num_args()) {
			$this->args = func_get_args();
			array_unshift($this->args, $this);
		}

		$this->pos--;
		$res = call_user_func_array($this->handlers[$this->pos], $this->args);
		if ($res === NULL) {
			throw new UnexpectedValueException("Invalid value returned from handler '" . print_r($this->handlers[$this->pos], TRUE) . "'.");
		}
		return $res;
	}

	/**
	 * @return TexyParser
	 */
	public function getParser()
	{
		return $this->parser;
	}

	/**
	 * @return Texy
	 */
	public function getTexy()
	{
		return $this->parser->getTexy();
	}

	/**
	 * PHP garbage collector helper.
	 */
	public function free()
	{
		$this->handlers = $this->parser = $this->args = NULL;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * HTML helper.
 *
 * usage:
 * $anchor = TexyHtml::el('a')->href($link)->setText('Texy');
 * $el->class = 'myclass';
 *
 * echo $el->startTag(), $el->endTag();
 *
 * @property   mixed element's attributes
 */
class TexyHtml extends TexyObject implements ArrayAccess, /* Countable, */ IteratorAggregate
{
	/** @var string  element's name */
	private $name;

	/** @var bool  is element empty? */
	private $isEmpty;

	/** @var array  element's attributes */
	public $attrs = array();

	/** @var array  of TexyHtml | string nodes */
	protected $children = array();

	/** @var bool  use XHTML syntax? */
	public static $xhtml = TRUE;

	/** @var array  empty elements */
	public static $emptyElements = array('img'=>1,'hr'=>1,'br'=>1,'input'=>1,'meta'=>1,'area'=>1,
		'base'=>1,'col'=>1,'link'=>1,'param'=>1,'basefont'=>1,'frame'=>1,'isindex'=>1,'wbr'=>1,'embed'=>1);

	/** @var array  %inline; elements; replaced elements + br have value '1' */
	public static $inlineElements = array('ins'=>0,'del'=>0,'tt'=>0,'i'=>0,'b'=>0,'big'=>0,'small'=>0,'em'=>0,
		'strong'=>0,'dfn'=>0,'code'=>0,'samp'=>0,'kbd'=>0,'var'=>0,'cite'=>0,'abbr'=>0,'acronym'=>0,
		'sub'=>0,'sup'=>0,'q'=>0,'span'=>0,'bdo'=>0,'a'=>0,'object'=>1,'img'=>1,'br'=>1,'script'=>1,
		'map'=>0,'input'=>1,'select'=>1,'textarea'=>1,'label'=>0,'button'=>1,
		'u'=>0,'s'=>0,'strike'=>0,'font'=>0,'applet'=>1,'basefont'=>0, // transitional
		'embed'=>1,'wbr'=>0,'nobr'=>0,'canvas'=>1, // proprietary
	);

	/** @var array  elements with optional end tag in HTML */
	public static $optionalEnds = array('body'=>1,'head'=>1,'html'=>1,'colgroup'=>1,'dd'=>1,
		'dt'=>1,'li'=>1,'option'=>1,'p'=>1,'tbody'=>1,'td'=>1,'tfoot'=>1,'th'=>1,'thead'=>1,'tr'=>1);

	/** @see http://www.w3.org/TR/xhtml1/prohibitions.html */
	public static $prohibits = array(
		'a' => array('a','button'),
		'img' => array('pre'),
		'object' => array('pre'),
		'big' => array('pre'),
		'small' => array('pre'),
		'sub' => array('pre'),
		'sup' => array('pre'),
		'input' => array('button'),
		'select' => array('button'),
		'textarea' => array('button'),
		'label' => array('button', 'label'),
		'button' => array('button'),
		'form' => array('button', 'form'),
		'fieldset' => array('button'),
		'iframe' => array('button'),
		'isindex' => array('button'),
	);

	/**
	 * Static factory.
	 * @param  string element name (or NULL)
	 * @param  array|string element's attributes (or textual content)
	 * @return TexyHtml
	 */
	public static function el($name = NULL, $attrs = NULL)
	{
		$el = new self;
		$el->setName($name);
		if (is_array($attrs)) {
			$el->attrs = $attrs;
		} elseif ($attrs !== NULL) {
			$el->setText($attrs);
		}
		return $el;
	}

	/**
	 * Changes element's name.
	 * @param  string
	 * @param  bool  Is element empty?
	 * @return self
	 * @throws InvalidArgumentException
	 */
	final public function setName($name, $empty = NULL)
	{
		if ($name !== NULL && !is_string($name)) {
			throw new InvalidArgumentException('Name must be string or NULL.');
		}

		$this->name = $name;
		$this->isEmpty = $empty === NULL ? isset(self::$emptyElements[$name]) : (bool) $empty;
		return $this;
	}

	/**
	 * Returns element's name.
	 * @return string
	 */
	final public function getName()
	{
		return $this->name;
	}

	/**
	 * Is element empty?
	 * @return bool
	 */
	final public function isEmpty()
	{
		return $this->isEmpty;
	}

	/**
	 * Overloaded setter for element's attribute.
	 * @param  string    property name
	 * @param  mixed     property value
	 * @return void
	 */
	final public function __set($name, $value)
	{
		$this->attrs[$name] = $value;
	}

	/**
	 * Overloaded getter for element's attribute.
	 * @param  string    property name
	 * @return mixed    property value
	 */
	final public function & __get($name)
	{
		return $this->attrs[$name];
	}

	/**
	 * Overloaded setter for element's attribute.
	 * @param  string attribute name
	 * @param  array value
	 * @return self
	 */
/*
	final public function __call($m, $args)
	{
		if (count($args) !== 1) {
			throw new InvalidArgumentException("Just one argument is required.");
		}
		$this->attrs[$m] = $args[0];
		return $this;
	}
*/

	/**
	 * Special setter for element's attribute.
	 * @param  string path
	 * @param  array query
	 * @return self
	 */
	final public function href($path, $query = NULL)
	{
		if ($query) {
			$query = http_build_query($query, NULL, '&');
			if ($query !== '') {
				$path .= '?' . $query;
			}
		}
		$this->attrs['href'] = $path;
		return $this;
	}

	/**
	 * Sets element's textual content.
	 * @param  string
	 * @return self
	 */
	final public function setText($text)
	{
		if (is_scalar($text)) {
			$this->removeChildren();
			$this->children = array($text);
		} elseif ($text !== NULL) {
			throw new InvalidArgumentException('Content must be scalar.');
		}
		return $this;
	}

	/**
	 * Gets element's textual content.
	 * @return string
	 */
	final public function getText()
	{
		$s = '';
		foreach ($this->children as $child) {
			if (is_object($child)) {
				return FALSE;
			}
			$s .= $child;
		}
		return $s;
	}

	/**
	 * Adds new element's child.
	 * @param  TexyHtml|string child node
	 * @return self
	 */
	final public function add($child)
	{
		return $this->insert(NULL, $child);
	}

	/**
	 * Creates and adds a new TexyHtml child.
	 * @param  string  elements's name
	 * @param  array|string element's attributes (or textual content)
	 * @return TexyHtml  created element
	 */
	final public function create($name, $attrs = NULL)
	{
		$this->insert(NULL, $child = self::el($name, $attrs));
		return $child;
	}

	/**
	 * Inserts child node.
	 * @param  int
	 * @param  TexyHtml node
	 * @param  bool
	 * @return self
	 * @throws Exception
	 */
	public function insert($index, $child, $replace = FALSE)
	{
		if ($child instanceof self || is_string($child)) {
			if ($index === NULL) { // append
				$this->children[] = $child;

			} else { // insert or replace
				array_splice($this->children, (int) $index, $replace ? 1 : 0, array($child));
			}

		} else {
			throw new /*::*/InvalidArgumentException('Child node must be scalar or TexyHtml object.');
		}

		return $this;
	}

	/**
	 * Inserts (replaces) child node (ArrayAccess implementation).
	 * @param  int
	 * @param  TexyHtml node
	 * @return void
	 */
	final public function offsetSet($index, $child)
	{
		$this->insert($index, $child, TRUE);
	}

	/**
	 * Returns child node (ArrayAccess implementation).
	 * @param  int index
	 * @return mixed
	 */
	final public function offsetGet($index)
	{
		return $this->children[$index];
	}

	/**
	 * Exists child node? (ArrayAccess implementation).
	 * @param  int index
	 * @return bool
	 */
	final public function offsetExists($index)
	{
		return isset($this->children[$index]);
	}

	/**
	 * Removes child node (ArrayAccess implementation).
	 * @param  int index
	 * @return void
	 */
	public function offsetUnset($index)
	{
		if (isset($this->children[$index])) {
			array_splice($this->children, (int) $index, 1);
		}
	}

	/**
	 * Required by the Countable interface.
	 * @return int
	 */
	final public function count()
	{
		return count($this->children);
	}

	/**
	 * Removed all children.
	 * @return void
	 */
	public function removeChildren()
	{
		$this->children = array();
	}

	/**
	 * Required by the IteratorAggregate interface.
	 * @return ArrayIterator
	 */
	final public function getIterator()
	{
		return new ArrayIterator($this->children);
	}

	/**
	 * Returns all of children.
	 * return array
	 */
	final public function getChildren()
	{
		return $this->children;
	}

	/**
	 * Renders element's start tag, content and end tag to internal string representation.
	 * @param  Texy
	 * @return string
	 */
	final public function toString(Texy $texy)
	{
		$ct = $this->getContentType();
		$s = $texy->protect($this->startTag(), $ct);

		// empty elements are finished now
		if ($this->isEmpty) {
			return $s;
		}

		// add content
		foreach ($this->children as $child) {
			if (is_object($child)) {
				$s .= $child->toString($texy);
			} else {
				$s .= $child;
			}
		}

		// add end tag
		return $s . $texy->protect($this->endTag(), $ct);
	}

	/**
	 * Renders to final HTML.
	 * @param  Texy
	 * @return string
	 */
	final public function toHtml(Texy $texy)
	{
		return $texy->stringToHtml($this->toString($texy));
	}

	/**
	 * Renders to final text.
	 * @param  Texy
	 * @return string
	 */
	final public function toText(Texy $texy)
	{
		return $texy->stringToText($this->toString($texy));
	}

	/**
	 * Returns element's start tag.
	 * @return string
	 */
	public function startTag()
	{
		if (!$this->name) {
			return '';
		}

		$s = '<' . $this->name;

		if (is_array($this->attrs)) {
			foreach ($this->attrs as $key => $value) {
				// skip NULLs and false boolean attributes
				if ($value === NULL || $value === FALSE) {
					continue;
				}

				// true boolean attribute
				if ($value === TRUE) {
					// in XHTML must use unminimized form
					if (self::$xhtml) {
						$s .= ' ' . $key . '="' . $key . '"';
					} else {
						$s .= ' ' . $key;
					}
					continue;

				} elseif (is_array($value)) {

					// prepare into temporary array
					$tmp = NULL;
					foreach ($value as $k => $v) {
						// skip NULLs & empty string; composite 'style' vs. 'others'
						if ($v == NULL) {
							continue;
						} elseif (is_string($k)) {
							$tmp[] = $k . ':' . $v;
						} else {
							$tmp[] = $v;
						}
					}

					if (!$tmp) {
						continue;
					}
					$value = implode($key === 'style' ? ';' : ' ', $tmp);

				} else {
					$value = (string) $value;
				}

				// add new attribute
				$value = str_replace(array('&', '"', '<', '>', '@'), array('&', '"', '<', '>', '@'), $value);
				$s .= ' ' . $key . '="' . Texy::freezeSpaces($value) . '"';
			}
		}

		// finish start tag
		if (self::$xhtml && $this->isEmpty) {
			return $s . ' />';
		}
		return $s . '>';
	}

	/**
	 * Returns element's end tag.
	 * @return string
	 */
	public function endTag()
	{
		if ($this->name && !$this->isEmpty) {
			return '</' . $this->name . '>';
		}
		return '';
	}

	/**
	 * Clones all children too.
	 */
	public function __clone()
	{
		foreach ($this->children as $key => $value) {
			if (is_object($value)) {
				$this->children[$key] = clone $value;
			}
		}
	}

	/**
	 * @return int
	 */
	final public function getContentType()
	{
		if (!isset(self::$inlineElements[$this->name])) {
			return Texy::CONTENT_BLOCK;
		}

		return self::$inlineElements[$this->name] ? Texy::CONTENT_REPLACED : Texy::CONTENT_MARKUP;
	}

	/**
	 * @param  array
	 * @return void
	 */
	final public function validateAttrs($dtd)
	{
		if (isset($dtd[$this->name])) {
			$allowed = $dtd[$this->name][0];
			if (is_array($allowed)) {
				foreach ($this->attrs as $attr => $foo) {
					if (!isset($allowed[$attr])) {
						unset($this->attrs[$attr]);
					}
				}
			}
		}
	}

	public function validateChild($child, $dtd)
	{
		if (isset($dtd[$this->name])) {
			if ($child instanceof self) {
				$child = $child->name;
			}
			return isset($dtd[$this->name][1][$child]);
		} else {
			return TRUE; // unknown element
		}
	}

	/**
	 * Parses text as single line.
	 * @param  Texy
	 * @param  string
	 * @return void
	 */
	final public function parseLine(Texy $texy, $s)
	{
		// TODO!
		// special escape sequences
		$s = str_replace(array('\)', '\*'), array(')', '*'), $s);

		$parser = new TexyLineParser($texy, $this);
		$parser->parse($s);
		return $parser;
	}

	/**
	 * Parses text as block.
	 * @param  Texy
	 * @param  string
	 * @param  bool
	 * @return void
	 */
	final public function parseBlock(Texy $texy, $s, $indented = FALSE)
	{
		$parser = new TexyBlockParser($texy, $this, $indented);
		$parser->parse($s);
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Parser for single line structures.
 */
class TexyLineParser extends TexyParser
{
	/** @var bool */
	public $again;

	/**
	 * @param  Texy
	 * @param  TexyHtml
	 */
	public function __construct(Texy $texy, TexyHtml $element)
	{
		$this->texy = $texy;
		$this->element = $element;
		$this->patterns = $texy->getLinePatterns();
	}

	/**
	 * @param  string
	 * @return void
	 */
	public function parse($text)
	{
		$tx = $this->texy;

		// initialization
		$pl = $this->patterns;
		if (!$pl) {
			// nothing to do
			$this->element->insert(NULL, $text);
			return;
		}

		$offset = 0;
		$names = array_keys($pl);
		$arrMatches = $arrOffset = array();
		foreach ($names as $name) {
			$arrOffset[$name] = -1;
		}

		// parse loop
		do {
			$min = NULL;
			$minOffset = strlen($text);

			foreach ($names as $index => $name) {
				if ($arrOffset[$name] < $offset) {
					$delta = 0;
					if ($arrOffset[$name] === -2) {
						do {
							$delta++;
						} while (isset($text[$offset + $delta]) && $text[$offset + $delta] >= "\x80" && $text[$offset + $delta] < "\xC0");
					}

					if ($offset + $delta > strlen($text)) {
						unset($names[$index]);
						continue;

					} elseif ($arrMatches[$name] = TexyRegexp::match(
							$text,
							$pl[$name]['pattern'],
							TexyRegexp::OFFSET_CAPTURE,
							$offset + $delta)
					) {
						$m = & $arrMatches[$name];
						if (!strlen($m[0][0])) {
							continue;
						}
						$arrOffset[$name] = $m[0][1];
						foreach ($m as $keyx => $value) {
							$m[$keyx] = $value[0];
						}

					} else {
						// try next time?
						if (!$pl[$name]['again'] || !TexyRegexp::match($text, $pl[$name]['again'], NULL, $offset + $delta)) {
							unset($names[$index]);
						}
						continue;
					}
				} // if

				if ($arrOffset[$name] < $minOffset) {
					$minOffset = $arrOffset[$name];
					$min = $name;
				}
			} // foreach

			if ($min === NULL) {
				break;
			}

			$px = $pl[$min];
			$offset = $start = $arrOffset[$min];

			$this->again = FALSE;
			$res = call_user_func_array(
				$px['handler'],
				array($this, $arrMatches[$min], $min)
			);

			if ($res instanceof TexyHtml) {
				$res = $res->toString($tx);
			} elseif ($res === FALSE) {
				$arrOffset[$min] = -2;
				continue;
			}

			$len = strlen($arrMatches[$min][0]);
			$text = substr_replace(
				$text,
				(string) $res,
				$start,
				$len
			);

			$delta = strlen($res) - $len;
			foreach ($names as $name) {
				if ($arrOffset[$name] < $start + $len) {
					$arrOffset[$name] = -1;
				} else {
					$arrOffset[$name] += $delta;
				}
			}

			if ($this->again) {
				$arrOffset[$min] = -2;
			} else {
				$arrOffset[$min] = -1;
				$offset += strlen($res);
			}

		} while (1);

		$this->element->insert(NULL, $text);
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Modifier processor.
 *
 * Modifiers are texts like .(title)[class1 class2 #id]{color: red}>^
 *   .         starts with dot
 *   (...)     title or alt modifier
 *   [...]     classes or ID modifier
 *   {...}     inner style modifier
 *   < > <> =  horizontal align modifier
 *   ^ - _     vertical align modifier
 */
final class TexyModifier extends TexyObject
{
	/** @var string */
	public $id;

	/** @var array of classes (as keys) */
	public $classes = array();

	/** @var array of CSS styles */
	public $styles = array();

	/** @var array of HTML element attributes */
	public $attrs = array();

	/** @var string */
	public $hAlign;

	/** @var string */
	public $vAlign;

	/** @var string */
	public $title;

	/** @var string */
	public $cite;

	/** @var array  list of properties which are regarded as HTML element attributes */
	public static $elAttrs = array(
		'abbr'=>1,'accesskey'=>1,'align'=>1,'alt'=>1,'archive'=>1,'axis'=>1,'bgcolor'=>1,'cellpadding'=>1,
		'cellspacing'=>1,'char'=>1,'charoff'=>1,'charset'=>1,'cite'=>1,'classid'=>1,'codebase'=>1,'codetype'=>1,
		'colspan'=>1,'compact'=>1,'coords'=>1,'data'=>1,'datetime'=>1,'declare'=>1,'dir'=>1,'face'=>1,'frame'=>1,
		'headers'=>1,'href'=>1,'hreflang'=>1,'hspace'=>1,'ismap'=>1,'lang'=>1,'longdesc'=>1,'name'=>1,
		'noshade'=>1,'nowrap'=>1,'onblur'=>1,'onclick'=>1,'ondblclick'=>1,'onkeydown'=>1,'onkeypress'=>1,
		'onkeyup'=>1,'onmousedown'=>1,'onmousemove'=>1,'onmouseout'=>1,'onmouseover'=>1,'onmouseup'=>1,'rel'=>1,
		'rev'=>1,'rowspan'=>1,'rules'=>1,'scope'=>1,'shape'=>1,'size'=>1,'span'=>1,'src'=>1,'standby'=>1,
		'start'=>1,'summary'=>1,'tabindex'=>1,'target'=>1,'title'=>1,'type'=>1,'usemap'=>1,'valign'=>1,
		'value'=>1,'vspace'=>1,
	);

	/**
	 * @param  string modifier to parse
	 */
	public function __construct($mod = NULL)
	{
		$this->setProperties($mod);
	}

	public function setProperties($mod)
	{
		if (!$mod) {
			return;
		}

		$p = 0;
		$len = strlen($mod);

		while ($p < $len) {
			$ch = $mod[$p];

			if ($ch === '(') { // title
				$a = strpos($mod, ')', $p) + 1;
				$this->title = Texy::unescapeHtml(trim(substr($mod, $p + 1, $a - $p - 2)));
				$p = $a;

			} elseif ($ch === '{') { // style & attributes
				$a = strpos($mod, '}', $p) + 1;
				foreach (explode(';', substr($mod, $p + 1, $a - $p - 2)) as $value) {
					$pair = explode(':', $value, 2);
					$prop = strtolower(trim($pair[0]));
					if ($prop === '' || !isset($pair[1])) {
						continue;
					}
					$value = trim($pair[1]);

					if (isset(self::$elAttrs[$prop])) { // attribute
						$this->attrs[$prop] = $value;
					} elseif ($value !== '') { // style
						$this->styles[$prop] = $value;
					}
				}
				$p = $a;

			} elseif ($ch === '[') { // classes & ID
				$a = strpos($mod, ']', $p) + 1;
				$s = str_replace('#', ' #', substr($mod, $p + 1, $a - $p - 2));
				foreach (explode(' ', $s) as $value) {
					if ($value === '') {
						continue;
					} elseif ($value[0] === '#') {
						$this->id = substr($value, 1);
					} else {
						$this->classes[$value] = TRUE;
					}
				}
				$p = $a;

			} elseif ($ch === '^') { // alignment
				$this->vAlign = 'top'; $p++;
			} elseif ($ch === '-') {
				$this->vAlign = 'middle'; $p++;
			} elseif ($ch === '_') {
				$this->vAlign = 'bottom'; $p++;
			} elseif ($ch === '=') {
				$this->hAlign = 'justify'; $p++;
			} elseif ($ch === '>') {
				$this->hAlign = 'right'; $p++;
			} elseif (substr($mod, $p, 2) === '<>') {
				$this->hAlign = 'center'; $p+=2;
			} elseif ($ch === '<') {
				$this->hAlign = 'left'; $p++;
			} else {
				break;
			}
		}
	}

	/**
	 * Decorates TexyHtml element.
	 * @param  Texy   base Texy object
	 * @param  TexyHtml  element to decorate
	 * @return void
	 */
	public function decorate($texy, $el)
	{
		$elAttrs = & $el->attrs;

		// tag & attibutes
		$tmp = $texy->allowedTags; // speed-up
		if (!$this->attrs) {

		} elseif ($tmp === Texy::ALL) {
			$elAttrs = $this->attrs;
			$el->validateAttrs($texy->dtd);

		} elseif (is_array($tmp) && isset($tmp[$el->getName()])) {
			$tmp = $tmp[$el->getName()];

			if ($tmp === Texy::ALL) {
				$elAttrs = $this->attrs;

			} elseif (is_array($tmp) && count($tmp)) {
				$tmp = array_flip($tmp);
				foreach ($this->attrs as $key => $value) {
					if (isset($tmp[$key])) {
						$el->attrs[$key] = $value;
					}
				}
			}
			$el->validateAttrs($texy->dtd);
		}

		// title
		if ($this->title !== NULL) {
			$elAttrs['title'] = $texy->typographyModule->postLine($this->title);
		}

		// classes & ID
		if ($this->classes || $this->id !== NULL) {
			$tmp = $texy->_classes; // speed-up
			if ($tmp === Texy::ALL) {
				foreach ($this->classes as $value => $foo) {
					$elAttrs['class'][] = $value;
				}
				$elAttrs['id'] = $this->id;
			} elseif (is_array($tmp)) {
				foreach ($this->classes as $value => $foo) {
					if (isset($tmp[$value])) {
						$elAttrs['class'][] = $value;
					}
				}

				if (isset($tmp['#' . $this->id])) {
					$elAttrs['id'] = $this->id;
				}
			}
		}

		// styles
		if ($this->styles) {
			$tmp = $texy->_styles; // speed-up
			if ($tmp === Texy::ALL) {
				foreach ($this->styles as $prop => $value) {
					$elAttrs['style'][$prop] = $value;
				}
			} elseif (is_array($tmp)) {
				foreach ($this->styles as $prop => $value) {
					if (isset($tmp[$prop])) {
						$elAttrs['style'][$prop] = $value;
					}
				}
			}
		}

		// horizontal align
		if ($this->hAlign) {
			if (empty($texy->alignClasses[$this->hAlign])) {
				$elAttrs['style']['text-align'] = $this->hAlign;
			} else {
				$elAttrs['class'][] = $texy->alignClasses[$this->hAlign];
			}
		}

		// vertical align
		if ($this->vAlign) {
			if (empty($texy->alignClasses[$this->vAlign])) {
				$elAttrs['style']['vertical-align'] = $this->vAlign;
			} else {
				$elAttrs['class'][] = $texy->alignClasses[$this->vAlign];
			}
		}

		return $el;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Texy! modules base class.
 */
abstract class TexyModule extends TexyObject
{
	/** @var Texy */
	protected $texy;

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * TexyObject is the ultimate ancestor of all instantiable classes.
 *
 * TexyObject is copy of Nette\Object from Nette Framework (https://nette.org).
 *
 * It defines some handful methods and enhances object core of PHP:
 * - access to undeclared members throws exceptions
 * - support for conventional properties with getters and setters
 * - support for event raising functionality
 * - ability to add new methods to class (extension methods)
 *
 * Properties is a syntactic sugar which allows access public getter and setter
 * methods as normal object variables. A property is defined by a getter method
 * and optional setter method (no setter method means read-only property).
 *
 * $val = $obj->label; // equivalent to $val = $obj->getLabel();
 * $obj->label = 'Nette'; // equivalent to $obj->setLabel('Nette');
 *
 * Property names are case-sensitive, and they are written in the camelCaps
 * or PascalCaps.
 *
 * Event functionality is provided by declaration of property named 'on{Something}'
 * Multiple handlers are allowed.
 *
 * public $onClick; // declaration in class
 * $this->onClick[] = 'callback'; // attaching event handler
 * if (!empty($this->onClick)) ... // are there any handlers?
 * $this->onClick($sender, $arg); // raises the event with arguments
 *
 *
 * Adding method to class (i.e. to all instances) works similar to JavaScript
 * prototype property. The syntax for adding a new method is:
 *
 * MyClass::extensionMethod('newMethod', function (MyClass $obj, $arg, ...) { ... });
 * $obj = new MyClass;
 * $obj->newMethod($x);
 *
 */
abstract class TexyObject
{
	/** @var array (method => array(type => callback)) */
	private static $extMethods;

	/**
	 * Returns the name of the class of this object.
	 *
	 * @return string
	 */
	final public /*static*/ function getClass()
	{
		return /*get_called_class()*/ /**/get_class($this)/**/;
	}

	/**
	 * Access to reflection.
	 *
	 * @return ReflectionObject
	 */
	final public function getReflection()
	{
		return new ReflectionObject($this);
	}

	/**
	 * Call to undefined method.
	 *
	 * @param  string  method name
	 * @param  array   arguments
	 * @return mixed
	 * @throws LogicException
	 */
	public function __call($name, $args)
	{
		$class = get_class($this);

		if ($name === '') {
			throw new LogicException("Call to class '$class' method without name.");
		}

		// event functionality
		if (preg_match('#^on[A-Z]#', $name)) {
			$rp = new ReflectionProperty($class, $name);
			if ($rp->isPublic() && !$rp->isStatic()) {
				$list = $this->$name;
				if (is_array($list) || $list instanceof Traversable) {
					foreach ($list as $handler) {
						/**/if (is_object($handler)) {
							call_user_func_array(array($handler, '__invoke'), $args);

						} else /**/{
							call_user_func_array($handler, $args);
						}
					}
				}
				return NULL;
			}
		}

		// extension methods
		if ($cb = self::extensionMethod("$class::$name")) {
			array_unshift($args, $this);
			return call_user_func_array($cb, $args);
		}

		throw new LogicException("Call to undefined method $class::$name().");
	}

	/**
	 * Call to undefined static method.
	 *
	 * @param  string  method name (in lower case!)
	 * @param  array   arguments
	 * @return mixed
	 * @throws LogicException
	 */
	public static function __callStatic($name, $args)
	{
		$class = get_called_class();
		throw new LogicException("Call to undefined static method $class::$name().");
	}

	/**
	 * Adding method to class.
	 *
	 * @param  string  method name
	 * @param  mixed   callback or closure
	 * @return mixed
	 */
	public static function extensionMethod($name, $callback = NULL)
	{
		if (self::$extMethods === NULL || $name === NULL) { // for backwards compatibility
			$list = get_defined_functions();
			foreach ($list['user'] as $fce) {
				$pair = explode('_prototype_', $fce);
				if (count($pair) === 2) {
					self::$extMethods[$pair[1]][$pair[0]] = $fce;
					self::$extMethods[$pair[1]][''] = NULL;
				}
			}
			if ($name === NULL) {
				return NULL;
			}
		}

		$name = strtolower($name);
		$a = strrpos($name, ':'); // search ::
		if ($a === FALSE) {
			$class = strtolower(get_called_class());
			$l = & self::$extMethods[$name];
		} else {
			$class = substr($name, 0, $a - 1);
			$l = & self::$extMethods[substr($name, $a + 1)];
		}

		if ($callback !== NULL) { // works as setter
			$l[$class] = $callback;
			$l[''] = NULL;
			return NULL;
		}

		// works as getter
		if (empty($l)) {
			return FALSE;

		} elseif (isset($l[''][$class])) { // cached value
			return $l[''][$class];
		}
		$cl = $class;
		do {
			$cl = strtolower($cl);
			if (isset($l[$cl])) {
				return $l[''][$class] = $l[$cl];
			}
		} while (($cl = get_parent_class($cl)) !== FALSE);

		foreach (class_implements($class) as $cl) {
			$cl = strtolower($cl);
			if (isset($l[$cl])) {
				return $l[''][$class] = $l[$cl];
			}
		}
		return $l[''][$class] = FALSE;
	}

	/**
	 * Returns property value. Do not call directly.
	 *
	 * @param  string  property name
	 * @return mixed   property value
	 * @throws LogicException if the property is not defined.
	 */
	public function & __get($name)
	{
		$class = get_class($this);

		if ($name === '') {
			throw new LogicException("Cannot read a class '$class' property without name.");
		}

		// property getter support
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
		$m = 'get' . $name;
		if (self::hasAccessor($class, $m)) {
			// ampersands:
			// - uses & __get() because declaration should be forward compatible (e.g. with Nette\Web\Html)
			// - doesn't call & $this->$m because user could bypass property setter by: $x = & $obj->property; $x = 'new value';
			$val = $this->$m();
			return $val;
		}

		$m = 'is' . $name;
		if (self::hasAccessor($class, $m)) {
			$val = $this->$m();
			return $val;
		}

		$name = func_get_arg(0);
		throw new LogicException("Cannot read an undeclared property $class::\$$name.");
	}

	/**
	 * Sets value of a property. Do not call directly.
	 *
	 * @param  string  property name
	 * @param  mixed   property value
	 * @return void
	 * @throws LogicException if the property is not defined or is read-only
	 */
	public function __set($name, $value)
	{
		$class = get_class($this);

		if ($name === '') {
			throw new LogicException("Cannot assign to a class '$class' property without name.");
		}

		// property setter support
		$name[0] = $name[0] & "\xDF"; // case-sensitive checking, capitalize first character
		if (self::hasAccessor($class, 'get' . $name) || self::hasAccessor($class, 'is' . $name)) {
			$m = 'set' . $name;
			if (self::hasAccessor($class, $m)) {
				$this->$m($value);
				return;

			} else {
				$name = func_get_arg(0);
				throw new LogicException("Cannot assign to a read-only property $class::\$$name.");
			}
		}

		$name = func_get_arg(0);
		throw new LogicException("Cannot assign to an undeclared property $class::\$$name.");
	}

	/**
	 * Is property defined?
	 *
	 * @param  string  property name
	 * @return bool
	 */
	public function __isset($name)
	{
		$name[0] = $name[0] & "\xDF";
		return $name !== '' && self::hasAccessor(get_class($this), 'get' . $name);
	}

	/**
	 * Access to undeclared property.
	 *
	 * @param  string  property name
	 * @return void
	 * @throws LogicException
	 */
	public function __unset($name)
	{
		$class = get_class($this);
		throw new LogicException("Cannot unset the property $class::\$$name.");
	}

	/**
	 * Has property an accessor?
	 *
	 * @param  string  class name
	 * @param  string  method name
	 * @return bool
	 */
	private static function hasAccessor($c, $m)
	{
		static $cache;
		if (!isset($cache[$c])) {
			// get_class_methods returns private, protected and public methods of Object (doesn't matter)
			// and ONLY PUBLIC methods of descendants (perfect!)
			// but returns static methods too (nothing doing...)
			// and is much faster than reflection
			// (works good since 5.0.4)
			$cache[$c] = array_flip(get_class_methods($c));
		}
		return isset($cache[$c][$m]);
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Texy parser base class.
 */
class TexyParser extends TexyObject
{
	/** @var Texy */
	protected $texy;

	/** @var TexyHtml */
	protected $element;

	/** @var array */
	public $patterns;

	/**
	 * @return Texy
	 */
	public function getTexy()
	{
		return $this->texy;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

class TexyRegexp
{
	const ALL = 1;
	const OFFSET_CAPTURE = 2;

	private static $messages = array(
		PREG_INTERNAL_ERROR => 'Internal error',
		PREG_BACKTRACK_LIMIT_ERROR => 'Backtrack limit was exhausted',
		PREG_RECURSION_LIMIT_ERROR => 'Recursion limit was exhausted',
		PREG_BAD_UTF8_ERROR => 'Malformed UTF-8 data',
		5 => 'Offset didn\'t correspond to the begin of a valid UTF-8 code point', // PREG_BAD_UTF8_OFFSET_ERROR
	);

	/**
	 * Splits string by a regular expression.
	 * @param  string
	 * @param  string
	 * @param  int  OFFSET_CAPTURE
	 * @return array
	 */
	public static function split($subject, $pattern, $flags = 0)
	{
		$reFlags = (($flags & self::OFFSET_CAPTURE) ? PREG_SPLIT_OFFSET_CAPTURE : 0) | PREG_SPLIT_DELIM_CAPTURE;
		$res = preg_split($pattern, $subject, -1, $reFlags);
		if (preg_last_error()) { // run-time error
			trigger_error(@self::$messages[preg_last_error()], E_USER_WARNING);
		}
		return $res;
	}

	/**
	 * Performs a regular expression match.
	 * @param  string
	 * @param  string
	 * @param  int  OFFSET_CAPTURE, ALL
	 * @param  int  offset in bytes
	 * @return mixed
	 */
	public static function match($subject, $pattern, $flags = 0, $offset = 0)
	{
		$empty = $flags & self::ALL ? array() : NULL;
		if ($offset > strlen($subject)) {
			return $empty;
		}
		$reFlags = ($flags & self::OFFSET_CAPTURE) ? PREG_OFFSET_CAPTURE : 0;
		if ($flags & self::ALL) {
			$res = preg_match_all($pattern, $subject, $m, $reFlags | PREG_SET_ORDER, $offset);
		} else {
			$res = preg_match($pattern, $subject, $m, $reFlags, $offset);
		}
		if (preg_last_error()) { // run-time error
			trigger_error(@self::$messages[preg_last_error()], E_USER_WARNING);
		} elseif ($res) {
			return $m;
		}
		return $empty;
	}

	/**
	 * Perform a regular expression search and replace.
	 * @param  string
	 * @param  string|array
	 * @param  string|callable
	 * @return string
	 */
	public static function replace($subject, $pattern, $replacement = NULL)
	{
		if (is_object($replacement) || is_array($replacement)) {
			$res = preg_replace_callback($pattern, $replacement, $subject);
			if ($res === NULL && preg_last_error()) { // run-time error
				trigger_error(@self::$messages[preg_last_error()], E_USER_WARNING);
			}
			return $res;

		} elseif ($replacement === NULL && is_array($pattern)) {
			$replacement = array_values($pattern);
			$pattern = array_keys($pattern);
		}

		$res = preg_replace($pattern, $replacement, $subject);
		if (preg_last_error()) { // run-time error
			trigger_error(@self::$messages[preg_last_error()], E_USER_WARNING);
		}
		return $res;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * UTF-8 helper.
 */
class TexyUtf
{
	/** @var array */
	private static $xlat;

	/** @var array */
	private static $xlatCache;

	/**
	 * static class.
	 */
	final public function __construct()
	{
		throw new LogicException('Cannot instantiate static class ' . get_class($this));
	}

	/**
	 * Converts from source encoding to UTF-8.
	 */
	public static function toUtf($s, $encoding)
	{
		return iconv($encoding, 'UTF-8', $s);
	}

	/**
	 * Converts from UTF-8 to dest encoding.
	 */
	public static function utfTo($s, $encoding)
	{
		return iconv('UTF-8', $encoding.'//TRANSLIT', $s);
	}

	/**
	 * StrToLower in UTF-8.
	 */
	public static function strtolower($s)
	{
		if (function_exists('mb_strtolower')) {
			return mb_strtolower($s, 'UTF-8');
		}

		return @iconv('WINDOWS-1250', 'UTF-8', strtr( // intentionally @
			iconv('UTF-8', 'WINDOWS-1250//IGNORE', $s),
			"ABCDEFGHIJKLMNOPQRSTUVWXYZ\x8a\x8c\x8d\x8e\x8f\xa3\xa5\xaa\xaf\xbc\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2\xd3\xd4\xd5\xd6\xd8\xd9\xda\xdb\xdc\xdd\xde",
			"abcdefghijklmnopqrstuvwxyz\x9a\x9c\x9d\x9e\x9f\xb3\xb9\xba\xbf\xbe\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe"
		));
	}

	/**
	 * Converts UTF-8 to ASCII.
	 * iconv('UTF-8', 'ASCII//TRANSLIT', ...) has problem with glibc!
	 */
	public static function utf2ascii($s)
	{
		$s = strtr($s, '`\'"^~', '-----');
		if (ICONV_IMPL === 'glibc') {
			$s = @iconv('UTF-8', 'WINDOWS-1250//TRANSLIT', $s); // intentionally @
			$s = strtr($s, "\xa5\xa3\xbc\x8c\xa7\x8a\xaa\x8d\x8f\x8e\xaf\xb9\xb3\xbe\x9c\x9a\xba\x9d\x9f\x9e\xbf\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0\xd1\xd2"
				."\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0\xf1\xf2\xf3\xf4\xf5\xf6\xf8\xf9\xfa\xfb\xfc\xfd\xfe",
				'ALLSSSSTZZZallssstzzzRAAAALCCCEEEEIIDDNNOOOOxRUUUUYTsraaaalccceeeeiiddnnooooruuuuyt');
		} else {
			$s = @iconv('UTF-8', 'ASCII//TRANSLIT', $s); // intentionally @
		}
		$s = str_replace(array('`', "'", '"', '^', '~'), '', $s);
		return $s;
	}

	/**
	 * Converts UTF-8 to dest encoding + html entities.
	 */
	public static function utf2html($s, $encoding)
	{
		// convert from UTF-8
		if (strcasecmp($encoding, 'utf-8') === 0) {
			return $s;
		}

		// prepare UTF-8 -> charset table
		self::$xlat = & self::$xlatCache[strtolower($encoding)];
		if (!self::$xlat) {
			for ($i = 128; $i < 256; $i++) {
				$ch = @iconv($encoding, 'UTF-8//IGNORE', chr($i)); // intentionally @
				if ($ch) {
					self::$xlat[$ch] = chr($i);
				}
			}
		}

		// convert
		return preg_replace_callback('#[\x80-\x{FFFF}]#u', array(__CLASS__, 'cb'), $s);
	}

	/**
	 * Callback; converts UTF-8 to HTML entity OR character in dest encoding.
	 */
	private static function cb($m)
	{
		$m = $m[0];
		if (isset(self::$xlat[$m])) {
			return self::$xlat[$m];
		}

		$ch1 = ord($m[0]);
		$ch2 = ord($m[1]);
		if (($ch2 >> 6) !== 2) {
			return '';
		}

		if (($ch1 & 0xE0) === 0xC0) {
			return '&#' . ((($ch1 & 0x1F) << 6) + ($ch2 & 0x3F)) . ';';
		}

		if (($ch1 & 0xF0) === 0xE0) {
			$ch3 = ord($m[2]);
			if (($ch3 >> 6) !== 2) {
				return '';
			}
			return '&#' . ((($ch1 & 0xF) << 12) + (($ch2 & 0x3F) << 06) + (($ch3 & 0x3F))) . ';';
		}

		return '';
	}

}


code:
<?php

/**
 * For Texy 1 backward compatibility.
 */
define('TEXY_ALL', Texy::ALL);
define('TEXY_NONE', Texy::NONE);
define('TEXY_CONTENT_MARKUP', Texy::CONTENT_MARKUP);
define('TEXY_CONTENT_REPLACED', Texy::CONTENT_REPLACED);
define('TEXY_CONTENT_TEXTUAL', Texy::CONTENT_TEXTUAL);
define('TEXY_CONTENT_BLOCK', Texy::CONTENT_BLOCK);
define('TEXY_VERSION', Texy::VERSION);
define('TEXY_HEADING_DYNAMIC', 1);
define('TEXY_HEADING_FIXED', 2);

/**
 * For Texy 2.2 compatibility
 */
define('TEXY_CHAR',        TexyPatterns::CHAR);
define('TEXY_MARK',        TexyPatterns::MARK);
define('TEXY_MODIFIER',    TexyPatterns::MODIFIER);
define('TEXY_MODIFIER_H',  TexyPatterns::MODIFIER_H);
define('TEXY_MODIFIER_HV', TexyPatterns::MODIFIER_HV);
define('TEXY_IMAGE',       TexyPatterns::IMAGE);
define('TEXY_LINK_URL',    TexyPatterns::LINK_URL);
define('TEXY_LINK',        '(?::('.TEXY_LINK_URL.'))');
define('TEXY_LINK_N',      '(?::('.TEXY_LINK_URL.'|:))');
define('TEXY_EMAIL',       '['.TEXY_CHAR.'][0-9.+_'.TEXY_CHAR.'-]{0,63}@[0-9.+_'.TEXY_CHAR.'\x{ad}-]{1,252}\.['.TEXY_CHAR.'\x{ad}]{2,19}');
define('TEXY_URLSCHEME',   '[a-z][a-z0-9+.-]{0,20}:');


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Special blocks module.
 */
final class TexyBlockModule extends TexyModule
{

	public function __construct($texy)
	{
		$this->texy = $texy;

		//$texy->allowed['blocks'] = TRUE;
		$texy->allowed['block/default'] = TRUE;
		$texy->allowed['block/pre'] = TRUE;
		$texy->allowed['block/code'] = TRUE;
		$texy->allowed['block/html'] = TRUE;
		$texy->allowed['block/text'] = TRUE;
		$texy->allowed['block/texysource'] = TRUE;
		$texy->allowed['block/comment'] = TRUE;
		$texy->allowed['block/div'] = TRUE;

		$texy->addHandler('block', array($this, 'solve'));
		$texy->addHandler('beforeBlockParse', array($this, 'beforeBlockParse'));

		$texy->registerBlockPattern(
			array($this, 'pattern'),
			'#^/--++ *+(.*)'.TexyPatterns::MODIFIER_H.'?$((?:\n(?0)|\n.*+)*)(?:\n\\\\--.*$|\z)#mUi',
			'blocks'
		);
	}

	/**
	 * Single block pre-processing.
	 * @param  TexyBlockParser
	 * @param  string
	 * @return void
	 */
	public function beforeBlockParse($parser, & $text)
	{
		// autoclose exclusive blocks
		$text = TexyRegexp::replace(
			$text,
			'#^(/--++ *+(?!div|texysource).*)$((?:\n.*+)*?)(?:\n\\\\--.*$|(?=(\n/--.*$)))#mi',
			"\$1\$2\n\\--"
		);
	}

	/**
	 * Callback for:.
	 * /-----code html .(title)[class]{style}
	 * ....
	 * ....
	 * \----
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function pattern($parser, $matches)
	{
		list(, $mParam, $mMod, $mContent) = $matches;
		// [1] => code | text | ...
		// [2] => ... additional parameters
		// [3] => .(title)[class]{style}<>
		// [4] => ... content

		$mod = new TexyModifier($mMod);
		$parts = preg_split('#\s+#u', $mParam, 2);
		$blocktype = empty($parts[0]) ? 'block/default' : 'block/' . $parts[0];
		$param = empty($parts[1]) ? NULL : $parts[1];

		return $this->texy->invokeAroundHandlers('block', $parser, array($blocktype, $mContent, $param, $mod));
	}

	// for backward compatibility
	function outdent($s)
	{
		trigger_error('Use Texy::outdent()', E_USER_WARNING);
		return Texy::outdent($s);
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  string   blocktype
	 * @param  string   content
	 * @param  string   additional parameter
	 * @param  TexyModifier
	 * @return TexyHtml|string|FALSE
	 */
	public function solve($invocation, $blocktype, $s, $param, $mod)
	{
		$tx = $this->texy;
		$parser = $invocation->getParser();

		if ($blocktype === 'block/texy') {
			$el = TexyHtml::el();
			$el->parseBlock($tx, $s, $parser->isIndented());
			return $el;
		}

		if (empty($tx->allowed[$blocktype])) {
			return FALSE;
		}

		if ($blocktype === 'block/texysource') {
			$s = Texy::outdent($s);
			if ($s === '') {
				return "\n";
			}
			$el = TexyHtml::el();
			if ($param === 'line') {
				$el->parseLine($tx, $s);
			} else {
				$el->parseBlock($tx, $s);
			}
			$s = $el->toHtml($tx);
			$blocktype = 'block/code';
			$param = 'html'; // to be continue (as block/code)
		}

		if ($blocktype === 'block/code') {
			$s = Texy::outdent($s);
			if ($s === '') {
				return "\n";
			}
			$s = Texy::escapeHtml($s);
			$s = $tx->protect($s, Texy::CONTENT_BLOCK);
			$el = TexyHtml::el('pre');
			$mod->decorate($tx, $el);
			$el->attrs['class'][] = $param; // lang
			$el->create('code', $s);
			return $el;
		}

		if ($blocktype === 'block/default') {
			$s = Texy::outdent($s);
			if ($s === '') {
				return "\n";
			}
			$el = TexyHtml::el('pre');
			$mod->decorate($tx, $el);
			$el->attrs['class'][] = $param; // lang
			$s = Texy::escapeHtml($s);
			$s = $tx->protect($s, Texy::CONTENT_BLOCK);
			$el->setText($s);
			return $el;
		}

		if ($blocktype === 'block/pre') {
			$s = Texy::outdent($s);
			if ($s === '') {
				return "\n";
			}
			$el = TexyHtml::el('pre');
			$mod->decorate($tx, $el);
			$lineParser = new TexyLineParser($tx, $el);
			// special mode - parse only html tags
			$tmp = $lineParser->patterns;
			$lineParser->patterns = array();
			if (isset($tmp['html/tag'])) {
				$lineParser->patterns['html/tag'] = $tmp['html/tag'];
			}
			if (isset($tmp['html/comment'])) {
				$lineParser->patterns['html/comment'] = $tmp['html/comment'];
			}
			unset($tmp);

			$lineParser->parse($s);
			$s = $el->getText();
			$s = Texy::unescapeHtml($s);
			$s = Texy::escapeHtml($s);
			$s = $tx->unprotect($s);
			$s = $tx->protect($s, Texy::CONTENT_BLOCK);
			$el->setText($s);
			return $el;
		}

		if ($blocktype === 'block/html') {
			$s = trim($s, "\n");
			if ($s === '') {
				return "\n";
			}
			$el = TexyHtml::el();
			$lineParser = new TexyLineParser($tx, $el);
			// special mode - parse only html tags
			$tmp = $lineParser->patterns;
			$lineParser->patterns = array();
			if (isset($tmp['html/tag'])) {
				$lineParser->patterns['html/tag'] = $tmp['html/tag'];
			}
			if (isset($tmp['html/comment'])) {
				$lineParser->patterns['html/comment'] = $tmp['html/comment'];
			}
			unset($tmp);

			$lineParser->parse($s);
			$s = $el->getText();
			$s = Texy::unescapeHtml($s);
			$s = Texy::escapeHtml($s);
			$s = $tx->unprotect($s);
			return $tx->protect($s, Texy::CONTENT_BLOCK) . "\n";
		}

		if ($blocktype === 'block/text') {
			$s = trim($s, "\n");
			if ($s === '') {
				return "\n";
			}
			$s = Texy::escapeHtml($s);
			$s = str_replace("\n", TexyHtml::el('br')->startTag(), $s); // nl2br
			return $tx->protect($s, Texy::CONTENT_BLOCK) . "\n";
		}

		if ($blocktype === 'block/comment') {
			return "\n";
		}

		if ($blocktype === 'block/div') {
			$s = Texy::outdent($s);
			if ($s === '') {
				return "\n";
			}
			$el = TexyHtml::el('div');
			$mod->decorate($tx, $el);
			$el->parseBlock($tx, $s, $parser->isIndented()); // TODO: INDENT or NORMAL ?
			return $el;
		}

		return FALSE;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Blockquote module.
 */
final class TexyBlockQuoteModule extends TexyModule
{

	public function __construct($texy)
	{
		$this->texy = $texy;

		$texy->registerBlockPattern(
			array($this, 'pattern'),
			'#^(?:'.TexyPatterns::MODIFIER_H.'\n)?\>(?!>)(\ *+|:)(\S.*+)$#mU', // original
			// '#^(?:'.TexyPatterns::MODIFIER_H.'\n)?\>(?:(\>|\ +?|:)(.*))?()$#mU', // >>>>
			// '#^(?:'.TexyPatterns::MODIFIER_H.'\n)?\>(?:(\ +?|:)(.*))()$#mU', // only >
			'blockquote'
		);
	}

	/**
	 * Callback for:.
	 *
	 * > They went in single file, running like hounds on a strong scent,
	 * and an eager light was in their eyes. Nearly due west the broad
	 * swath of the marching Orcs tramped its ugly slot; the sweet grass
	 * of Rohan had been bruised and blackened as they passed.
	 * >:http://www.mycom.com/tolkien/twotowers.html
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function pattern($parser, $matches)
	{
		list(, $mMod, $mPrefix, $mContent) = $matches;
		// [1] => .(title)[class]{style}<>
		// [2] => spaces |
		// [3] => ... / LINK

		$tx = $this->texy;

		$el = TexyHtml::el('blockquote');
		$mod = new TexyModifier($mMod);
		$mod->decorate($tx, $el);

		$content = '';
		$spaces = '';
		do {
			if ($mPrefix === ':') {
				$mod->cite = $tx->blockQuoteModule->citeLink($mContent);
				$content .= "\n";
			} else {
				if ($spaces === '') {
					$spaces = max(1, strlen($mPrefix));
				}
				$content .= $mContent . "\n";
			}

			if (!$parser->next("#^>(?:|(\\ {1,$spaces}|:)(.*))()$#mA", $matches)) {
				break;
			}

/*
			if ($mPrefix === '>') {
				$content .= $mPrefix . $mContent . "\n";
			} elseif ($mPrefix === ':') {
				$mod->cite = $tx->blockQuoteModule->citeLink($mContent);
				$content .= "\n";
			} else {
				if ($spaces === '') $spaces = max(1, strlen($mPrefix));
				$content .= $mContent . "\n";
			}
			if (!$parser->next("#^\\>(?:(\\>|\\ {1,$spaces}|:)(.*))?()$#mA", $matches)) break;
*/

			list(, $mPrefix, $mContent) = $matches;
		} while (TRUE);

		$el->attrs['cite'] = $mod->cite;
		$el->parseBlock($tx, $content, $parser->isIndented());

		// no content?
		if (!$el->count()) {
			return FALSE;
		}

		// event listener
		$tx->invokeHandlers('afterBlockquote', array($parser, $el, $mod));

		return $el;
	}

	/**
	 * Converts cite source to URL.
	 * @param  string
	 * @return string|NULL
	 */
	public function citeLink($link)
	{
		$tx = $this->texy;

		if ($link == NULL) {
			return NULL;
		}

		if ($link[0] === '[') { // [ref]
			$link = substr($link, 1, -1);
			$ref = $tx->linkModule->getReference($link);
			if ($ref) {
				return Texy::prependRoot($ref->URL, $tx->linkModule->root);
			}
		}

		// special supported case
		if (strncasecmp($link, 'www.', 4) === 0) {
			return 'https://' . $link;
		}

		return Texy::prependRoot($link, $tx->linkModule->root);
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Emoticon module.
 */
final class TexyEmoticonModule extends TexyModule
{
	/** @var array  supported emoticons and image files */
	public $icons = array(
		':-)' => 'smile.gif',
		':-(' => 'sad.gif',
		';-)' => 'wink.gif',
		':-D' => 'biggrin.gif',
		'8-O' => 'eek.gif',
		'8-)' => 'cool.gif',
		':-?' => 'confused.gif',
		':-x' => 'mad.gif',
		':-P' => 'razz.gif',
		':-|' => 'neutral.gif',
	);

	/** @var string  CSS class for emoticons */
	public $class;

	/** @var string  root of relative images (default value is $texy->imageModule->root) */
	public $root;

	/** @var string  physical location of images on server (default value is $texy->imageModule->fileRoot) */
	public $fileRoot;

	public function __construct($texy)
	{
		$this->texy = $texy;
		$texy->allowed['emoticon'] = FALSE;
		$texy->addHandler('emoticon', array($this, 'solve'));
		$texy->addHandler('beforeParse', array($this, 'beforeParse'));
	}

	public function beforeParse()
	{
		if (empty($this->texy->allowed['emoticon'])) {
			return;
		}

		krsort($this->icons);

		$pattern = array();
		foreach ($this->icons as $key => $foo) {
			$pattern[] = preg_quote($key, '#') . '+'; // last char can be repeated
		}

		$this->texy->registerLinePattern(
			array($this, 'pattern'),
			'#(?<=^|[\\x00-\\x20])(' . implode('|', $pattern) . ')#',
			'emoticon',
			'#' . implode('|', $pattern) . '#'
		);
	}

	/**
	 * Callback for: :-))).
	 *
	 * @param  TexyLineParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function pattern($parser, $matches)
	{
		$match = $matches[0];
		$tx = $this->texy;

		// find the closest match
		foreach ($this->icons as $emoticon => $foo) {
			if (strncmp($match, $emoticon, strlen($emoticon)) === 0) {
				return $tx->invokeAroundHandlers('emoticon', $parser, array($emoticon, $match));
			}
		}

		return FALSE; // tohle se nestane
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  string
	 * @param  string
	 * @return TexyHtml|FALSE
	 */
	public function solve($invocation, $emoticon, $raw)
	{
		$tx = $this->texy;
		$file = $this->icons[$emoticon];
		$el = TexyHtml::el('img');
		$el->attrs['src'] = Texy::prependRoot($file, $this->root === NULL ? $tx->imageModule->root : $this->root);
		$el->attrs['alt'] = $raw;
		$el->attrs['class'][] = $this->class;

		// file path
		$file = rtrim($this->fileRoot === NULL ? $tx->imageModule->fileRoot : $this->fileRoot, '/\\') . '/' . $file;
		if (@is_file($file)) { // intentionally @
			$size = @getImageSize($file); // intentionally @
			if (is_array($size)) {
				$el->attrs['width'] = $size[0];
				$el->attrs['height'] = $size[1];
			}
		}
		$tx->summary['images'][] = $el->attrs['src'];
		return $el;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * The captioned figures.
 */
final class TexyFigureModule extends TexyModule
{
	/** @var string  non-floated box CSS class */
	public $class = 'figure';

	/** @var string  left-floated box CSS class */
	public $leftClass;

	/** @var string  right-floated box CSS class */
	public $rightClass;

	/** @var int  how calculate div's width */
	public $widthDelta = 10;

	public function __construct($texy)
	{
		$this->texy = $texy;

		$texy->addHandler('figure', array($this, 'solve'));

		$texy->registerBlockPattern(
			array($this, 'pattern'),
			'#^\[\* *+([^\n'.TexyPatterns::MARK.']{1,1000})'.TexyPatterns::MODIFIER.'? *+(\*|(?<!<)>|<)\]' // [* urls .(title)[class]{style} >]
			. '(?::('.TexyPatterns::LINK_URL.'|:))?? ++\*\*\* ++(.{0,2000})'.TexyPatterns::MODIFIER_H.'?()$#mUu',
			'figure'
		);
	}

	/**
	 * Callback for [*image*]:link *** .... .(title)[class]{style}>.
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function pattern($parser, $matches)
	{
		list(, $mURLs, $mImgMod, $mAlign, $mLink, $mContent, $mMod) = $matches;
		// [1] => URLs
		// [2] => .(title)[class]{style}<>
		// [3] => * < >
		// [4] => url | [ref] | [*image*]
		// [5] => ...
		// [6] => .(title)[class]{style}<>

		$tx = $this->texy;
		$image = $tx->imageModule->factoryImage($mURLs, $mImgMod.$mAlign);
		$mod = new TexyModifier($mMod);
		$mContent = ltrim($mContent);

		if ($mLink) {
			if ($mLink === ':') {
				$link = new TexyLink($image->linkedURL === NULL ? $image->URL : $image->linkedURL);
				$link->raw = ':';
				$link->type = TexyLink::IMAGE;
			} else {
				$link = $tx->linkModule->factoryLink($mLink, NULL, NULL);
			}
		} else {
			$link = NULL;
		}

		return $tx->invokeAroundHandlers('figure', $parser, array($image, $link, $mContent, $mod));
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  TexyImage
	 * @param  TexyLink
	 * @param  string
	 * @param  TexyModifier
	 * @return TexyHtml|FALSE
	 */
	public function solve($invocation, TexyImage $image, $link, $content, $mod)
	{
		$tx = $this->texy;

		$hAlign = $image->modifier->hAlign;
		$image->modifier->hAlign = NULL;

		$elImg = $tx->imageModule->solve(NULL, $image, $link); // returns TexyHtml or false!
		if (!$elImg) {
			return FALSE;
		}

		$el = TexyHtml::el('div');
		if (!empty($image->width) && $this->widthDelta !== FALSE) {
			$el->attrs['style']['width'] = ($image->width + $this->widthDelta) . 'px';
		}
		$mod->decorate($tx, $el);

		$el[0] = $elImg;
		$el[1] = TexyHtml::el('p');
		$el[1]->parseLine($tx, ltrim($content));

		$class = $this->class;
		if ($hAlign) {
			$var = $hAlign . 'Class'; // leftClass, rightClass
			if (!empty($this->$var)) {
				$class = $this->$var;

			} elseif (empty($tx->alignClasses[$hAlign])) {
				$el->attrs['style']['float'] = $hAlign;

			} else {
				$class .= '-' . $tx->alignClasses[$hAlign];
			}
		}
		$el->attrs['class'][] = $class;

		return $el;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Heading module.
 */
final class TexyHeadingModule extends TexyModule
{
	const
		DYNAMIC = 1, // auto-leveling
		FIXED = 2; // fixed-leveling

	/** @var string  textual content of first heading */
	public $title;

	/** @var array  generated Table of Contents */
	public $TOC;

	/** @var bool  autogenerate ID */
	public $generateID = FALSE;

	/** @var string  prefix for autogenerated ID */
	public $idPrefix = 'toc-';

	/** @var int  level of top heading, 1..6 */
	public $top = 1;

	/** @var bool  surrounded headings: more #### means higher heading */
	public $moreMeansHigher = TRUE;

	/** @var int  balancing mode */
	public $balancing = self::DYNAMIC;

	/** @var array  when $balancing = TexyHeadingModule::FIXED */
	public $levels = array(
		'#' => 0, // # --> $levels['#'] + $top = 0 + 1 = 1 --> <h1> ... </h1>
		'*' => 1,
		'=' => 2,
		'-' => 3,
	);

	/** @var array  used ID's */
	private $usedID;

	public function __construct($texy)
	{
		$this->texy = $texy;

		$texy->addHandler('heading', array($this, 'solve'));
		$texy->addHandler('beforeParse', array($this, 'beforeParse'));
		$texy->addHandler('afterParse', array($this, 'afterParse'));

		$texy->registerBlockPattern(
			array($this, 'patternUnderline'),
			'#^(\S.{0,1000})'.TexyPatterns::MODIFIER_H.'?\n'
			. '(\#{3,}+|\*{3,}+|={3,}+|-{3,}+)$#mU',
			'heading/underlined'
		);

		$texy->registerBlockPattern(
			array($this, 'patternSurround'),
			'#^(\#{2,}+|={2,}+)(.+)'.TexyPatterns::MODIFIER_H.'?()$#mU',
			'heading/surrounded'
		);
	}

	public function beforeParse()
	{
		$this->title = NULL;
		$this->usedID = array();
		$this->TOC = array();
	}

	/**
	 * @param  Texy
	 * @param  TexyHtml
	 * @param  bool
	 * @return void
	 */
	public function afterParse($texy, $DOM, $isSingleLine)
	{
		if ($isSingleLine) {
			return;
		}

		if ($this->balancing === self::DYNAMIC) {
			$top = $this->top;
			$map = array();
			$min = 100;
			foreach ($this->TOC as $item) {
				$level = $item['level'];
				if ($item['type'] === 'surrounded') {
					$min = min($level, $min);
					$top = $this->top - $min;

				} elseif ($item['type'] === 'underlined') {
					$map[$level] = $level;
				}
			}

			asort($map);
			$map = array_flip(array_values($map));
		}

		foreach ($this->TOC as $key => $item) {
			if ($this->balancing === self::DYNAMIC) {
				if ($item['type'] === 'surrounded') {
					$level = $item['level'] + $top;

				} elseif ($item['type'] === 'underlined') {
					$level = $map[$item['level']] + $this->top;

				} else {
					$level = $item['level'];
				}

				$item['el']->setName('h' . min(6, max(1, $level)));
				$this->TOC[$key]['level'] = $level;
			}

			if ($this->generateID) {
				if (!empty($item['el']->style['toc']) && is_array($item['el']->style)) {
					$title = $item['el']->style['toc'];
					unset($item['el']->style['toc']);
				} else {
					$title = trim($item['el']->toText($this->texy));
				}
				$this->TOC[$key]['title'] = $title;
				if (empty($item['el']->attrs['id'])) {
					$id = $this->idPrefix . Texy::webalize($title);
					$counter = '';
					if (isset($this->usedID[$id . $counter])) {
						$counter = 2;
						while (isset($this->usedID[$id . '-' . $counter])) {
							$counter++;
						}
						$id .= '-' . $counter;
					}
					$this->usedID[$id] = TRUE;
					$item['el']->attrs['id'] = $id;
				}
			}
		}

		// document title
		if ($this->title === NULL && count($this->TOC)) {
			$item = reset($this->TOC);
			$this->title = isset($item['title']) ? $item['title'] : trim($item['el']->toText($this->texy));
		}
	}

	/**
	 * Callback for underlined heading.
	 *
	 * Heading .(title)[class]{style}>
	 * -------------------------------
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function patternUnderline($parser, $matches)
	{
		list(, $mContent, $mMod, $mLine) = $matches;
		// $matches:
		// [1] => ...
		// [2] => .(title)[class]{style}<>
		// [3] => ...

		$mod = new TexyModifier($mMod);
		$level = $this->levels[$mLine[0]];
		return $this->texy->invokeAroundHandlers('heading', $parser, array($level, $mContent, $mod, FALSE));
	}

	/**
	 * Callback for surrounded heading.
	 *
	 * ### Heading .(title)[class]{style}>
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function patternSurround($parser, $matches)
	{
		list(, $mLine, $mContent, $mMod) = $matches;
		// [1] => ###
		// [2] => ...
		// [3] => .(title)[class]{style}<>

		$mod = new TexyModifier($mMod);
		$level = min(7, max(2, strlen($mLine)));
		$level = $this->moreMeansHigher ? 7 - $level : $level - 2;
		$mContent = rtrim($mContent, $mLine[0] . ' ');
		return $this->texy->invokeAroundHandlers('heading', $parser, array($level, $mContent, $mod, TRUE));
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  int  0..5
	 * @param  string
	 * @param  TexyModifier
	 * @param  bool
	 * @return TexyHtml
	 */
	public function solve($invocation, $level, $content, $mod, $isSurrounded)
	{
		// as fixed balancing, for block/texysource & correct decorating
		$el = TexyHtml::el('h' . min(6, max(1, $level + $this->top)));
		$mod->decorate($this->texy, $el);

		$el->parseLine($this->texy, trim($content));

		$this->TOC[] = array(
			'el' => $el,
			'level' => $level,
			'type' => $isSurrounded ? 'surrounded' : 'underlined',
		);

		return $el;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Horizontal line module.
 */
final class TexyHorizLineModule extends TexyModule
{
	/** @var array  default CSS class */
	public $classes = array(
		'-' => NULL,
		'*' => NULL,
	);

	public function __construct($texy)
	{
		$this->texy = $texy;

		$texy->addHandler('horizline', array($this, 'solve'));

		$texy->registerBlockPattern(
			array($this, 'pattern'),
			'#^(\*{3,}+|-{3,}+)\ *'.TexyPatterns::MODIFIER.'?()$#mU',
			'horizline'
		);
	}

	/**
	 * Callback for: -------.
	 *
	 * @param  TexyBlockParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml
	 */
	public function pattern($parser, $matches)
	{
		list(, $mType, $mMod) = $matches;
		// [1] => ---
		// [2] => .(title)[class]{style}<>

		$mod = new TexyModifier($mMod);
		return $this->texy->invokeAroundHandlers('horizline', $parser, array($mType, $mod));
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  string
	 * @param  TexyModifier
	 * @return TexyHtml
	 */
	public function solve($invocation, $type, $modifier)
	{
		$el = TexyHtml::el('hr');
		$modifier->decorate($invocation->getTexy(), $el);

		$class = $this->classes[ $type[0] ];
		if ($class && !isset($modifier->classes[$class])) {
			$el->attrs['class'][] = $class;
		}

		return $el;
	}

}


code:
<?php

/**
 * This file is part of the Texy! (http://texy.info)
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
 */

/**
 * Html tags module.
 */
final class TexyHtmlModule extends TexyModule
{
	/** @var bool   pass HTML comments to output? */
	public $passComment = TRUE;

	public function __construct($texy)
	{
		$this->texy = $texy;

		$texy->addHandler('htmlComment', array($this, 'solveComment'));
		$texy->addHandler('htmlTag', array($this, 'solveTag'));

		$texy->registerLinePattern(
			array($this, 'patternTag'),
			'#<(/?)([a-z][a-z0-9_:-]{0,50})((?:\s++[a-z0-9:-]++|=\s*+"[^"'.TexyPatterns::MARK.']*+"|=\s*+\'[^\''.TexyPatterns::MARK.']*+\'|=[^\s>'.TexyPatterns::MARK.']++)*)\s*+(/?)>#isu',
			'html/tag'
		);

		$texy->registerLinePattern(
			array($this, 'patternComment'),
			'#<!--([^'.TexyPatterns::MARK.']*?)-->#is',
			'html/comment'
		);
	}

	/**
	 * Callback for: <!-- comment -->.
	 *
	 * @param  TexyLineParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function patternComment($parser, $matches)
	{
		list(, $mComment) = $matches;
		return $this->texy->invokeAroundHandlers('htmlComment', $parser, array($mComment));
	}

	/**
	 * Callback for: <tag attr="...">.
	 *
	 * @param  TexyLineParser
	 * @param  array      regexp matches
	 * @param  string     pattern name
	 * @return TexyHtml|string|FALSE
	 */
	public function patternTag($parser, $matches)
	{
		list(, $mEnd, $mTag, $mAttr, $mEmpty) = $matches;
		// [1] => /
		// [2] => tag
		// [3] => attributes
		// [4] => /

		$tx = $this->texy;

		$isStart = $mEnd !== '/';
		$isEmpty = $mEmpty === '/';
		if (!$isEmpty && substr($mAttr, -1) === '/') { // uvizlo v $mAttr?
			$mAttr = substr($mAttr, 0, -1);
			$isEmpty = TRUE;
		}

		// error - can't close empty element
		if ($isEmpty && !$isStart) {
			return FALSE;
		}

		// error - end element with atttrs
		$mAttr = trim(strtr($mAttr, "\n", ' '));
		if ($mAttr && !$isStart) {
			return FALSE;
		}

		$el = TexyHtml::el($mTag);

		if ($isStart) {
			// parse attributes
			$matches2 = NULL;
			preg_match_all(
				'#([a-z0-9:-]+)\s*(?:=\s*(\'[^\']*\'|"[^"]*"|[^\'"\s]+))?()#isu',
				$mAttr,
				$matches2,
				PREG_SET_ORDER
			);

			foreach ($matches2 as $m) {
				$key = strtolower($m[1]);
				$value = $m[2];
				if ($value == NULL) {
					$el->attrs[$key] = TRUE;
				} elseif ($value{0} === '\'' || $value{0} === '"') {
					$el->attrs[$key] = Texy::unescapeHtml(substr($value, 1, -1));
				} else {
					$el->attrs[$key] = Texy::unescapeHtml($value);
				}
			}
		}

		$res = $tx->invokeAroundHandlers('htmlTag', $parser, array($el, $isStart, $isEmpty));

		if ($res instanceof TexyHtml) {
			return $tx->protect($isStart ? $res->startTag() : $res->endTag(), $res->getContentType());
		}

		return $res;
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  TexyHtml  element
	 * @param  bool      is start tag?
	 * @param  bool      is empty?
	 * @return TexyHtml|string|FALSE
	 */
	public function solveTag($invocation, TexyHtml $el, $isStart, $forceEmpty = NULL)
	{
		$tx = $this->texy;

		// tag & attibutes
		$allowedTags = $tx->allowedTags; // speed-up
		if (!$allowedTags) {
			return FALSE; // all tags are disabled
		}

		// convert case
		$name = $el->getName();
		$lower = strtolower($name);
		if (isset($tx->dtd[$lower]) || $name === strtoupper($name)) {
			// complete UPPER convert to lower
			$name = $lower;
			$el->setName($name);
		}

		if (is_array($allowedTags)) {
			if (!isset($allowedTags[$name])) {
				return FALSE;
			}
			$allowedAttrs = $allowedTags[$name]; // allowed attrs

		} else {
			// allowedTags === Texy::ALL
			if ($forceEmpty) {
				$el->setName($name, TRUE);
			}
			$allowedAttrs = Texy::ALL; // all attrs are allowed
		}

		// end tag? we are finished
		if (!$isStart) {
			return $el;
		}

		$elAttrs = & $el->attrs;

		// process attributes
		if (!$allowedAttrs) {
			$elAttrs = array();

		} elseif (is_array($allowedAttrs)) {

			// skip disabled
			$allowedAttrs = array_flip($allowedAttrs);
			foreach ($elAttrs as $key => $foo) {
				if (!isset($allowedAttrs[$key])) {
					unset($elAttrs[$key]);
				}
			}
		}

		// apply allowedClasses
		$tmp = $tx->_classes; // speed-up
		if (isset($elAttrs['class'])) {
			if (is_array($tmp)) {
				$elAttrs['class'] = explode(' ', $elAttrs['class']);
				foreach ($elAttrs['class'] as $key => $value) {
					if (!isset($tmp[$value])) {
						unset($elAttrs['class'][$key]); // id & class are case-sensitive
					}
				}

			} elseif ($tmp !== Texy::ALL) {
				$elAttrs['class'] = NULL;
			}
		}

		// apply allowedClasses for ID
		if (isset($elAttrs['id'])) {
			if (is_array($tmp)) {
				if (!isset($tmp['#' . $elAttrs['id']])) {
					$elAttrs['id'] = NULL;
				}
			} elseif ($tmp !== Texy::ALL) {
				$elAttrs['id'] = NULL;
			}
		}

		// apply allowedStyles
		if (isset($elAttrs['style'])) {
			$tmp = $tx->_styles; // speed-up
			if (is_array($tmp)) {
				$styles = explode(';', $elAttrs['style']);
				$elAttrs['style'] = NULL;
				foreach ($styles as $value) {
					$pair = explode(':', $value, 2);
					$prop = trim($pair[0]);
					if (isset($pair[1]) && isset($tmp[strtolower($prop)])) { // CSS is case-insensitive
						$elAttrs['style'][$prop] = $pair[1];
					}
				}
			} elseif ($tmp !== Texy::ALL) {
				$elAttrs['style'] = NULL;
			}
		}

		if ($name === 'img') {
			if (isset($elAttrs['src'])) {
				$elAttrs['src'] = trim($elAttrs['src']);
				if (!$tx->checkURL($elAttrs['src'], Texy::FILTER_IMAGE)) {
					return FALSE;
				}

				$tx->summary['images'][] = $elAttrs['src'];
			}

		} elseif ($name === 'a') {
			if (!isset($elAttrs['href']) && !isset($elAttrs['name']) && !isset($elAttrs['id'])) {
				return FALSE;
			}
			if (isset($elAttrs['href'])) {
				$elAttrs['href'] = trim($elAttrs['href']);
				if ($tx->linkModule->forceNoFollow && strpos($elAttrs['href'], '//') !== FALSE) {
					if (isset($elAttrs['rel'])) {
						$elAttrs['rel'] = (array) $elAttrs['rel'];
					}
					$elAttrs['rel'][] = 'nofollow';
				}

				if (!$tx->checkURL($elAttrs['href'], Texy::FILTER_ANCHOR)) {
					return FALSE;
				}

				$tx->summary['links'][] = $elAttrs['href'];
			}

		} elseif (preg_match('#^h[1-6]#i', $name)) {
			$tx->headingModule->TOC[] = array(
				'el' => $el,
				'level' => (int) substr($name, 1),
				'type' => 'html',
			);
		}

		$el->validateAttrs($tx->dtd);

		return $el;
	}

	/**
	 * Finish invocation.
	 *
	 * @param  TexyHandlerInvocation  handler invocation
	 * @param  string
	 * @return string
	 */
	public function solveComment($invocation, $content)
	{
		if (!$this->passComment) {
			return '';
		}

		// sanitize comment
		$content = TexyRegexp::replace($content, '#-{2,}#', ' - ');
		$content = trim($content, '-');

		return $this->texy->protect('<!--' . $content . '-->', Texy::CONTENT_MARKUP);
	}

}