<?php
/**
 * Inspects and prints out PHP values as HTML in a nicer way than print_r().
 *
 * @author digitalnature
 * @link https://github.com/digitalnature/php-ref GitHub Repository
 * @version 1.2
 * @since 3.1.6
 * @package Wsal
 */

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
	exit;
}

/**
 * Shortcut to ref, HTML mode.
 */
function wsal_r() {

	// Arguments passed to this function.
	$args = func_get_args();

	// Options (operators) gathered by the expression parser;
	// This variable gets passed as reference to getInputExpressions(), which will store the operators in it.
	$options = array();

	// Names of the arguments that were passed to this function.
	$expressions = WSAL_Ref::getInputExpressions( $options );
	$capture = in_array( '@', $options, true );

	// Something went wrong while trying to parse the source expressions?.
	// If so, silently ignore this part and leave out the expression info.
	if ( func_num_args() !== count( $expressions ) ) {
		$expressions = null;
	}

	// Use HTML formatter only if we're not in CLI mode, or if return was requested.
	$format = (php_sapi_name() !== 'cli') || $capture ? 'html' : 'cliText';

	// IE goes funky if there's no doctype.
	if ( ! $capture && ($format === 'html') && ! headers_sent() && ( ! ob_get_level() || ini_get( 'output_buffering' )) ) {
		print '<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>';
	}

	$ref = new WSAL_Ref( $format );

	if ( $capture ) {
		ob_start();
	}

	foreach ( $args as $index => $arg ) {
		$ref->query( $arg, $expressions ? $expressions[ $index ] : null );
	}

	// Return the results if this function was called with the error suppression operator.
	if ( $capture ) {
		return ob_get_clean();
	}

	// Stop the script if this function was called with the bitwise not operator.
	if ( in_array( '~', $options, true ) && ($format === 'html') ) {
		print '</body></html>';
		exit( 0 );
	}
}



/**
 * Shortcut to ref, plain text mode.
 */
function wsal_rt() {
	$args        = func_get_args();
	$options     = array();
	$output      = '';
	$expressions = WSAL_Ref::getInputExpressions( $options );
	$capture     = in_array( '@', $options, true );
	$ref         = new WSAL_Ref( (php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText' );

	if ( func_num_args() !== count( $expressions ) ) {
		$expressions = null;
	}

	if ( ! headers_sent() ) {
		header( 'Content-Type: text/plain; charset=utf-8' );
	}

	if ( $capture ) {
		ob_start();
	}

	foreach ( $args as $index => $arg ) {
		$ref->query( $arg, $expressions ? $expressions[ $index ] : null );
	}

	if ( $capture ) {
		return ob_get_clean();
	}

	if ( in_array( '~', $options, true ) ) {
		exit( 0 );
	}
}



/**
 * REF is a nicer alternative to PHP's print_r() / var_dump().
 *
 * @version  1.0
 * @author   digitalnature - http://digitalnature.eu
 */
class WSAL_Ref {

	const MARKER_KEY = '_phpRefArrayMarker_';



	protected static

		/**
		 * CPU time used for processing
		 *
		 * @var  array
		 */
		$time = 0,

		/**
		 * Configuration (+ default values)
		 *
		 * @var  array
		 */
		$config = array(

			// initially expanded levels (for HTML mode only)
			'expLvl'               => 1,

			// depth limit (0 = no limit);
			// this is not related to recursion
			'maxDepth'             => 6,

			// show the place where r() has been called from
			'showBacktrace'        => true,

			// display iterator contents
			'showIteratorContents' => false,

			// display extra information about resources
			'showResourceInfo'     => true,

			// display method and parameter list on objects
			'showMethods'          => true,

			// display private properties / methods
			'showPrivateMembers'   => false,

			// peform string matches (date, file, functions, classes, json, serialized data, regex etc.)
			// note: seriously slows down queries on large amounts of data
			'showStringMatches'    => true,

			// shortcut functions used to access the query method below;
			// if they are namespaced, the namespace must be present as well (methods are not supported)
			'shortcutFunc'         => array( 'r', 'rt' ),

			// custom/external formatters (as associative array: format => className)
			'formatters'           => array(),

			// stylesheet path (for HTML only);
			// 'false' means no styles
			'stylePath'            => '{:dir}/ref.css',

			// javascript path (for HTML only);
			// 'false' means no js
			'scriptPath'           => '{:dir}/ref.js',

			// display url info via cURL
			'showUrls'             => false,

			// stop evaluation after this amount of time (seconds)
			'timeout'              => 10,

			// whether to produce W3c-valid HTML,
			// or unintelligible, but optimized markup that takes less space
			'validHtml'            => false,
		),

		/**
		 * Some environment variables
		 * used to determine feature support
		 *
		 * @var  array
		 */
		$env = array(),

		/**
		 * Timeout point
		 *
		 * @var  bool
		 */
		$timeout = -1,

		$debug = array(
			'cacheHits' => 0,
			'objects'   => 0,
			'arrays'    => 0,
			'scalars'   => 0,
		);


	protected

		/**
		 * Output formatter of this instance
		 *
		 * @var  RFormatter
		 */
		$fmt = null,

		/**
		 * Start time of the current instance
		 *
		 * @var  float
		 */
		$startTime = 0,

		/**
		 * Internally created objects
		 *
		 * @var  SplObjectStorage
		 */
		$intObjects = null;



	/**
	 * Constructor
	 *
	 * @param string|RFormatter $format - Output format ID, or formatter instance defaults to 'html'.
	 * @throws Exception - On error.
	 */
	public function __construct( $format = 'html' ) {

		static $didIni = false;

		if ( ! $didIni ) {
			$didIni = true;
			foreach ( array_keys( static::$config ) as $key ) {
				$iniVal = get_cfg_var( 'ref.' . $key );
				if ( $iniVal !== false ) {
					static::$config[ $key ] = $iniVal;
				}
			}
		}

		if ( $format instanceof RFormatter ) {
			$this->fmt = $format;

		} else {
			$format = isset( static::$config['formatters'][ $format ] ) ? static::$config['formatters'][ $format ] : 'R' . ucfirst( $format ) . 'Formatter';

			if ( ! class_exists( $format, false ) ) {
				throw new \Exception( sprintf( '%s class not found', $format ) );
			}

			$this->fmt = new $format();
		}

		if ( static::$env ) {
			return;
		}

		static::$env = array(

			// php 5.4+ ?
			'is54'         => version_compare( PHP_VERSION, '5.4' ) >= 0,

			// php 5.4.6+ ?
			'is546'        => version_compare( PHP_VERSION, '5.4.6' ) >= 0,

			// php 5.6+
			'is56'         => version_compare( PHP_VERSION, '5.6' ) >= 0,

			// php 7.0+ ?
			'is7'          => version_compare( PHP_VERSION, '7.0' ) >= 0,

			// curl extension running?
			'curlActive'   => function_exists( 'curl_version' ),

			// is the 'mbstring' extension active?
			'mbStr'        => function_exists( 'mb_detect_encoding' ),

			// @see: https://bugs.php.net/bug.php?id=52469
			'supportsDate' => (strncasecmp( PHP_OS, 'WIN', 3 ) !== 0) || (version_compare( PHP_VERSION, '5.3.10' ) >= 0),
		);
	}



	/**
	 * Enforce proper use of this class
	 *
	 * @param   string $name
	 */
	public function __get( $name ) {
		throw new \Exception( sprintf( 'No such property: %s', $name ) );
	}



	/**
	 * Enforce proper use of this class
	 *
	 * @param   string $name
	 * @param   mixed  $value
	 */
	public function __set( $name, $value ) {
		throw new \Exception( sprintf( 'Cannot set %s. Not allowed', $name ) );
	}



	/**
	 * Generate structured information about a variable/value/expression (subject)
	 *
	 * Output is flushed to the screen
	 *
	 * @param   mixed  $subject
	 * @param   string $expression
	 */
	public function query( $subject, $expression = null ) {

		if ( static::$timeout > 0 ) {
			return;
		}

		$this->startTime = microtime( true );

		$this->intObjects = new \SplObjectStorage();

		$this->fmt->startRoot();
		$this->fmt->startExp();
		$this->evaluateExp( $expression );
		$this->fmt->endExp();
		$this->evaluate( $subject );
		$this->fmt->endRoot();
		$this->fmt->flush();

		static::$time += microtime( true ) - $this->startTime;
	}




	/**
	 * Executes a function the given number of times and returns the elapsed time.
	 *
	 * Keep in mind that the returned time includes function call overhead (including
	 * microtime calls) x iteration count. This is why this is better suited for
	 * determining which of two or more functions is the fastest, rather than
	 * finding out how fast is a single function.
	 *
	 * @param   int      $iterations      Number of times the function will be executed
	 * @param   callable $function   Function to execute
	 * @param   mixed    &$output       If given, last return value will be available in this variable
	 * @return  double               Elapsed time
	 */
	public static function timeFunc( $iterations, $function, &$output = null ) {

		$time = 0;

		for ( $i = 0; $i < $iterations; $i++ ) {
			$start  = microtime( true );
			$output = call_user_func( $function );
			$time  += microtime( true ) - $start;
		}

		return round( $time, 4 );
	}



	/**
	 * Timer utility
	 *
	 * First call of this function will start the timer.
	 * The second call will stop the timer and return the elapsed time
	 * since the timer started.
	 *
	 * Multiple timers can be controlled simultaneously by specifying a timer ID.
	 *
	 * @since   1.0
	 * @param   int $id          Timer ID, optional
	 * @param   int $precision   Precision of the result, optional
	 * @return  void|double      Elapsed time, or void if the timer was just started
	 */
	public static function timer( $id = 1, $precision = 4 ) {

		static
			$timers = array();

		// check if this timer was started, and display the elapsed time if so
		if ( isset( $timers[ $id ] ) ) {
			$elapsed = round( microtime( true ) - $timers[ $id ], $precision );
			unset( $timers[ $id ] );
			return $elapsed;
		}

		// ID doesn't exist, start new timer
		$timers[ $id ] = microtime( true );
	}



	/**
	 * Parses a DocBlock comment into a data structure.
	 *
	 * @link    http://pear.php.net/manual/en/standards.sample.php
	 * @param   string      $comment    DocBlock comment (must start with /**)
	 * @param   string|null $key   Field to return (optional)
	 * @return  array|string|null  Array containing all fields, array/string with the contents of
	 *                             the requested field, or null if the comment is empty/invalid
	 */
	public static function parseComment( $comment, $key = null ) {

		$description = '';
		$tags        = array();
		$tag         = null;
		$pointer     = '';
		$padding     = 0;
		$comment     = preg_split( '/\r\n|\r|\n/', '* ' . trim( $comment, "/* \t\n\r\0\x0B" ) );

		// analyze each line
		foreach ( $comment as $line ) {

			// drop any wrapping spaces
			$line = trim( $line );

			// drop "* "
			if ( $line !== '' ) {
				$line = substr( $line, 2 );
			}

			if ( strpos( $line, '@' ) !== 0 ) {

				// preserve formatting of tag descriptions,
				// because they may span across multiple lines
				if ( $tag !== null ) {
					$trimmed = trim( $line );

					if ( $padding !== 0 ) {
						$trimmed = static::strPad( $trimmed, static::strLen( $line ) - $padding, ' ', STR_PAD_LEFT );
					} else {
						$padding = static::strLen( $line ) - static::strLen( $trimmed );
					}

					$pointer .= "\n{$trimmed}";
					continue;
				}

				// tag definitions have not started yet; assume this is part of the description text
				$description .= "\n{$line}";
				continue;
			}

			$padding = 0;
			$parts = explode( ' ', $line, 2 );

			// invalid tag? (should we include it as an empty array?)
			if ( ! isset( $parts[1] ) ) {
				continue;
			}

			$tag = substr( $parts[0], 1 );
			$line = ltrim( $parts[1] );

			// tags that have a single component (eg. link, license, author, throws...);
			// note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...",
			// which, if broken into two values, leads to an inconsistent description sentence
			if ( ! in_array( $tag, array( 'global', 'param', 'return', 'var' ) ) ) {
				$tags[ $tag ][] = $line;
				end( $tags[ $tag ] );
				$pointer = &$tags[ $tag ][ key( $tags[ $tag ] ) ];
				continue;
			}

			// tags with 2 or 3 components (var, param, return);
			$parts    = explode( ' ', $line, 2 );
			$parts[1] = isset( $parts[1] ) ? ltrim( $parts[1] ) : null;
			$lastIdx  = 1;

			// expecting 3 components on the 'param' tag: type varName varDescription
			if ( $tag === 'param' ) {
				$lastIdx = 2;
				if ( in_array( $parts[1][0], array( '&', '$' ), true ) ) {
					$line     = ltrim( array_pop( $parts ) );
					$parts    = array_merge( $parts, explode( ' ', $line, 2 ) );
					$parts[2] = isset( $parts[2] ) ? ltrim( $parts[2] ) : null;
				} else {
					$parts[2] = $parts[1];
					$parts[1] = null;
				}
			}

			$tags[ $tag ][] = $parts;
			end( $tags[ $tag ] );
			$pointer = &$tags[ $tag ][ key( $tags[ $tag ] ) ][ $lastIdx ];
		}

		// split title from the description texts at the nearest 2x new-line combination
		// (note: loose check because 0 isn't valid as well)
		if ( strpos( $description, "\n\n" ) ) {
			list($title, $description) = explode( "\n\n", $description, 2 );

			// if we don't have 2 new lines, try to extract first sentence
		} else {
			// in order for a sentence to be considered valid,
			// the next one must start with an uppercase letter
			$sentences = preg_split( '/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY );

			// failed to detect a second sentence? then assume there's only title and no description text
			$title = isset( $sentences[0] ) ? $sentences[0] : $description;
			$description = isset( $sentences[1] ) ? $sentences[1] : '';
		}

		$title = ltrim( $title );
		$description = ltrim( $description );

		$data = compact( 'title', 'description', 'tags' );

		if ( ! array_filter( $data ) ) {
			return null;
		}

		if ( $key !== null ) {
			return isset( $data[ $key ] ) ? $data[ $key ] : null;
		}

		return $data;
	}



	/**
	 * Split a regex into its components
	 *
	 * Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript)
	 *
	 * @link     https://github.com/slevithan/regex-colorizer
	 * @link     https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74
	 * @param    string $pattern
	 * @return   array
	 */
	public static function splitRegex( $pattern ) {

		// detection attempt code from the Symfony Finder component
		$maybeValid = false;
		if ( preg_match( '/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m ) ) {
			$start = substr( $m[1], 0, 1 );
			$end   = substr( $m[1], -1 );

			if ( ($start === $end && ! preg_match( '/[*?[:alnum:] \\\\]/', $start )) || ($start === '{' && $end === '}') ) {
				$maybeValid = true;
			}
		}

		if ( ! $maybeValid ) {
			throw new \Exception( 'Pattern does not appear to be a valid PHP regex' );
		}

		$output              = array();
		$capturingGroupCount = 0;
		$groupStyleDepth     = 0;
		$openGroups          = array();
		$lastIsQuant         = false;
		$lastType            = 1;      // 1 = none; 2 = alternator
		$lastStyle           = null;

		preg_match_all( '/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches );

		$matches = $matches[0];

		$getTokenCharCode = function( $token ) {
			if ( strlen( $token ) > 1 && $token[0] === '\\' ) {
				$t1 = substr( $token, 1 );

				if ( preg_match( '/^c[A-Za-z]$/', $t1 ) ) {
					return strpos( 'ABCDEFGHIJKLMNOPQRSTUVWXYZ', strtoupper( $t1[1] ) ) + 1;
				}

				if ( preg_match( '/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1 ) ) {
					return intval( substr( $t1, 1 ), 16 );
				}

				if ( preg_match( '/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1 ) ) {
					return intval( $t1, 8 );
				}

				$len = strlen( $t1 );

				if ( $len === 1 && strpos( 'cuxDdSsWw', $t1 ) !== false ) {
					return null;
				}

				if ( $len === 1 ) {
					switch ( $t1 ) {
						case 'b':
							return 8;
						case 'f':
							return 12;
						case 'n':
							return 10;
						case 'r':
							return 13;
						case 't':
							return 9;
						case 'v':
							return 11;
						default:
							return $t1[0];
					}
				}
			}

			return ($token !== '\\') ? $token[0] : null;
		};

		foreach ( $matches as $m ) {

			if ( $m[0] === '[' ) {
				$lastCC         = null;
				$cLastRangeable = false;
				$cLastType      = 0;  // 0 = none; 1 = range hyphen; 2 = short class

				preg_match( '/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts );

				array_shift( $parts );
				list($opening, $content, $closing) = $parts;

				if ( ! $closing ) {
					throw new \Exception( 'Unclosed character class' );
				}

				preg_match_all( '/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens );
				$ccTokens     = $ccTokens[0];
				$ccTokenCount = count( $ccTokens );
				$output[]     = array(
					'chr' => $opening,
				);

				foreach ( $ccTokens as $i => $cm ) {

					if ( $cm[0] === '\\' ) {
						if ( preg_match( '/^\\\\[cux]$/', $cm ) ) {
							throw new \Exception( 'Incomplete regex token' );
						}

						if ( preg_match( '/^\\\\[dsw]$/i', $cm ) ) {
							$output[]     = array(
								'chr-meta' => $cm,
							);
							$cLastRangeable  = ($cLastType !== 1);
							$cLastType       = 2;

						} elseif ( $cm === '\\' ) {
							throw new \Exception( 'Incomplete regex token' );

						} else {
							$output[]       = array(
								'chr-meta' => $cm,
							);
							$cLastRangeable = $cLastType !== 1;
							$lastCC         = $getTokenCharCode( $cm );
						}
					} elseif ( $cm === '-' ) {
						if ( $cLastRangeable ) {
							$nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[ $i + 1 ] : false;

							if ( $nextToken ) {
								$nextTokenCharCode = $getTokenCharCode( $nextToken[0] );

								if ( ( ! is_null( $nextTokenCharCode ) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match( '/^\\\\[dsw]$/i', $nextToken[0] ) ) {
									throw new \Exception( 'Reversed or invalid range' );
								}

								$output[]       = array(
									'chr-range' => '-',
								);
								$cLastRangeable = false;
								$cLastType      = 1;

							} else {
								$output[] = $closing ? array(
									'chr' => '-',
								) : array(
									'chr-range' => '-',
								);
							}
						} else {
							$output[]        = array(
								'chr' => '-',
							);
							$cLastRangeable  = ($cLastType !== 1);
						}
					} else {
						$output[]       = array(
							'chr' => $cm,
						);
						$cLastRangeable = strlen( $cm ) > 1 || ($cLastType !== 1);
						$lastCC         = $cm[ strlen( $cm ) - 1 ];
					}
				}

				$output[] = array(
					'chr' => $closing,
				);
				$lastIsQuant  = true;

			} elseif ( $m[0] === '(' ) {
				if ( strlen( $m ) === 2 ) {
					throw new \Exception( 'Invalid or unsupported group type' );
				}

				if ( strlen( $m ) === 1 ) {
					$capturingGroupCount++;
				}

				$groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1;
				$openGroups[]    = $m; // opening
				$lastIsQuant     = false;
				$output[]        = array(
					"g{$groupStyleDepth}" => $m,
				);

			} elseif ( $m[0] === ')' ) {
				if ( ! count( $openGroups ) ) {
					throw new \Exception( 'No matching opening parenthesis' );
				}

				$output[]        = array(
					'g' . $groupStyleDepth => ')',
				);
				$prevGroup       = $openGroups[ count( $openGroups ) - 1 ];
				$prevGroup       = isset( $prevGroup[2] ) ? $prevGroup[2] : '';
				$lastIsQuant     = ! preg_match( '/^[=!]/', $prevGroup );
				$lastStyle       = "g{$groupStyleDepth}";
				$lastType        = 0;
				$groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5;

				array_pop( $openGroups );
				continue;

			} elseif ( $m[0] === '\\' ) {
				if ( isset( $m[1] ) && preg_match( '/^[1-9]/', $m[1] ) ) {
					$nonBackrefDigits = '';
					$num = substr( +$m, 1 );

					while ( $num > $capturingGroupCount ) {
						preg_match( '/[0-9]$/', $num, $digits );
						$nonBackrefDigits = $digits[0] . $nonBackrefDigits;
						$num = floor( $num / 10 );
					}

					if ( $num > 0 ) {
						$output[] = array(
							'meta' => "\\{$num}",
							'text' => $nonBackrefDigits,
						);

					} else {
						preg_match( '/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts );
						$output[] = array(
							'meta' => '\\' . $pts[1],
							'text' => $pts[2],
						);
					}

					$lastIsQuant = true;

				} elseif ( isset( $m[1] ) && preg_match( '/^[0bBcdDfnrsStuvwWx]/', $m[1] ) ) {

					if ( preg_match( '/^\\\\[cux]$/', $m ) ) {
						throw new \Exception( 'Incomplete regex token' );
					}

					$output[]    = array(
						'meta' => $m,
					);
					$lastIsQuant = (strpos( 'bB', $m[1] ) === false);

				} elseif ( $m === '\\' ) {
					throw new \Exception( 'Incomplete regex token' );

				} else {
					$output[]    = array(
						'text' => $m,
					);
					$lastIsQuant = true;
				}
			} elseif ( preg_match( '/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m ) ) {
				if ( ! $lastIsQuant ) {
					throw new \Exception( 'Quantifiers must be preceded by a token that can be repeated' );
				}

				preg_match( '/^\{([0-9]+)(?:,([0-9]*))?/', $m, $interval );

				if ( $interval && (+$interval[1] > 65535 || (isset( $interval[2] ) && (+$interval[2] > 65535))) ) {
					throw new \Exception( 'Interval quantifier cannot use value over 65,535' );
				}

				if ( $interval && isset( $interval[2] ) && (+$interval[1] > +$interval[2]) ) {
					throw new \Exception( 'Interval quantifier range is reversed' );
				}

				$output[]     = array(
					$lastStyle ? $lastStyle : 'meta' => $m,
				);
				$lastIsQuant  = false;

			} elseif ( $m === '|' ) {
				if ( $lastType === 1 || ($lastType === 2 && ! count( $openGroups )) ) {
					throw new \Exception( 'Empty alternative effectively truncates the regex here' );
				}

				$output[]    = count( $openGroups ) ? array(
					"g{$groupStyleDepth}" => '|',
				) : array(
					'meta' => '|',
				);
				$lastIsQuant = false;
				$lastType    = 2;
				$lastStyle   = '';
				continue;

			} elseif ( $m === '^' || $m === '$' ) {
				$output[]    = array(
					'meta' => $m,
				);
				$lastIsQuant = false;

			} elseif ( $m === '.' ) {
				$output[]    = array(
					'meta' => '.',
				);
				$lastIsQuant = true;

			} else {
				$output[]    = array(
					'text' => $m,
				);
				$lastIsQuant = true;
			}

			$lastType  = 0;
			$lastStyle = '';
		}

		if ( $openGroups ) {
			throw new \Exception( 'Unclosed grouping' );
		}

		return $output;
	}



	/**
	 * Set or get configuration options
	 *
	 * @param   string     $key
	 * @param   mixed|null $value
	 * @return  mixed
	 */
	public static function config( $key, $value = null ) {

		if ( ! array_key_exists( $key, static::$config ) ) {
			throw new \Exception( sprintf( 'Unrecognized option: "%s". Valid options are: %s', $key, implode( ', ', array_keys( static::$config ) ) ) );
		}

		if ( $value === null ) {
			return static::$config[ $key ];
		}

		if ( is_array( static::$config[ $key ] ) ) {
			return static::$config[ $key ] = (array) $value;
		}

		return static::$config[ $key ] = $value;
	}



	/**
	 * Total CPU time used by the class
	 *
	 * @param   int precision
	 * @return  double
	 */
	public static function getTime( $precision = 4 ) {
		return round( static::$time, $precision );
	}



	/**
	 * Get relevant backtrace info for last ref call
	 *
	 * @return  array|false
	 */
	public static function getBacktrace() {

		// pull only basic info with php 5.3.6+ to save some memory
		$trace = defined( 'DEBUG_BACKTRACE_IGNORE_ARGS' ) ? debug_backtrace( DEBUG_BACKTRACE_IGNORE_ARGS ) : debug_backtrace();

		while ( $callee = array_pop( $trace ) ) {

			// extract only the information we neeed
			$callee = array_intersect_key( $callee, array_fill_keys( array( 'file', 'function', 'line' ), false ) );
			extract( $callee, EXTR_OVERWRITE );

			// skip, if the called function doesn't match the shortcut function name
			if ( ! $function || ! in_array( mb_strtolower( (string) $function ), static::$config['shortcutFunc'] ) ) {
				continue;
			}

			return compact( 'file', 'function', 'line' );
		}

		return false;
	}



	/**
	 * Determines the input expression(s) passed to the shortcut function
	 *
	 * @param   array &$options   Optional, options to gather (from operators)
	 * @return  array             Array of string expressions
	 */
	public static function getInputExpressions( array &$options = null ) {

		// used to determine the position of the current call,
		// if more queries calls were made on the same line
		static $lineInst = array();

		$trace = static::getBacktrace();

		if ( ! $trace ) {
			return array();
		}

		extract( $trace );

		$code     = file( $file );
		$code     = $code[ $line - 1 ]; // multiline expressions not supported!
		$instIndx = 0;
		$tokens   = token_get_all( "<?php {$code}" );

		// locate the caller position in the line, and isolate argument tokens
		foreach ( $tokens as $i => $token ) {

			// match token with our shortcut function name
			if ( is_string( $token ) || ($token[0] !== T_STRING) || (strcasecmp( $token[1], $function ) !== 0) ) {
				continue;
			}

			// is this some method that happens to have the same name as the shortcut function?
			if ( isset( $tokens[ $i - 1 ] ) && is_array( $tokens[ $i - 1 ] ) && in_array( $tokens[ $i - 1 ][0], array( T_DOUBLE_COLON, T_OBJECT_OPERATOR ), true ) ) {
				continue;
			}

			// find argument definition start, just after '('
			if ( isset( $tokens[ $i + 1 ] ) && ($tokens[ $i + 1 ][0] === '(') ) {
				$instIndx++;

				if ( ! isset( $lineInst[ $line ] ) ) {
					$lineInst[ $line ] = 0;
				}

				if ( $instIndx <= $lineInst[ $line ] ) {
					continue;
				}

				$lineInst[ $line ]++;

				// gather options
				if ( $options !== null ) {
					$j = $i - 1;
					while ( isset( $tokens[ $j ] ) && is_string( $tokens[ $j ] ) && in_array( $tokens[ $j ], array( '@', '+', '-', '!', '~' ) ) ) {
						$options[] = $tokens[ $j-- ];
					}
				}

				$lvl = $index = $curlies = 0;
				$expressions = array();

				// get the expressions
				foreach ( array_slice( $tokens, $i + 2 ) as $token ) {

					if ( is_array( $token ) ) {
						if ( $token[0] !== T_COMMENT ) {
							$expressions[ $index ][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' ';
						}

						continue;
					}

					if ( $token === '{' ) {
						$curlies++;
					}

					if ( $token === '}' ) {
						$curlies--;
					}

					if ( $token === '(' ) {
						$lvl++;
					}

					if ( $token === ')' ) {
						$lvl--;
					}

					// assume next argument if a comma was encountered,
					// and we're not insde a curly bracket or inner parentheses
					if ( ($curlies < 1) && ($lvl === 0) && ($token === ',') ) {
						$index++;
						continue;
					}

					// negative parentheses count means we reached the end of argument definitions
					if ( $lvl < 0 ) {
						foreach ( $expressions as &$expression ) {
							$expression = trim( implode( '', $expression ) );
						}

						return $expressions;
					}

					$expressions[ $index ][] = $token;
				}

				break;
			}
		}

	}



	/**
	 * Get all parent classes of a class
	 *
	 * @param   Reflector $class   Reflection object
	 * @return  array              Array of ReflectionClass objects (starts with the ancestor, ends with the given class)
	 */
	protected static function getParentClasses( \Reflector $class ) {

		$parents = array( $class );
		while ( ($class = $class->getParentClass()) !== false ) {
			$parents[] = $class;
		}

		return array_reverse( $parents );
	}



	/**
	 * Generate class / function info
	 *
	 * @param   Reflector      $reflector      Class name or reflection object
	 * @param   string         $single            Skip parent classes
	 * @param   Reflector|null $context   Object context (for methods)
	 * @return  string
	 */
	protected function fromReflector( \Reflector $reflector, $single = '', \Reflector $context = null ) {

		// @todo: test this
		$hash = var_export( func_get_args(), true );
		// $hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : '');
		if ( $this->fmt->didCache( $hash ) ) {
			static::$debug['cacheHits']++;
			return;
		}

		$items = array( $reflector );

		if ( ($single === '') && ($reflector instanceof \ReflectionClass) ) {
			$items = static::getParentClasses( $reflector );
		}

		$first = true;
		foreach ( $items as $item ) {

			if ( ! $first ) {
				$this->fmt->sep( ' :: ' );
			}

			$first    = false;
			$name     = ($single !== '') ? $single : $item->getName();
			$comments = $item->isInternal() ? array() : static::parseComment( $item->getDocComment() );
			$meta     = array(
				'sub' => array(),
			);
			$bubbles  = array();

			if ( $item->isInternal() ) {
				$extension = $item->getExtension();
				$meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf( 'Internal - part of %s (%s)', $extension->getName(), $extension->getVersion() ) : 'Internal';

			} else {
				$comments = static::parseComment( $item->getDocComment() );

				if ( $comments ) {
					$meta += $comments;
				}

				$meta['sub'][] = array( 'Defined in', basename( $item->getFileName() ) . ':' . $item->getStartLine() );
			}

			if ( ($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod) ) {
				if ( ($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName()) ) {
					$meta['sub'][] = array( 'Inherited from', $item->getDeclaringClass()->getShortName() );
				}

				// @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke()
				if ( ($item instanceof \ReflectionMethod) && ! $item->isInternal() ) {
					try {
						$proto = $item->getPrototype();
						$meta['sub'][] = array( 'Prototype defined by', $proto->class );
					} catch ( \Exception $e ) {
					}
				}

				$this->fmt->text( 'name', $name, $meta, $this->linkify( $item ) );
				continue;
			}

			// @todo: maybe - list interface methods
			if ( ! ($item->isInterface() || (static::$env['is54'] && $item->isTrait())) ) {

				if ( $item->isAbstract() ) {
					$bubbles[] = array( 'A', 'Abstract' );
				}

				if ( static::$env['is7'] && $item->isAnonymous() ) {
					$bubbles[] = array( '?', 'Anonymous' );
				}

				if ( $item->isFinal() ) {
					$bubbles[] = array( 'F', 'Final' );
				}

				// php 5.4+ only
				if ( static::$env['is54'] && $item->isCloneable() ) {
					$bubbles[] = array( 'C', 'Cloneable' );
				}

				if ( $item->isIterateable() ) {
					$bubbles[] = array( 'X', 'Iterateable' );
				}
			}

			if ( $item->isInterface() && $single !== '' ) {
				$bubbles[] = array( 'I', 'Interface' );
			}

			if ( $bubbles ) {
				$this->fmt->bubbles( $bubbles );
			}

			if ( $item->isInterface() && $single === '' ) {
				$name .= sprintf( ' (%d)', count( $item->getMethods() ) );
			}

			$this->fmt->text( 'name', $name, $meta, $this->linkify( $item ) );
		}

		$this->fmt->cacheLock( $hash );
	}



	/**
	 * Generates an URL that points to the documentation page relevant for the requested context
	 *
	 * For internal functions and classes, the URI will point to the local PHP manual
	 * if installed and configured, otherwise to php.net/manual (the english one)
	 *
	 * @param   Reflector   $reflector    Reflector object (used to determine the URL scheme for internal stuff)
	 * @param   string|null $constant   Constant name, if this is a request to linkify a constant
	 * @return  string|null             URL
	 */
	protected function linkify( \Reflector $reflector, $constant = null ) {

		static $docRefRoot = null, $docRefExt = null;

		// most people don't have this set
		if ( ! $docRefRoot ) {
			$docRefRoot = ($docRefRoot = rtrim( ini_get( 'docref_root' ), '/' )) ? $docRefRoot : 'http://php.net/manual/en';
		}

		if ( ! $docRefExt ) {
			$docRefExt = ($docRefExt = ini_get( 'docref_ext' )) ? $docRefExt : '.php';
		}

		$phpNetSchemes = array(
			'class'     => $docRefRoot . '/class.%s' . $docRefExt,
			'function'  => $docRefRoot . '/function.%s' . $docRefExt,
			'method'    => $docRefRoot . '/%2$s.%1$s' . $docRefExt,
			'property'  => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.props.%1$s',
			'constant'  => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.constants.%1$s',
		);

		$url  = null;
		$args = array();

		// determine scheme
		if ( $constant !== null ) {
			$type = 'constant';
			$args[] = $constant;

		} else {
			$type = explode( '\\', get_class( $reflector ) );
			$type = strtolower( ltrim( end( $type ), 'Reflection' ) );

			if ( $type === 'object' ) {
				$type = 'class';
			}
		}

		// properties don't have the internal flag;
		// also note that many internal classes use some kind of magic as properties (eg. DateTime);
		// these will only get linkifed if the declared class is internal one, and not an extension :(
		$parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass();

		// internal function/method/class/property/constant
		if ( $parent->isInternal() ) {
			$args[] = $reflector->name;

			if ( in_array( $type, array( 'method', 'property' ), true ) ) {
				$args[] = $reflector->getDeclaringClass()->getName();
			}

			$args = array_map(
				function( $text ) {
					return str_replace( '_', '-', ltrim( strtolower( $text ), '\\_' ) );
				}, $args
			);

			// check for some special cases that have no links
			$valid = (($type === 'method') || (strcasecmp( $parent->name, 'stdClass' ) !== 0))
						&& (($type !== 'method') || (($reflector->name === '__construct') || strpos( $reflector->name, '__' ) !== 0));

			if ( $valid ) {
				$url = vsprintf( $phpNetSchemes[ $type ], $args );
			}

			// custom
		} else {
			switch ( true ) {

				// WordPress function;
				// like pretty much everything else in WordPress, API links are inconsistent as well;
				// so we're using queryposts.com as doc source for API
				case ($type === 'function') && class_exists( 'WP', false ) && defined( 'ABSPATH' ) && defined( 'WPINC' ):
					if ( strpos( $reflector->getFileName(), realpath( ABSPATH . WPINC ) ) === 0 ) {
						$url = sprintf( 'http://queryposts.com/function/%s', urlencode( strtolower( $reflector->getName() ) ) );
						break;
					}

					// @todo: handle more apps
			}
		}

		return $url;
	}


	public static function getTimeoutPoint() {
		return static::$timeout;
	}


	public static function getDebugInfo() {
		return static::$debug;
	}



	protected function hasInstanceTimedOut() {

		if ( static::$timeout > 0 ) {
			return true;
		}

		$timeout = static::$config['timeout'];

		if ( ($timeout > 0) && ((microtime( true ) - $this->startTime) > $timeout) ) {
			return (static::$timeout = (microtime( true ) - $this->startTime));
		}

		return false;
	}



	/**
	 * Evaluates the given variable
	 *
	 * @param   mixed &$subject   Variable to query
	 * @param   bool  $specialStr  Should this be interpreted as a special string?
	 * @return  mixed             Result (both HTML and text modes generate strings)
	 */
	protected function evaluate( &$subject, $specialStr = false ) {

		switch ( $type = gettype( $subject ) ) {

			// https://github.com/digitalnature/php-ref/issues/13
			case 'unknown type':
				return $this->fmt->text( 'unknown' );

			// null value
			case 'NULL':
				return $this->fmt->text( 'null' );

			// integer/double/float
			case 'integer':
			case 'double':
				return $this->fmt->text( $type, $subject, $type );

			// boolean
			case 'boolean':
				$text = $subject ? 'true' : 'false';
				return $this->fmt->text( $text, $text, $type );

			// arrays
			case 'array':
				// empty array?
				if ( empty( $subject ) ) {
					$this->fmt->text( 'array' );
					return $this->fmt->emptyGroup();
				}

				if ( isset( $subject[ static::MARKER_KEY ] ) ) {
					unset( $subject[ static::MARKER_KEY ] );
					$this->fmt->text( 'array' );
					$this->fmt->emptyGroup( 'recursion' );
					return;
				}

				// first recursion level detection;
				// this is optional (used to print consistent recursion info)
				foreach ( $subject as $key => &$value ) {

					if ( ! is_array( $value ) ) {
						continue;
					}

					// save current value in a temporary variable
					$buffer = $value;

					// assign new value
					$value = ($value !== 1) ? 1 : 2;

					// if they're still equal, then we have a reference
					if ( $value === $subject ) {
						$value = $buffer;
						$value[ static::MARKER_KEY ] = true;
						$this->evaluate( $value );
						return;
					}

					// restoring original value
					$value = $buffer;
				}

				$this->fmt->text( 'array' );
				$count = count( $subject );
				if ( ! $this->fmt->startGroup( $count ) ) {
					return;
				}

				$max = max( array_map( 'static::strLen', array_keys( $subject ) ) );
				$subject[ static::MARKER_KEY ] = true;

				foreach ( $subject as $key => &$value ) {

					// ignore our temporary marker
					if ( $key === static::MARKER_KEY ) {
						continue;
					}

					if ( $this->hasInstanceTimedOut() ) {
						break;
					}

					$keyInfo = gettype( $key );

					if ( $keyInfo === 'string' ) {
						$encoding = static::$env['mbStr'] ? mb_detect_encoding( $key ) : '';
						$keyLen   = $encoding && ($encoding !== 'ASCII') ? static::strLen( $key ) . '; ' . $encoding : static::strLen( $key );
						$keyInfo  = "{$keyInfo}({$keyLen})";
					} else {
						$keyLen   = strlen( $key );
					}

					$this->fmt->startRow();
					$this->fmt->text( 'key', $key, "Key: {$keyInfo}" );
					$this->fmt->colDiv( $max - $keyLen );
					$this->fmt->sep( '=>' );
					$this->fmt->colDiv();
					$this->evaluate( $value, $specialStr );
					$this->fmt->endRow();
				}

				unset( $subject[ static::MARKER_KEY ] );

				$this->fmt->endGroup();
				return;

			// resource
			case 'resource':
				$meta    = array();
				$resType = get_resource_type( $subject );

				$this->fmt->text( 'resource', strval( $subject ) );

				if ( ! static::$config['showResourceInfo'] ) {
					return $this->fmt->emptyGroup( $resType );
				}

				// @see: http://php.net/manual/en/resource.php
				// need to add more...
				switch ( $resType ) {

					// curl extension resource
					case 'curl':
						$meta = curl_getinfo( $subject );
						break;

					case 'FTP Buffer':
						$meta = array(
							'time_out'  => ftp_get_option( $subject, FTP_TIMEOUT_SEC ),
							'auto_seek' => ftp_get_option( $subject, FTP_AUTOSEEK ),
						);

						break;

					// gd image extension resource
					case 'gd':
						$meta = array(
							'size'       => sprintf( '%d x %d', imagesx( $subject ), imagesy( $subject ) ),
							'true_color' => imageistruecolor( $subject ),
						);

						break;

					case 'ldap link':
						$constants = get_defined_constants();

						array_walk(
							$constants, function( $value, $key ) use ( &$constants ) {
								if ( strpos( $key, 'LDAP_OPT_' ) !== 0 ) {
									unset( $constants[ $key ] );
								}
							}
						);

						// this seems to fail on my setup :(
						unset( $constants['LDAP_OPT_NETWORK_TIMEOUT'] );

						foreach ( array_slice( $constants, 3 ) as $key => $value ) {
							if ( ldap_get_option( $subject, (int) $value, $ret ) ) {
								$meta[ strtolower( substr( $key, 9 ) ) ] = $ret;
							}
						}

						break;

					// mysql connection (mysql extension is deprecated from php 5.4/5.5)
					case 'mysql link':
					case 'mysql link persistent':
						$dbs = array();
						$query = @mysql_list_dbs( $subject );
						while ( $row = @mysql_fetch_array( $query ) ) {
							$dbs[] = $row['Database'];
						}

						$meta = array(
							'host'             => ltrim( @mysql_get_host_info( $subject ), 'MySQL host info: ' ),
							'server_version'   => @mysql_get_server_info( $subject ),
							'protocol_version' => @mysql_get_proto_info( $subject ),
							'databases'        => $dbs,
						);

						break;

					// mysql result
					case 'mysql result':
						while ( $row = @mysql_fetch_object( $subject ) ) {
							$meta[] = (array) $row;

							if ( $this->hasInstanceTimedOut() ) {
								break;
							}
						}

						break;

					// stream resource (fopen, fsockopen, popen, opendir etc)
					case 'stream':
						$meta = stream_get_meta_data( $subject );
						break;

				}

				if ( ! $meta ) {
					return $this->fmt->emptyGroup( $resType );
				}

				if ( ! $this->fmt->startGroup( $resType ) ) {
					return;
				}

				$max = max( array_map( 'static::strLen', array_keys( $meta ) ) );
				foreach ( $meta as $key => $value ) {
					$this->fmt->startRow();
					$this->fmt->text( 'resourceProp', ucwords( str_replace( '_', ' ', $key ) ) );
					$this->fmt->colDiv( $max - static::strLen( $key ) );
					$this->fmt->sep( ':' );
					$this->fmt->colDiv();
					$this->evaluate( $value );
					$this->fmt->endRow();
				}
				$this->fmt->endGroup();
				return;

			// string
			case 'string':
				$length   = static::strLen( $subject );
				$encoding = static::$env['mbStr'] ? mb_detect_encoding( $subject ) : false;
				$info     = $encoding && ($encoding !== 'ASCII') ? $length . '; ' . $encoding : $length;

				if ( $specialStr ) {
					$this->fmt->sep( '"' );
					$this->fmt->text( array( 'string', 'special' ), $subject, "string({$info})" );
					$this->fmt->sep( '"' );
					return;
				}

				$this->fmt->text( 'string', $subject, "string({$info})" );

				// advanced checks only if there are 3 characteres or more
				if ( static::$config['showStringMatches'] && ($length > 2) && (trim( $subject ) !== '') ) {

					$isNumeric = is_numeric( $subject );

					// very simple check to determine if the string could match a file path
					// @note: this part of the code is very expensive
					$isFile = ($length < 2048)
						&& (max( array_map( 'strlen', explode( '/', str_replace( '\\', '/', $subject ) ) ) ) < 128)
						&& ! preg_match( '/[^\w\.\-\/\\\\:]|\..*\.|\.$|:(?!(?<=^[a-zA-Z]:)[\/\\\\])/', $subject );

					if ( $isFile ) {
						try {
							$file  = new \SplFileInfo( $subject );
							$flags = array();
							$perms = $file->getPerms();

							if ( ($perms & 0xC000) === 0xC000 ) {       // socket
								$flags[] = 's';
							} elseif ( ($perms & 0xA000) === 0xA000 ) {   // symlink
								$flags[] = 'l';
							} elseif ( ($perms & 0x8000) === 0x8000 ) {   // regular
								$flags[] = '-';
							} elseif ( ($perms & 0x6000) === 0x6000 ) {   // block special
								$flags[] = 'b';
							} elseif ( ($perms & 0x4000) === 0x4000 ) {   // directory
								$flags[] = 'd';
							} elseif ( ($perms & 0x2000) === 0x2000 ) {   // character special
								$flags[] = 'c';
							} elseif ( ($perms & 0x1000) === 0x1000 ) {   // FIFO pipe
								$flags[] = 'p';
							} else { // unknown
								$flags[] = 'u';
							}

							// owner
							$flags[] = (($perms & 0x0100) ? 'r' : '-');
							$flags[] = (($perms & 0x0080) ? 'w' : '-');
							$flags[] = (($perms & 0x0040) ? (($perms & 0x0800) ? 's' : 'x' ) : (($perms & 0x0800) ? 'S' : '-'));

							// group
							$flags[] = (($perms & 0x0020) ? 'r' : '-');
							$flags[] = (($perms & 0x0010) ? 'w' : '-');
							$flags[] = (($perms & 0x0008) ? (($perms & 0x0400) ? 's' : 'x' ) : (($perms & 0x0400) ? 'S' : '-'));

							// world
							$flags[] = (($perms & 0x0004) ? 'r' : '-');
							$flags[] = (($perms & 0x0002) ? 'w' : '-');
							$flags[] = (($perms & 0x0001) ? (($perms & 0x0200) ? 't' : 'x' ) : (($perms & 0x0200) ? 'T' : '-'));

							$size = is_dir( $subject ) ? '' : sprintf( ' %.2fK', $file->getSize() / 1024 );

							$this->fmt->startContain( 'file', true );
							$this->fmt->text( 'file', implode( '', $flags ) . $size );
							$this->fmt->endContain();

						} catch ( \Exception $e ) {
							$isFile = false;
						}
					}

					// class/interface/function
					if ( ! preg_match( '/[^\w+\\\\]/', $subject ) && ($length < 96) ) {
						$isClass = class_exists( $subject, false );
						if ( $isClass ) {
							$this->fmt->startContain( 'class', true );
							$this->fromReflector( new \ReflectionClass( $subject ) );
							$this->fmt->endContain();
						}

						if ( ! $isClass && interface_exists( $subject, false ) ) {
							$this->fmt->startContain( 'interface', true );
							$this->fromReflector( new \ReflectionClass( $subject ) );
							$this->fmt->endContain( 'interface' );
						}

						if ( function_exists( $subject ) ) {
							$this->fmt->startContain( 'function', true );
							$this->fromReflector( new \ReflectionFunction( $subject ) );
							$this->fmt->endContain( 'function' );
						}
					}

					// skip serialization/json/date checks if the string appears to be numeric,
					// or if it's shorter than 5 characters
					if ( ! $isNumeric && ($length > 4) ) {

						// url
						if ( static::$config['showUrls'] && static::$env['curlActive'] && filter_var( $subject, FILTER_VALIDATE_URL ) ) {
							$ch = curl_init( $subject );
							curl_setopt( $ch, CURLOPT_NOBODY, true );
							curl_exec( $ch );
							$nfo = curl_getinfo( $ch );
							curl_close( $ch );

							if ( $nfo['http_code'] ) {
								$this->fmt->startContain( 'url', true );
								$contentType = explode( ';', $nfo['content_type'] );
								$this->fmt->text( 'url', sprintf( '%s:%d %s %.2fms (%d)', ! empty( $nfo['primary_ip'] ) ? $nfo['primary_ip'] : null, ! empty( $nfo['primary_port'] ) ? $nfo['primary_port'] : null, $contentType[0], $nfo['total_time'], $nfo['http_code'] ) );
								$this->fmt->endContain();
							}
						}

						// date
						if ( ($length < 128) && static::$env['supportsDate'] && ! preg_match( '/[^A-Za-z0-9.:+\s\-\/]/', $subject ) ) {
							try {
								$date   = new \DateTime( $subject );
								$errors = \DateTime::getLastErrors();

								if ( ($errors['warning_count'] < 1) && ($errors['error_count'] < 1) ) {
									$now    = new \Datetime( 'now' );
									$nowUtc = new \Datetime( 'now', new \DateTimeZone( 'UTC' ) );
									$diff   = $now->diff( $date );

									$map = array(
										'y' => 'yr',
										'm' => 'mo',
										'd' => 'da',
										'h' => 'hr',
										'i' => 'min',
										's' => 'sec',
									);

									$timeAgo = 'now';
									foreach ( $map as $k => $label ) {
										if ( $diff->{$k} > 0 ) {
											$timeAgo = $diff->format( "%R%{$k}{$label}" );
											break;
										}
									}

									$tz   = $date->getTimezone();
									$offs = round( $tz->getOffset( $nowUtc ) / 3600 );

									if ( $offs > 0 ) {
										$offs = "+{$offs}";
									}

									$timeAgo .= ( (int) $offs !== 0) ? ' ' . sprintf( '%s (UTC%s)', $tz->getName(), $offs ) : ' UTC';
									$this->fmt->startContain( 'date', true );
									$this->fmt->text( 'date', $timeAgo );
									$this->fmt->endContain();

								}
							} catch ( \Exception $e ) {
								// not a date
							}
						}

						// attempt to detect if this is a serialized string
						static $unserializing = 0;
						$isSerialized = ($unserializing < 3)
							&& (($subject[ $length - 1 ] === ';') || ($subject[ $length - 1 ] === '}'))
							&& in_array( $subject[0], array( 's', 'a', 'O' ), true )
							&& ((($subject[0] === 's') && ($subject[ $length - 2 ] !== '"')) || preg_match( "/^{$subject[0]}:[0-9]+:/s", $subject ))
							&& (($unserialized = @unserialize( $subject )) !== false);

						if ( $isSerialized ) {
							$unserializing++;
							$this->fmt->startContain( 'serialized', true );
							$this->evaluate( $unserialized );
							$this->fmt->endContain();
							$unserializing--;
						}

						// try to find out if it's a json-encoded string;
						// only do this for json-encoded arrays or objects, because other types have too generic formats
						static $decodingJson = 0;
						$isJson = ! $isSerialized && ($decodingJson < 3) && in_array( $subject[0], array( '{', '[' ), true );

						if ( $isJson ) {
							$decodingJson++;
							$data = json_decode( $subject );

							// ensure created objects live enough for PHP to provide a unique hash
							if ( is_object( $data ) ) {
								$this->intObjects->attach( $data );
							}

							if ( $isJson = (json_last_error() === JSON_ERROR_NONE) ) {
								$this->fmt->startContain( 'json', true );
								$this->evaluate( $data );
								$this->fmt->endContain();
							}

							$decodingJson--;
						}

						// attempt to match a regex
						if ( ! $isSerialized && ! $isJson && $length < 768 ) {
							try {
								$components = $this->splitRegex( $subject );
								if ( $components ) {
									$regex = '';

									$this->fmt->startContain( 'regex', true );
									foreach ( $components as $component ) {
										$this->fmt->text( 'regex-' . key( $component ), reset( $component ) );
									}
									$this->fmt->endContain();
								}
							} catch ( \Exception $e ) {
								// not a regex
							}
						}
					}
				}

				return;
		}

		// if we reached this point, $subject must be an object
		// track objects to detect recursion
		static $hashes = array();

		// hash ID of this object
		$hash = spl_object_hash( $subject );
		$recursion = isset( $hashes[ $hash ] );

		// sometimes incomplete objects may be created from string unserialization,
		// if the class to which the object belongs wasn't included until the unserialization stage...
		if ( $subject instanceof \__PHP_Incomplete_Class ) {
			$this->fmt->text( 'object' );
			$this->fmt->emptyGroup( 'incomplete' );
			return;
		}

		// check cache at this point
		if ( ! $recursion && $this->fmt->didCache( $hash ) ) {
			static::$debug['cacheHits']++;
			return;
		}

		$reflector = new \ReflectionObject( $subject );
		$this->fmt->startContain( 'class' );
		$this->fromReflector( $reflector );
		$this->fmt->text( 'object', ' object' );
		$this->fmt->endContain();

		// already been here?
		if ( $recursion ) {
			return $this->fmt->emptyGroup( 'recursion' );
		}

		$hashes[ $hash ] = 1;

		$flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED;

		if ( static::$config['showPrivateMembers'] ) {
			$flags |= \ReflectionProperty::IS_PRIVATE;
		}

		$props = $magicProps = $methods = array();

		if ( $reflector->hasMethod( '__debugInfo' ) ) {
			$magicProps = $subject->__debugInfo();
		} else {
			$props = $reflector->getProperties( $flags );
		}

		if ( static::$config['showMethods'] ) {
			$flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED;

			if ( static::$config['showPrivateMembers'] ) {
				$flags |= \ReflectionMethod::IS_PRIVATE;
			}

			$methods = $reflector->getMethods( $flags );
		}

		$constants  = $reflector->getConstants();
		$interfaces = $reflector->getInterfaces();
		$traits     = static::$env['is54'] ? $reflector->getTraits() : array();
		$parents    = static::getParentClasses( $reflector );

		// work-around for https://bugs.php.net/bug.php?id=49154
		// @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys
		if ( ! static::$env['is54'] ) {
			$props = array_values(
				array_filter(
					$props, function( $prop ) use ( $subject ) {
						return ! $prop->isPublic() || property_exists( $subject, $prop->name );
					}
				)
			);
		}

		// no data to display?
		if ( ! $props && ! $methods && ! $constants && ! $interfaces && ! $traits ) {
			unset( $hashes[ $hash ] );
			return $this->fmt->emptyGroup();
		}

		if ( ! $this->fmt->startGroup() ) {
			return;
		}

		// show contents for iterators
		if ( static::$config['showIteratorContents'] && $reflector->isIterateable() ) {

			$itContents = iterator_to_array( $subject );
			$this->fmt->sectionTitle( sprintf( 'Contents (%d)', count( $itContents ) ) );

			foreach ( $itContents as $key => $value ) {
				$keyInfo = gettype( $key );
				if ( $keyInfo === 'string' ) {
					$encoding = static::$env['mbStr'] ? mb_detect_encoding( $key ) : '';
					$length   = $encoding && ($encoding !== 'ASCII') ? static::strLen( $key ) . '; ' . $encoding : static::strLen( $key );
					$keyInfo  = sprintf( '%s(%s)', $keyInfo, $length );
				}

				$this->fmt->startRow();
				$this->fmt->text( array( 'key', 'iterator' ), $key, sprintf( 'Iterator key: %s', $keyInfo ) );
				$this->fmt->colDiv();
				$this->fmt->sep( '=>' );
				$this->fmt->colDiv();
				$this->evaluate( $value );
				// $this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value);
				$this->fmt->endRow();
			}
		}

		// display the interfaces this objects' class implements
		if ( $interfaces ) {
			$items = array();
			$this->fmt->sectionTitle( 'Implements' );
			$this->fmt->startRow();
			$this->fmt->startContain( 'interfaces' );

			$i     = 0;
			$count = count( $interfaces );

			foreach ( $interfaces as $name => $interface ) {
				$this->fromReflector( $interface );

				if ( ++$i < $count ) {
					$this->fmt->sep( ', ' );
				}
			}

			$this->fmt->endContain();
			$this->fmt->endRow();
		}

		// traits this objects' class uses
		if ( $traits ) {
			$items = array();
			$this->fmt->sectionTitle( 'Uses' );
			$this->fmt->startRow();
			$this->fmt->startContain( 'traits' );

			$i     = 0;
			$count = count( $traits );

			foreach ( $traits as $name => $trait ) {
				$this->fromReflector( $trait );

				if ( ++$i < $count ) {
					$this->fmt->sep( ', ' );
				}
			}

			$this->fmt->endContain();
			$this->fmt->endRow();
		}

		// class constants
		if ( $constants ) {
			$this->fmt->sectionTitle( 'Constants' );
			$max = max( array_map( 'static::strLen', array_keys( $constants ) ) );
			foreach ( $constants as $name => $value ) {
				$meta = null;
				$type = array( 'const' );
				foreach ( $parents as $parent ) {
					if ( $parent->hasConstant( $name ) ) {
						if ( $parent !== $reflector ) {
							$type[] = 'inherited';
							$meta = array(
								'sub' => array( array( 'Prototype defined by', $parent->name ) ),
							);
						}
						break;
					}
				}

				$this->fmt->startRow();
				$this->fmt->sep( '::' );
				$this->fmt->colDiv();
				$this->fmt->startContain( $type );
				$this->fmt->text( 'name', $name, $meta, $this->linkify( $parent, $name ) );
				$this->fmt->endContain();
				$this->fmt->colDiv( $max - static::strLen( $name ) );
				$this->fmt->sep( '=' );
				$this->fmt->colDiv();
				$this->evaluate( $value );
				$this->fmt->endRow();
			}
		}

		// object/class properties
		if ( $props ) {
			$this->fmt->sectionTitle( 'Properties' );

			$max = 0;
			foreach ( $props as $idx => $prop ) {
				if ( ($propNameLen = static::strLen( $prop->name )) > $max ) {
					$max = $propNameLen;
				}
			}

			foreach ( $props as $idx => $prop ) {

				if ( $this->hasInstanceTimedOut() ) {
					break;
				}

				$bubbles     = array();
				$sourceClass = $prop->getDeclaringClass();
				$inherited   = $reflector->getShortName() !== $sourceClass->getShortName();
				$meta        = $sourceClass->isInternal() ? null : static::parseComment( $prop->getDocComment() );

				if ( $meta ) {
					if ( $inherited ) {
						$meta['sub'] = array( array( 'Declared in', $sourceClass->getShortName() ) );
					}

					if ( isset( $meta['tags']['var'][0] ) ) {
						$meta['left'] = $meta['tags']['var'][0][0];
					}

					unset( $meta['tags'] );
				}

				if ( $prop->isProtected() || $prop->isPrivate() ) {
					$prop->setAccessible( true );
				}

				$value = $prop->getValue( $subject );

				$this->fmt->startRow();
				$this->fmt->sep( $prop->isStatic() ? '::' : '->' );
				$this->fmt->colDiv();

				$bubbles  = array();
				if ( $prop->isProtected() ) {
					$bubbles[] = array( 'P', 'Protected' );
				}

				if ( $prop->isPrivate() ) {
					$bubbles[] = array( '!', 'Private' );
				}

				$this->fmt->bubbles( $bubbles );

				$type = array( 'prop' );

				if ( $inherited ) {
					$type[] = 'inherited';
				}

				if ( $prop->isPrivate() ) {
					$type[] = 'private';
				}

				$this->fmt->colDiv( 2 - count( $bubbles ) );
				$this->fmt->startContain( $type );
				$this->fmt->text( 'name', $prop->name, $meta, $this->linkify( $prop ) );
				$this->fmt->endContain();
				$this->fmt->colDiv( $max - static::strLen( $prop->name ) );
				$this->fmt->sep( '=' );
				$this->fmt->colDiv();
				$this->evaluate( $value );
				$this->fmt->endRow();
			}
		}

		// __debugInfo()
		if ( $magicProps ) {
			$this->fmt->sectionTitle( 'Properties (magic)' );

			$max = 0;
			foreach ( $magicProps as $name => $value ) {
				if ( ($propNameLen = static::strLen( $name )) > $max ) {
					$max = $propNameLen;
				}
			}

			foreach ( $magicProps as $name => $value ) {

				if ( $this->hasInstanceTimedOut() ) {
					break;
				}

				// attempt to pull out doc comment from the "regular" property definition
				try {
					$prop = $reflector->getProperty( $name );
					$meta = static::parseComment( $prop->getDocComment() );

				} catch ( \Exception $e ) {
					$meta = null;
				}

				$this->fmt->startRow();
				$this->fmt->sep( '->' );
				$this->fmt->colDiv();

				$type = array( 'prop' );

				$this->fmt->startContain( $type );
				$this->fmt->text( 'name', $name, $meta );
				$this->fmt->endContain();
				$this->fmt->colDiv( $max - static::strLen( $name ) );
				$this->fmt->sep( '=' );
				$this->fmt->colDiv();
				$this->evaluate( $value );
				$this->fmt->endRow();
			}
		}

		// class methods
		if ( $methods && ! $this->hasInstanceTimedOut() ) {

			$this->fmt->sectionTitle( 'Methods' );
			foreach ( $methods as $idx => $method ) {

				$this->fmt->startRow();
				$this->fmt->sep( $method->isStatic() ? '::' : '->' );
				$this->fmt->colDiv();

				$bubbles = array();
				if ( $method->isAbstract() ) {
					$bubbles[] = array( 'A', 'Abstract' );
				}

				if ( $method->isFinal() ) {
					$bubbles[] = array( 'F', 'Final' );
				}

				if ( $method->isProtected() ) {
					$bubbles[] = array( 'P', 'Protected' );
				}

				if ( $method->isPrivate() ) {
					$bubbles[] = array( '!', 'Private' );
				}

				$this->fmt->bubbles( $bubbles );

				$this->fmt->colDiv( 4 - count( $bubbles ) );

				// is this method inherited?
				$inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName();

				$type = array( 'method' );

				if ( $inherited ) {
					$type[] = 'inherited';
				}

				if ( $method->isPrivate() ) {
					$type[] = 'private';
				}

				$this->fmt->startContain( $type );

				$name = $method->name;
				if ( $method->returnsReference() ) {
					$name = "&{$name}";
				}

				$this->fromReflector( $method, $name, $reflector );

				$paramCom   = $method->isInternal() ? array() : static::parseComment( $method->getDocComment(), 'tags' );
				$paramCom   = empty( $paramCom['param'] ) ? array() : $paramCom['param'];
				$paramCount = $method->getNumberOfParameters();

				$this->fmt->sep( '(' );

				// process arguments
				foreach ( $method->getParameters() as $idx => $parameter ) {
					$meta      = null;
					$paramName = "\${$parameter->name}";
					$optional  = $parameter->isOptional();
					$variadic  = static::$env['is56'] && $parameter->isVariadic();

					if ( $parameter->isPassedByReference() ) {
						$paramName = "&{$paramName}";
					}

					if ( $variadic ) {
						$paramName = "...{$paramName}";
					}

					$type = array( 'param' );

					if ( $optional ) {
						$type[] = 'optional';
					}

					$this->fmt->startContain( $type );

					// attempt to build meta
					foreach ( $paramCom as $tag ) {
						list($pcTypes, $pcName, $pcDescription) = $tag;
						if ( $pcName !== $paramName ) {
							continue;
						}

						$meta = array(
							'title' => $pcDescription,
						);

						if ( $pcTypes ) {
							$meta['left'] = $pcTypes;
						}

						break;
					}

					try {
						$paramClass = $parameter->getClass();
					} catch ( \Exception $e ) {
						// @see https://bugs.php.net/bug.php?id=32177&edit=1
					}

					if ( ! empty( $paramClass ) ) {
						$this->fmt->startContain( 'hint' );
						$this->fromReflector( $paramClass, $paramClass->name );
						$this->fmt->endContain();
						$this->fmt->sep( ' ' );

					} elseif ( $parameter->isArray() ) {
						$this->fmt->text( 'hint', 'array' );
						$this->fmt->sep( ' ' );

					} else {
						$hasType = static::$env['is7'] && $parameter->hasType();
						if ( $hasType ) {
							$type = $parameter->getType();
							$this->fmt->text( 'hint', (string) $type );
							$this->fmt->sep( ' ' );
						}
					}

					$this->fmt->text( 'name', $paramName, $meta );

					if ( $optional ) {
						$paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
						if ( $paramValue !== null ) {
								$this->fmt->sep( ' = ' );

							if ( static::$env['is546'] && ! $parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant() ) {
								$this->fmt->text( 'constant', $parameter->getDefaultValueConstantName(), 'Constant' );

							} else {
								$this->evaluate( $paramValue, true );
							}
						}
					}

					$this->fmt->endContain();

					if ( $idx < $paramCount - 1 ) {
						$this->fmt->sep( ', ' );
					}
				}
				$this->fmt->sep( ')' );
				$this->fmt->endContain();

				$hasReturnType = static::$env['is7'] && $method->hasReturnType();
				if ( $hasReturnType ) {
					$type = $method->getReturnType();
					$this->fmt->startContain( 'ret' );
					$this->fmt->sep( ':' );
					$this->fmt->text( 'hint', (string) $type );
					$this->fmt->endContain();
				}

				$this->fmt->endRow();
			}
		}

		unset( $hashes[ $hash ] );
		$this->fmt->endGroup();

		$this->fmt->cacheLock( $hash );
	}



	/**
	 * Scans for known classes and functions inside the provided expression,
	 * and linkifies them when possible
	 *
	 * @param   string $expression   Expression to format
	 * @return  string               Formatted output
	 */
	protected function evaluateExp( $expression = null ) {

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

		if ( static::strLen( $expression ) > 120 ) {
			$expression = substr( $expression, 0, 120 ) . '...';
		}

		$this->fmt->sep( '> ' );

		if ( strpos( $expression, '(' ) === false ) {
			return $this->fmt->text( 'expTxt', $expression );
		}

		$keywords = array_map( 'trim', explode( '(', $expression, 2 ) );
		$parts = array();

		// try to find out if this is a function
		try {
			$reflector = new \ReflectionFunction( $keywords[0] );
			$parts[] = array( $keywords[0], $reflector, '' );

		} catch ( \Exception $e ) {

			if ( stripos( $keywords[0], 'new ' ) === 0 ) {
				$cn = explode( ' ' , $keywords[0], 2 );

				// linkify 'new keyword' (as constructor)
				try {
					$reflector = new \ReflectionMethod( $cn[1], '__construct' );
					$parts[] = array( $cn[0], $reflector, '' );

				} catch ( \Exception $e ) {
					$reflector = null;
					$parts[] = $cn[0];
				}

				// class name...
				try {
					$reflector = new \ReflectionClass( $cn[1] );
					$parts[] = array( $cn[1], $reflector, ' ' );

				} catch ( \Exception $e ) {
					$reflector = null;
					$parts[] = $cn[1];
				}
			} else {

				// we can only linkify methods called statically
				if ( strpos( $keywords[0], '::' ) === false ) {
					return $this->fmt->text( 'expTxt', $expression );
				}

				$cn = explode( '::', $keywords[0], 2 );

				// attempt to linkify class name
				try {
					$reflector = new \ReflectionClass( $cn[0] );
					$parts[] = array( $cn[0], $reflector, '' );

				} catch ( \Exception $e ) {
					$reflector = null;
					$parts[] = $cn[0];
				}

							// perhaps it's a static class method; try to linkify method
				try {
					$reflector = new \ReflectionMethod( $cn[0], $cn[1] );
					$parts[] = array( $cn[1], $reflector, '::' );

				} catch ( \Exception $e ) {
					$reflector = null;
					$parts[] = $cn[1];
				}
			}
		}

		$parts[] = "({$keywords[1]}";

		foreach ( $parts as $element ) {
			if ( ! is_array( $element ) ) {
				$this->fmt->text( 'expTxt', $element );
				continue;
			}

			list($text, $reflector, $prefix) = $element;

			if ( $prefix !== '' ) {
				$this->fmt->text( 'expTxt', $prefix );
			}

			$this->fromReflector( $reflector, $text );
		}

	}



	/**
	 * Calculates real string length
	 *
	 * @param   string $string
	 * @return  int
	 */
	protected static function strLen( $string ) {
		$encoding = function_exists( 'mb_detect_encoding' ) ? mb_detect_encoding( $string ) : false;
		return $encoding ? mb_strlen( $string, $encoding ) : strlen( $string );
	}



	/**
	 * Safe str_pad alternative
	 *
	 * @param   string $string
	 * @param   int    $padLen
	 * @param   string $padStr
	 * @param   int    $padType
	 * @return  string
	 */
	protected static function strPad( $input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT ) {
		$diff = strlen( $input ) - static::strLen( $input );
		return str_pad( $input, $padLen + $diff, $padStr, $padType );
	}

}



/**
 * Formatter abstraction
 */
abstract class RFormatter {

	/**
	 * Flush output and send contents to the output device
	 */
	abstract public function flush();

	/**
	 * Generate a base entity
	 *
	 * @param  string|array      $type
	 * @param  string|null       $text
	 * @param  string|array|null $meta
	 * @param  string|null       $uri
	 */
	abstract public function text( $type, $text = null, $meta = null, $uri = null);

	/**
	 * Generate container start token
	 *
	 * @param  string|array $type
	 * @param  string|bool  $label
	 */
	public function startContain( $type, $label = false ) {}

	/**
	 * Generate container ending token
	 */
	public function endContain(){}

	/**
	 * Generate empty group token
	 *
	 * @param  string $prefix
	 */
	public function emptyGroup( $prefix = '' ) {}

	/**
	 * Generate group start token
	 *
	 * This method must return boolean TRUE on success, false otherwise (eg. max depth reached).
	 * The evaluator will skip this group on FALSE
	 *
	 * @param   string $prefix
	 * @return  bool
	 */
	public function startGroup( $prefix = '' ) {}

	/**
	 * Generate group ending token
	 */
	public function endGroup(){}

	/**
	 * Generate section title
	 *
	 * @param  string $title
	 */
	public function sectionTitle( $title ) {}

	/**
	 * Generate row start token
	 */
	public function startRow(){}

	/**
	 * Generate row ending token
	 */
	public function endRow(){}

	/**
	 * Column divider (cell delimiter)
	 *
	 * @param  int $padLen
	 */
	public function colDiv( $padLen = null ) {}

	/**
	 * Generate modifier tokens
	 *
	 * @param  array $items
	 */
	public function bubbles( array $items ) {}

	/**
	 * Input expression start
	 */
	public function startExp(){}

	/**
	 * Input expression end
	 */
	public function endExp(){}

	/**
	 * Root starting token
	 */
	public function startRoot(){}

	/**
	 * Root ending token
	 */
	public function endRoot(){}

	/**
	 * Separator token
	 *
	 * @param  string $label
	 */
	public function sep( $label = ' ' ) {}

	/**
	 * Resolve cache request
	 *
	 * If the ID is not present in the cache, then a new cache entry is created
	 * for the given ID, and string offsets are captured until cacheLock is called
	 *
	 * This method must return TRUE if the ID exists in the cache, and append the cached item
	 * to the output, FALSE otherwise.
	 *
	 * @param   string $id
	 * @return  bool
	 */
	public function didCache( $id ) {
		return false;
	}

	/**
	 * Ends cache capturing for the given ID
	 *
	 * @param  string $id
	 */
	public function cacheLock( $id ) {}

}




/**
 * Generates the output in HTML5 format
 */
class RHtmlFormatter extends RFormatter {

	protected

		/**
		 * Actual output
		 *
		 * @var  string
		 */
		$out = '',

		/**
		 * Tracks current nesting level
		 *
		 * @var  int
		 */
		$level = 0,

		/**
		 * Stores tooltip content for all entries
		 *
		 * To avoid having duplicate tooltip data in the HTML, we generate them once,
		 * and use references (the Q index) to pull data when required;
		 * this improves performance significantly
		 *
		 * @var  array
		 */
		$tips = array(),

		/**
		 * Used to cache output to speed up processing.
		 *
		 * Contains hashes as keys and string offsets as values.
		 * Cached objects will not be processed again in the same query
		 *
		 * @var  array
		 */
		$cache = array(),

		/**
		 * Map of used HTML tag and attributes
		 *
		 * @var string
		 */
		$def = array();



	protected static

		/**
		 * Instance counter
		 *
		 * @var  int
		 */
		$counter = 0,

		/**
		 * Tracks style/jscript inclusion state
		 *
		 * @var  bool
		 */
		$didAssets = false;


	public function __construct() {

		if ( WSAL_Ref::config( 'validHtml' ) ) {

			$this->def = array(
				'base'   => 'span',
				'tip'    => 'div',
				'cell'   => 'data-cell',
				'table'  => 'data-table',
				'row'    => 'data-row',
				'group'  => 'data-group',
				'gLabel' => 'data-gLabel',
				'match'  => 'data-match',
				'tipRef' => 'data-tip',
			);

		} else {

			$this->def = array(
				'base'   => 'r',
				'tip'    => 't',
				'cell'   => 'c',
				'table'  => 't',
				'row'    => 'r',
				'group'  => 'g',
				'gLabel' => 'gl',
				'match'  => 'm',
				'tipRef' => 'h',
			);

		}

	}



	public function flush() {
		print $this->out;
		$this->out   = '';
		$this->cache = array();
		$this->tips  = array();
	}


	public function didCache( $id ) {

		if ( ! isset( $this->cache[ $id ] ) ) {
			$this->cache[ $id ] = array();
			$this->cache[ $id ][] = strlen( $this->out );
			return false;
		}

		if ( ! isset( $this->cache[ $id ][1] ) ) {
			$this->cache[ $id ][0] = strlen( $this->out );
			return false;
		}

		$this->out .= substr( $this->out, $this->cache[ $id ][0], $this->cache[ $id ][1] );
		return true;
	}

	public function cacheLock( $id ) {
		$this->cache[ $id ][] = strlen( $this->out ) - $this->cache[ $id ][0];
	}


	public function sep( $label = ' ' ) {
		$this->out .= $label !== ' ' ? '<i>' . static::escape( $label ) . '</i>' : $label;
	}

	public function text( $type, $text = null, $meta = null, $uri = null ) {

		if ( ! is_array( $type ) ) {
			$type = (array) $type;
		}

		$tip  = '';
		$text = ($text !== null) ? static::escape( $text ) : static::escape( $type[0] );

		if ( in_array( 'special', $type ) ) {
			$text = strtr(
				$text, array(
					"\r" => '<i>\r</i>',     // carriage return
				"\t" => '<i>\t</i>',     // horizontal tab
				"\n" => '<i>\n</i>',     // linefeed (new line)
				"\v" => '<i>\v</i>',     // vertical tab
				"\e" => '<i>\e</i>',     // escape
				"\f" => '<i>\f</i>',     // form feed
				"\0" => '<i>\0</i>',
				)
			);
		}

		// generate tooltip reference (probably the slowest part of the code ;)
		if ( $meta !== null ) {
			$tipIdx = array_search( $meta, $this->tips, true );

			if ( $tipIdx === false ) {
				$tipIdx = array_push( $this->tips, $meta ) - 1;
			}

			$tip = " {$this->def['tipRef']}=\"{$tipIdx}\"";
			// $tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx);
		}

		// wrap text in a link?
		if ( $uri !== null ) {
			$text = '<a href="' . $uri . '" target="_blank">' . $text . '</a>';
		}

		$typeStr = '';
		foreach ( $type as $part ) {
			$typeStr .= " data-{$part}";
		}

		$this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}</{$this->def['base']}>";
		// $this->out .= sprintf('<%1$s%2$s %3$s>%4$s</%1$s>', $this->def['base'], $typeStr, $tip, $text);
	}

	public function startContain( $type, $label = false ) {

		if ( ! is_array( $type ) ) {
			$type = (array) $type;
		}

		if ( $label ) {
			$this->out .= '<br>';
		}

		$typeStr = '';
		foreach ( $type as $part ) {
			$typeStr .= " data-{$part}";
		}

		$this->out .= "<{$this->def['base']}{$typeStr}>";

		if ( $label ) {
			$this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}</{$this->def['base']}>";
		}
	}

	public function endContain() {
		$this->out .= "</{$this->def['base']}>";
	}

	public function emptyGroup( $prefix = '' ) {

		if ( $prefix !== '' ) {
			$prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape( $prefix ) . "</{$this->def['base']}>";
		}

		$this->out .= "<i>(</i>{$prefix}<i>)</i>";
	}


	public function startGroup( $prefix = '' ) {

		$maxDepth = WSAL_Ref::config( 'maxDepth' );

		if ( ($maxDepth > 0) && (($this->level + 1) > $maxDepth) ) {
			$this->emptyGroup( '...' );
			return false;
		}

		$this->level++;

		$expLvl = WSAL_Ref::config( 'expLvl' );
		$exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : '';

		if ( $prefix !== '' ) {
			$prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape( $prefix ) . "</{$this->def['base']}>";
		}

		$this->out .= "<i>(</i>{$prefix}<{$this->def['base']} data-toggle{$exp}></{$this->def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>";

		return true;
	}

	public function endGroup() {
		$this->out .= "</{$this->def['base']}></{$this->def['base']}><i>)</i>";
		$this->level--;
	}

	public function sectionTitle( $title ) {
		$this->out .= "</{$this->def['base']}><{$this->def['base']} data-tHead>{$title}</{$this->def['base']}><{$this->def['base']} {$this->def['table']}>";
	}

	public function startRow() {
		$this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>";
	}

	public function endRow() {
		$this->out .= "</{$this->def['base']}></{$this->def['base']}>";
	}

	public function colDiv( $padLen = null ) {
		$this->out .= "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>";
	}

	public function bubbles( array $items ) {

		if ( ! $items ) {
			return;
		}

		$this->out .= "<{$this->def['base']} data-mod>";

		foreach ( $items as $info ) {
			$this->out .= $this->text( 'mod-' . strtolower( $info[1] ), $info[0], $info[1] );
		}

		$this->out .= "</{$this->def['base']}>";
	}

	public function startExp() {
		$this->out .= "<{$this->def['base']} data-input>";
	}

	public function endExp() {
		if ( WSAL_Ref::config( 'showBacktrace' ) && ($trace = WSAL_Ref::getBacktrace()) ) {
			$docRoot = isset( $_SERVER['DOCUMENT_ROOT'] ) ? $_SERVER['DOCUMENT_ROOT'] : '';
			$path = strpos( $trace['file'], $docRoot ) !== 0 ? $trace['file'] : ltrim( str_replace( $docRoot, '', $trace['file'] ), '/' );
			// $this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}</{$this->def['base']}>";
		}

		$this->out .= "</{$this->def['base']}><{$this->def['base']} data-output>";
	}

	public function startRoot() {
		$this->out .= '<!-- ref#' . ++static::$counter . ' --><div>' . static::getAssets() . '<div class="ref">';
	}

	public function endRoot() {
		$this->out .= "</{$this->def['base']}>";

		// process tooltips
		$tipHtml = '';
		foreach ( $this->tips as $idx => $meta ) {

			$tip = '';
			if ( ! is_array( $meta ) ) {
				$meta = array(
					'title' => $meta,
				);
			}

			$meta += array(
				'title'       => '',
				'left'        => '',
				'description' => '',
				'tags'        => array(),
				'sub'         => array(),
			);

			$meta = static::escape( $meta );
			$cols = array();

			if ( $meta['left'] ) {
				$cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}</{$this->def['base']}>";
			}

			$title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}</{$this->def['base']}>" : '';
			$desc  = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}</{$this->def['base']}>" : '';
			$tags  = '';

			foreach ( $meta['tags'] as $tag => $values ) {
				foreach ( $values as $value ) {
					if ( $tag === 'param' ) {
						$value[0] = "{$value[0]} {$value[1]}";
						unset( $value[1] );
					}

					$value  = is_array( $value ) ? implode( "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $value ) : $value;
					$tags  .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}</{$this->def['base']}></{$this->def['base']}>";
				}
			}

			if ( $tags ) {
				$tags = "<{$this->def['base']} {$this->def['table']}>{$tags}</{$this->def['base']}>";
			}

			if ( $title || $desc || $tags ) {
				$cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}</{$this->def['base']}>";
			}

			if ( $cols ) {
				$tip = "<{$this->def['base']} {$this->def['row']}>" . implode( '', $cols ) . "</{$this->def['base']}>";
			}

			$sub = '';
			foreach ( $meta['sub'] as $line ) {
				$sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode( "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $line ) . "</{$this->def['base']}></{$this->def['base']}>";
			}

			if ( $sub ) {
				$tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}</{$this->def['base']}></{$this->def['base']}></{$this->def['base']}>";
			}

			if ( $tip ) {
				$this->out .= "<{$this->def['tip']}>{$tip}</{$this->def['tip']}>";
			}
		}

		if ( ($timeout = WSAL_Ref::getTimeoutPoint()) > 0 ) {
			$this->out .= sprintf( "<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fs</{$this->def['base']}>", $timeout );
		}

		$this->out .= '</div></div><!-- /ref#' . static::$counter . ' -->';
	}



	/**
	 * Get styles and javascript (only generated for the 1st call)
	 *
	 * @return  string
	 */
	public static function getAssets() {

		// first call? include styles and javascript
		if ( static::$didAssets ) {
			return '';
		}

		ob_start();

		if ( WSAL_Ref::config( 'stylePath' ) !== false ) {
			?>
			<style>
				<?php readfile( str_replace( '{:dir}', __DIR__, WSAL_Ref::config( 'stylePath' ) ) ); ?>
			</style>
			<?php
		}

		if ( WSAL_Ref::config( 'scriptPath' ) !== false ) {
			?>
			<script>
				<?php readfile( str_replace( '{:dir}', __DIR__, WSAL_Ref::config( 'scriptPath' ) ) ); ?>
			</script>
			<?php
		}

		// normalize space and remove comments
		$output = preg_replace( '/\s+/', ' ', trim( ob_get_clean() ) );
		$output = preg_replace( '!/\*.*?\*/!s', '', $output );
		$output = preg_replace( '/\n\s*\n/', "\n", $output );

		static::$didAssets = true;
		return $output;
	}


	/**
	 * Escapes variable for HTML output
	 *
	 * @param   string|array $var
	 * @return  string|array
	 */
	protected static function escape( $var ) {
		return is_array( $var ) ? array_map( 'static::escape', $var ) : htmlspecialchars( $var, ENT_QUOTES );
	}

}



/**
 * Generates the output in plain text format
 */
class RTextFormatter extends RFormatter {

	protected

		/**
		 * Actual output
		 *
		 * @var  string
		 */
		$out        = '',

		/**
		 * Tracks current nesting level
		 *
		 * @var  int
		 */
		$level      = 0,

		/**
		 * Current indenting level
		 *
		 * @var  int
		 */
		$indent     = 0,

		$lastIdx    = 0,
		$lastLineSt = 0,
		$levelPad   = array( 0 );



	public function flush() {
		print $this->out;
		$this->out   = '';
		$this->cache = array();
	}

	public function sep( $label = ' ' ) {
		$this->out .= $label;
	}

	public function text( $type, $text = null, $meta = null, $uri = null ) {

		if ( ! is_array( $type ) ) {
			$type = (array) $type;
		}

		if ( $text === null ) {
			$text = $type[0];
		}

		if ( in_array( 'special', $type, true ) ) {
			$text = strtr(
				$text, array(
					"\r" => '\r',     // carriage return
				"\t" => '\t',     // horizontal tab
				"\n" => '\n',     // linefeed (new line)
				"\v" => '\v',     // vertical tab
				"\e" => '\e',     // escape
				"\f" => '\f',     // form feed
				"\0" => '\0',
				)
			);

			$this->out .= $text;
			return;
		}

		$formatMap = array(
			'string'   => '%3$s "%2$s"',
			'integer'  => 'int(%2$s)',
			'double'   => 'double(%2$s)',
			'true'     => 'bool(%2$s)',
			'false'    => 'bool(%2$s)',
			'key'      => '[%2$s]',
		);

		if ( ! is_string( $meta ) ) {
			$meta = '';
		}

		$this->out .= isset( $formatMap[ $type[0] ] ) ? sprintf( $formatMap[ $type[0] ], $type[0], $text, $meta ) : $text;
	}

	public function startContain( $type, $label = false ) {

		if ( ! is_array( $type ) ) {
			$type = (array) $type;
		}

		if ( $label ) {
			$this->out .= "\n" . str_repeat( ' ', $this->indent + $this->levelPad[ $this->level ] ) . "┗ {$type[0]} ~ ";
		}
	}

	public function emptyGroup( $prefix = '' ) {
		$this->out .= "({$prefix})";
	}

	public function startGroup( $prefix = '' ) {

		$maxDepth = WSAL_Ref::config( 'maxDepth' );

		if ( ($maxDepth > 0) && (($this->level + 1) > $maxDepth) ) {
			$this->emptyGroup( '...' );
			return false;
		}

		$this->level++;
		$this->out .= '(';

		$this->indent += $this->levelPad[ $this->level - 1 ];
		return true;
	}

	public function endGroup() {
		$this->out .= "\n" . str_repeat( ' ', $this->indent ) . ')';
		$this->indent -= $this->levelPad[ $this->level - 1 ];
		$this->level--;
	}

	public function sectionTitle( $title ) {
		$pad = str_repeat( ' ', $this->indent + 2 );
		$this->out .= sprintf( "\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat( '-', strlen( $title ) ) );
	}

	public function startRow() {
		$this->out .= "\n  " . str_repeat( ' ', $this->indent );
		$this->lastLineSt = strlen( $this->out );
	}

	public function endRow() {
	}

	public function colDiv( $padLen = null ) {
		$padLen = ($padLen !== null) ? $padLen + 1 : 1;
		$this->out .= str_repeat( ' ', $padLen );

		$this->lastIdx = strlen( $this->out );
		$this->levelPad[ $this->level ] = $this->lastIdx - $this->lastLineSt + 2;
	}

	public function bubbles( array $items ) {

		if ( ! $items ) {
			$this->out .= '  ';
			return;
		}

		$this->out .= '<';

		foreach ( $items as $item ) {
			$this->out .= $item[0];
		}

		$this->out .= '>';
	}

	public function endExp() {

		if ( WSAL_Ref::config( 'showBacktrace' ) && ($trace = WSAL_Ref::getBacktrace()) ) {
			$this->out .= ' - ' . $trace['file'] . ':' . $trace['line'];
		}

		$this->out .= "\n" . str_repeat( '=', strlen( $this->out ) ) . "\n";
	}

	public function startRoot() {
		$this->out .= "\n\n";

	}

	public function endRoot() {
		$this->out .= "\n";
		if ( ($timeout = WSAL_Ref::getTimeoutPoint()) > 0 ) {
			$this->out .= sprintf( "\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout );
		}
	}

}



/**
 * Text formatter with color support for CLI -- unfinished
 */
class RCliTextFormatter extends RTextFormatter {

	public function sectionTitle( $title ) {
		$pad = str_repeat( ' ', $this->indent + 2 );
		$this->out .= sprintf( "\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title );
	}

	public function startExp() {
		$this->out .= "\x1b[1;44;96m ";
	}

	public function endExp() {
		if ( WSAL_Ref::config( 'showBacktrace' ) && ($trace = WSAL_Ref::getBacktrace()) ) {
			$this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line'];
		}

		$this->out .= " \x1b[0m\n";
	}

	public function endRoot() {
		$this->out .= "\n";
		if ( ($timeout = WSAL_Ref::getTimeoutPoint()) > 0 ) {
			$this->out .= sprintf( "\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout );
		}
	}

}
