diff --git a/ref.php b/ref.php index 19bece7..c0db119 100644 --- a/ref.php +++ b/ref.php @@ -1,2871 +1,3076 @@ REF'; + // IE goes funky if there's no doctype + if (!$capture && ($format === 'html') && !headers_sent() && (!ob_get_level() || ini_get('output_buffering'))) { + print 'REF'; + } - $ref = new ref($format); + $ref = new ref($format); - if($capture) - ob_start(); + if ($capture) { + ob_start(); + } - foreach($args as $index => $arg) - $ref->query($arg, $expressions ? $expressions[$index] : null); + 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 ''; - exit(0); - } -} + // 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 ''; + exit(0); + } + } + /** + * Shortcut to ref, plain text mode + * + * @param mixed $args + * @return void|string + */ + function rt() + { + $args = func_get_args(); + $options = []; + $output = ''; + $expressions = ref::getInputExpressions($options); + $capture = in_array('@', $options, true); + $ref = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText'); + + if (func_num_args() !== count($expressions)) { + $expressions = null; + } -/** - * Shortcut to ref, plain text mode - * - * @param mixed $args - * @return void|string - */ -function rt(){ - $args = func_get_args(); - $options = array(); - $output = ''; - $expressions = ref::getInputExpressions($options); - $capture = in_array('@', $options, true); - $ref = new ref((php_sapi_name() !== 'cli') || $capture ? 'text' : 'cliText'); + if (!headers_sent()) { + header('Content-Type: text/plain; charset=utf-8'); + } - if(func_num_args() !== count($expressions)) - $expressions = null; + if ($capture) { + ob_start(); + } - if(!headers_sent()) - header('Content-Type: text/plain; charset=utf-8'); + foreach ($args as $index => $arg) { + $ref->query($arg, $expressions ? $expressions[$index] : null); + } - if($capture) - ob_start(); + if ($capture) { + return ob_get_clean(); + } - foreach($args as $index => $arg) - $ref->query($arg, $expressions ? $expressions[$index] : null); + if (in_array('~', $options, true)) { + exit(0); + } + } - if($capture) - return ob_get_clean(); + /** + * REF is a nicer alternative to PHP's print_r() / var_dump(). + * + * @version 1.0 + * @author digitalnature - http://digitalnature.eu + */ + class ref + { - if(in_array('~', $options, true)) - exit(0); -} + const + + MARKER_KEY = '_phpRefArrayMarker_'; + protected static + + /** + * CPU time used for processing + * + * @var array + */ + $time = 0, + + /** + * Configuration (+ default values) + * + * @var array + */ + $config = [ + + // 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' => ['r', 'rt'], + + // custom/external formatters (as associative array: format => className) + 'formatters' => [], + + // 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 = [], + + /** + * Timeout point + * + * @var bool + */ + $timeout = -1, + + $debug = [ + '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; + + /** + * Constructor + * + * @param string|RFormatter $format Output format ID, or formatter instance defaults to 'html' + */ + 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); + print_r($iniVal); + if ($iniVal !== false) { + static::$config[$key] = $iniVal; + } + } + } -/** - * REF is a nicer alternative to PHP's print_r() / var_dump(). - * - * @version 1.0 - * @author digitalnature - http://digitalnature.eu - */ -class ref{ + if ($format instanceof RFormatter) { + $this->fmt = $format; - const + } else { + $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R'.ucfirst($format).'Formatter'; - MARKER_KEY = '_phpRefArrayMarker_'; - + if (!class_exists($format, false)) { + throw new \Exception(sprintf('%s class not found', $format)); + } + $this->fmt = new $format(); + } - protected static - - /** - * CPU time used for processing - * - * @var array - */ - $time = 0, + if (static::$env) { + return; + } - /** - * Configuration (+ default values) - * - * @var array - */ - $config = array( + static::$env = [ - // initially expanded levels (for HTML mode only) - 'expLvl' => 1, + // php 5.4+ ? + 'is54' => version_compare(PHP_VERSION, '5.4') >= 0, - // depth limit (0 = no limit); - // this is not related to recursion - 'maxDepth' => 6, + // php 5.4.6+ ? + 'is546' => version_compare(PHP_VERSION, '5.4.6') >= 0, - // show the place where r() has been called from - 'showBacktrace' => true, + // php 5.6+ + 'is56' => version_compare(PHP_VERSION, '5.6') >= 0, - // display iterator contents - 'showIteratorContents' => false, + // curl extension running? + 'curlActive' => function_exists('curl_version'), - // display extra information about resources - 'showResourceInfo' => true, - - // display method and parameter list on objects - 'showMethods' => true, + // is the 'mbstring' extension active? + 'mbStr' => function_exists('mb_detect_encoding'), - // display private properties / methods - 'showPrivateMembers' => false, + // @see: https://bugs.php.net/bug.php?id=52469 + 'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0), + ]; + } - // peform string matches (date, file, functions, classes, json, serialized data, regex etc.) - // note: seriously slows down queries on large amounts of data - 'showStringMatches' => true, + /** + * Enforce proper use of this class + * + * @param string $name + */ + public function __get($name) + { + throw new \Exception(sprintf('No such property: %s', $name)); + } - // 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'), + /** + * 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)); + } - // custom/external formatters (as associative array: format => className) - 'formatters' => array(), + /** + * 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; + } - // stylesheet path (for HTML only); - // 'false' means no styles - 'stylePath' => '{:dir}/ref.css', + $this->startTime = microtime(true); - // javascript path (for HTML only); - // 'false' means no js - 'scriptPath' => '{:dir}/ref.js', + $this->fmt->startRoot(); + $this->fmt->startExp(); + $this->evaluateExp($expression); + $this->fmt->endExp(); + $this->evaluate($subject); + $this->fmt->endRoot(); + $this->fmt->flush(); - // display url info via cURL - 'showUrls' => false, + static::$time += microtime(true) - $this->startTime; + } - // stop evaluation after this amount of time (seconds) - 'timeout' => 10, + /** + * 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; + } - // whether to produce W3c-valid HTML, - // or unintelligible, but optimized markup that takes less space - 'validHtml' => false, - ), + return round($time, 4); + } - /** - * Some environment variables - * used to determine feature support - * - * @var array - */ - $env = array(), + /** + * 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 = []; + + // 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; + } - /** - * Timeout point - * - * @var bool - */ - $timeout = -1, + // ID doesn't exist, start new timer + $timers[$id] = microtime(true); + } - $debug = array( - 'cacheHits' => 0, - 'objects' => 0, - 'arrays' => 0, - 'scalars' => 0, - ); + /** + * 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 = []; + $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, ['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], ['&', '$'], 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; + } + } - protected + $tags[$tag][] = $parts; + end($tags[$tag]); + $pointer = &$tags[$tag][key($tags[$tag])][$lastIdx]; + } - /** - * Output formatter of this instance - * - * @var RFormatter - */ - $fmt = null, + // 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); - /** - * Start time of the current instance - * - * @var float - */ - $startTime = 0; + // 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); - /** - * Constructor - * - * @param string|RFormatter $format Output format ID, or formatter instance defaults to 'html' - */ - public function __construct($format = 'html'){ + $data = compact('title', 'description', 'tags'); - static $didIni = false; + if (!array_filter($data)) { + return null; + } - if(!$didIni){ - $didIni = true; - foreach(array_keys(static::$config) as $key){ - $iniVal = get_cfg_var('ref.' . $key); - print_r($iniVal); - if($iniVal !== false) - static::$config[$key] = $iniVal; - } + if ($key !== null) { + return isset($data[$key]) ? $data[$key] : null; + } - } + return $data; + } - if($format instanceof RFormatter){ - $this->fmt = $format; + /** + * 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; + } + } - }else{ - $format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter'; + if (!$maybeValid) { + throw new \Exception('Pattern does not appear to be a valid PHP regex'); + } - if(!class_exists($format, false)) - throw new \Exception(sprintf('%s class not found', $format)); + $output = []; + $capturingGroupCount = 0; + $groupStyleDepth = 0; + $openGroups = []; + $lastIsQuant = false; + $lastType = 1; // 1 = none; 2 = alternator + $lastStyle = null; - $this->fmt = new $format(); - } + 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); - if(static::$env) - return; + $matches = $matches[0]; - static::$env = array( + $getTokenCharCode = function ($token) { + if (strlen($token) > 1 && $token[0] === '\\') { + $t1 = substr($token, 1); - // php 5.4+ ? - 'is54' => version_compare(PHP_VERSION, '5.4') >= 0, + if (preg_match('/^c[A-Za-z]$/', $t1)) { + return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1; + } - // php 5.4.6+ ? - 'is546' => version_compare(PHP_VERSION, '5.4.6') >= 0, + if (preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1)) { + return intval(substr($t1, 1), 16); + } - // php 5.6+ - 'is56' => version_compare(PHP_VERSION, '5.6') >= 0, + if (preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1)) { + return intval($t1, 8); + } - // curl extension running? - 'curlActive' => function_exists('curl_version'), + $len = strlen($t1); - // is the 'mbstring' extension active? - 'mbStr' => function_exists('mb_detect_encoding'), + if ($len === 1 && strpos('cuxDdSsWw', $t1) !== false) { + return null; + } - // @see: https://bugs.php.net/bug.php?id=52469 - 'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0), - ); - } + 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) { - /** - * Enforce proper use of this class - * - * @param string $name - */ - public function __get($name){ - throw new \Exception(sprintf('No such property: %s', $name)); - } + 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; - /** - * 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)); - } + 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[] = ['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[] = ['chr-meta' => $cm]; + $cLastRangeable = ($cLastType !== 1); + $cLastType = 2; + + } elseif ($cm === '\\') { + throw new \Exception('Incomplete regex token'); + + } else { + $output[] = ['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[] = ['chr-range' => '-']; + $cLastRangeable = false; + $cLastType = 1; + + } else { + $output[] = $closing ? ['chr' => '-'] : ['chr-range' => '-']; + } + + } else { + $output[] = ['chr' => '-']; + $cLastRangeable = ($cLastType !== 1); + } + + } else { + $output[] = ['chr' => $cm]; + $cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1); + $lastCC = $cm[strlen($cm) - 1]; + } + } + $output[] = ['chr' => $closing]; + $lastIsQuant = true; - /** - * 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){ + } elseif ($m[0] === '(') { + if (strlen($m) === 2) { + throw new \Exception('Invalid or unsupported group type'); + } - if(static::$timeout > 0) - return; + if (strlen($m) === 1) { + $capturingGroupCount++; + } - $this->startTime = microtime(true); - - $this->fmt->startRoot(); - $this->fmt->startExp(); - $this->evaluateExp($expression); - $this->fmt->endExp(); - $this->evaluate($subject); - $this->fmt->endRoot(); - $this->fmt->flush(); + $groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1; + $openGroups[] = $m; // opening + $lastIsQuant = false; + $output[] = ["g{$groupStyleDepth}" => $m]; - static::$time += microtime(true) - $this->startTime; - } + } elseif ($m[0] === ')') { + if (!count($openGroups)) { + throw new \Exception('No matching opening parenthesis'); + } + $output[] = ['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); - /** - * 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; + while ($num > $capturingGroupCount) { + preg_match('/[0-9]$/', $num, $digits); + $nonBackrefDigits = $digits[0].$nonBackrefDigits; + $num = floor($num / 10); + } - 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; - } + if ($num > 0) { + $output[] = ['meta' => "\\{$num}", 'text' => $nonBackrefDigits]; - // 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]; - } + } else { + preg_match('/^\\\\([0-3][0-7]{0,2}|[4-7][0-7]?|[89])([0-9]*)/', $m, $pts); + $output[] = ['meta' => '\\'.$pts[1], 'text' => $pts[2]]; + } - // 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); + $lastIsQuant = true; - // 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); + } elseif (isset($m[1]) && preg_match('/^[0bBcdDfnrsStuvwWx]/', $m[1])) { - // 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] : ''; - } + if (preg_match('/^\\\\[cux]$/', $m)) { + throw new \Exception('Incomplete regex token'); + } - $title = ltrim($title); - $description = ltrim($description); + $output[] = ['meta' => $m]; + $lastIsQuant = (strpos('bB', $m[1]) === false); - $data = compact('title', 'description', 'tags'); + } elseif ($m === '\\') { + throw new \Exception('Incomplete regex token'); - if(!array_filter($data)) - return null; + } else { + $output[] = ['text' => $m]; + $lastIsQuant = true; + } - if($key !== null) - return isset($data[$key]) ? $data[$key] : null; + } elseif (preg_match('/^(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??$/', $m)) { + if (!$lastIsQuant) { + throw new \Exception('Quantifiers must be preceded by a token that can be repeated'); + } - return $data; - } + 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'); + } - /** - * 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){ + $output[] = [$lastStyle ? $lastStyle : 'meta' => $m]; + $lastIsQuant = false; - // 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); + } elseif ($m === '|') { + if ($lastType === 1 || ($lastType === 2 && !count($openGroups))) { + throw new \Exception('Empty alternative effectively truncates the regex here'); + } - if(($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) - $maybeValid = true; - } + $output[] = count($openGroups) ? ["g{$groupStyleDepth}" => '|'] : ['meta' => '|']; + $lastIsQuant = false; + $lastType = 2; + $lastStyle = ''; + continue; - if(!$maybeValid) - throw new \Exception('Pattern does not appear to be a valid PHP regex'); + } elseif ($m === '^' || $m === '$') { + $output[] = ['meta' => $m]; + $lastIsQuant = false; - $output = array(); - $capturingGroupCount = 0; - $groupStyleDepth = 0; - $openGroups = array(); - $lastIsQuant = false; - $lastType = 1; // 1 = none; 2 = alternator - $lastStyle = null; + } elseif ($m === '.') { + $output[] = ['meta' => '.']; + $lastIsQuant = true; - 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); + } else { + $output[] = ['text' => $m]; + $lastIsQuant = true; + } - $matches = $matches[0]; + $lastType = 0; + $lastStyle = ''; + } - $getTokenCharCode = function($token){ - if(strlen($token) > 1 && $token[0] === '\\'){ - $t1 = substr($token, 1); + if ($openGroups) { + throw new \Exception('Unclosed grouping'); + } - if(preg_match('/^c[A-Za-z]$/', $t1)) - return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1; + return $output; + } - if(preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1)) - return intval(substr($t1, 1), 16); + /** + * 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(preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1)) - return intval($t1, 8); + if ($value === null) { + return static::$config[$key]; + } - $len = strlen($t1); + if (is_array(static::$config[$key])) { + return static::$config[$key] = (array)$value; + } - if($len === 1 && strpos('cuxDdSsWw', $t1) !== false) - return null; + return static::$config[$key] = $value; + } - 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]; - } + /** + * Total CPU time used by the class + * + * @param int precision + * @return double + */ + public static function getTime($precision = 4) + { + return round(static::$time, $precision); } - } - return ($token !== '\\') ? $token[0] : null; - }; + /** + * Get relevant backtrace info for last ref call + * + * @return array|false + */ + public static function getBacktrace() + { - foreach($matches as $m){ + // 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(); - if($m[0] === '['){ - $lastCC = null; - $cLastRangeable = false; - $cLastType = 0; // 0 = none; 1 = range hyphen; 2 = short class + while ($callee = array_pop($trace)) { - preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts); + // extract only the information we neeed + $callee = array_intersect_key($callee, array_fill_keys(['file', 'function', 'line'], false)); + extract($callee, EXTR_OVERWRITE); - array_shift($parts); - list($opening, $content, $closing) = $parts; + // skip, if the called function doesn't match the shortcut function name + if (!$function || !in_array(mb_strtolower((string)$function), static::$config['shortcutFunc'])) { + continue; + } - if(!$closing) - throw new \Exception('Unclosed character class'); + return compact('file', 'function', 'line'); + } - 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); + return false; + } - foreach($ccTokens as $i => $cm) { + /** + * 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) + { - if($cm[0] === '\\'){ - if(preg_match('/^\\\\[cux]$/', $cm)) - throw new \Exception('Incomplete regex token'); + // used to determine the position of the current call, + // if more queries calls were made on the same line + static $lineInst = []; - if(preg_match('/^\\\\[dsw]$/i', $cm)) { - $output[] = array('chr-meta' => $cm); - $cLastRangeable = ($cLastType !== 1); - $cLastType = 2; + $trace = static::getBacktrace(); - }elseif($cm === '\\'){ - throw new \Exception('Incomplete regex token'); - - }else{ - $output[] = array('chr-meta' => $cm); - $cLastRangeable = $cLastType !== 1; - $lastCC = $getTokenCharCode($cm); + if (!$trace) { + return []; } - }elseif($cm === '-'){ - if($cLastRangeable){ - $nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false; - - if($nextToken){ - $nextTokenCharCode = $getTokenCharCode($nextToken[0]); + extract($trace); - 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 = ''; - } + $code = file($file); + $code = $code[$line - 1]; // multiline expressions not supported! + $instIndx = 0; + $tokens = token_get_all(" $token) { - return $output; - } + // 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], [T_DOUBLE_COLON, T_OBJECT_OPERATOR], true)) { + continue; + } + // find argument definition start, just after '(' + if (isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')) { + $instIndx++; - /** - * Set or get configuration options - * - * @param string $key - * @param mixed|null $value - * @return mixed - */ - public static function config($key, $value = null){ + if (!isset($lineInst[$line])) { + $lineInst[$line] = 0; + } - 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 ($instIndx <= $lineInst[$line]) { + continue; + } - if($value === null) - return static::$config[$key]; + $lineInst[$line]++; - if(is_array(static::$config[$key])) - return static::$config[$key] = (array)$value; + // gather options + if ($options !== null) { + $j = $i - 1; + while (isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], ['@', '+', '-', '!', '~'])) { + $options[] = $tokens[$j--]; + } + } - return static::$config[$key] = $value; - } + $lvl = $index = $curlies = 0; + $expressions = []; + // 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] : ' '; + } - /** - * Total CPU time used by the class - * - * @param int precision - * @return double - */ - public static function getTime($precision = 4){ - return round(static::$time, $precision); - } + continue; + } + if ($token === '{') { + $curlies++; + } + if ($token === '}') { + $curlies--; + } - /** - * Get relevant backtrace info for last ref call - * - * @return array|false - */ - public static function getBacktrace(){ + if ($token === '(') { + $lvl++; + } - // 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(); + if ($token === ')') { + $lvl--; + } - while($callee = array_pop($trace)){ + // 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; + } - // extract only the information we neeed - $callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false)); - extract($callee, EXTR_OVERWRITE); + // negative parentheses count means we reached the end of argument definitions + if ($lvl < 0) { + foreach ($expressions as &$expression) { + $expression = trim(implode('', $expression)); + } - // 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 $expressions; + } - return compact('file', 'function', 'line'); - } + $expressions[$index][] = $token; + } - return false; - } + 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 = [$class]; + while (($class = $class->getParentClass()) !== false) { + $parents[] = $class; + } - /** - * 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){ + return array_reverse($parents); + } - // used to determine the position of the current call, - // if more queries calls were made on the same line - static $lineInst = array(); + /** + * 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; + } - $trace = static::getBacktrace(); + $items = [$reflector]; - if(!$trace) - return array(); + if (($single === '') && ($reflector instanceof \ReflectionClass)) { + $items = static::getParentClasses($reflector); + } - extract($trace); + $first = true; + foreach ($items as $item) { - $code = file($file); - $code = $code[$line - 1]; // multiline expressions not supported! - $instIndx = 0; - $tokens = token_get_all("fmt->sep(' :: '); + } - // locate the caller position in the line, and isolate argument tokens - foreach($tokens as $i => $token){ + $first = false; + $name = ($single !== '') ? $single : $item->getName(); + $comments = $item->isInternal() ? [] : static::parseComment($item->getDocComment()); + $meta = ['sub' => []]; + $bubbles = []; - // match token with our shortcut function name - if(is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0)) - continue; + if ($item->isInternal()) { + $extension = $item->getExtension(); + $meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), + $extension->getVersion()) : 'Internal'; - // 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; + } else { + $comments = static::parseComment($item->getDocComment()); - // find argument definition start, just after '(' - if(isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')){ - $instIndx++; + if ($comments) { + $meta += $comments; + } - if(!isset($lineInst[$line])) - $lineInst[$line] = 0; + $meta['sub'][] = ['Defined in', basename($item->getFileName()).':'.$item->getStartLine()]; + } - if($instIndx <= $lineInst[$line]) - continue; + if (($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)) { + if (($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName())) { + $meta['sub'][] = ['Inherited from', $item->getDeclaringClass()->getShortName()]; + } - $lineInst[$line]++; + if ($item instanceof \ReflectionMethod) { + try { + $proto = $item->getPrototype(); + $meta['sub'][] = ['Prototype defined by', $proto->class]; + } catch (\Exception $e) { + } + } - // 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(); + $this->fmt->text('name', $name, $meta, $this->linkify($item)); + continue; + } - // get the expressions - foreach(array_slice($tokens, $i + 2) as $token){ + // @todo: maybe - list interface methods + if (!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))) { - if(is_array($token)){ - if($token[0] !== T_COMMENT) - $expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' '; + if ($item->isAbstract()) { + $bubbles[] = ['A', 'Abstract']; + } - continue; - } + if ($item->isFinal()) { + $bubbles[] = ['F', 'Final']; + } - if($token === '{') - $curlies++; + // php 5.4+ only + if (static::$env['is54'] && $item->isCloneable()) { + $bubbles[] = ['C', 'Cloneable']; + } - if($token === '}') - $curlies--; + if ($item->isIterateable()) { + $bubbles[] = ['X', 'Iterateable']; + } - if($token === '(') - $lvl++; + } - if($token === ')') - $lvl--; + if ($item->isInterface() && $single !== '') { + $bubbles[] = ['I', 'Interface']; + } - // 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; - } + if ($bubbles) { + $this->fmt->bubbles($bubbles); + } - // negative parentheses count means we reached the end of argument definitions - if($lvl < 0){ - foreach($expressions as &$expression) - $expression = trim(implode('', $expression)); + if ($item->isInterface() && $single === '') { + $name .= sprintf(' (%d)', count($item->getMethods())); + } - return $expressions; - } + $this->fmt->text('name', $name, $meta, $this->linkify($item)); + } - $expressions[$index][] = $token; + $this->fmt->cacheLock($hash); } - break; - } - } - - } - - + /** + * 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'; + } - /** - * 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){ + if (!$docRefExt) { + $docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php'; + } - $parents = array($class); - while(($class = $class->getParentClass()) !== false) - $parents[] = $class; - - return array_reverse($parents); - } + $phpNetSchemes = [ + '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 = []; + + // 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; - /** - * 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){ + if (in_array($type, ['method', 'property'], true)) { + $args[] = $reflector->getDeclaringClass()->getName(); + } - // @todo: test this - $hash = var_export(func_get_args(), true); - //$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : ''); + $args = array_map(function ($text) { + return str_replace('_', '-', ltrim(strtolower($text), '\\_')); + }, $args); - if($this->fmt->didCache($hash)){ - static::$debug['cacheHits']++; - return; - } + // 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)); - $items = array($reflector); + if ($valid) { + $url = vsprintf($phpNetSchemes[$type], $args); + } - if(($single === '') && ($reflector instanceof \ReflectionClass)) - $items = static::getParentClasses($reflector); + // custom + } else { + switch (true) { - $first = true; - foreach($items as $item){ + // 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; + } - if(!$first) - $this->fmt->sep(' :: '); + // @todo: handle more apps + } - $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()); + return $url; + } - if($comments) - $meta += $comments; + public static function getTimeoutPoint() + { + return static::$timeout; + } - $meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine()); - } + public static function getDebugInfo() + { + return static::$debug; + } - if(($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)){ - if(($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName())) - $meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName()); + protected function hasInstanceTimedOut() + { - if($item instanceof \ReflectionMethod){ - try{ - $proto = $item->getPrototype(); - $meta['sub'][] = array('Prototype defined by', $proto->class); - }catch(\Exception $e){} - } + if (static::$timeout > 0) { + return true; + } - $this->fmt->text('name', $name, $meta, $this->linkify($item)); - continue; - } - - // @todo: maybe - list interface methods - if(!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))){ + $timeout = static::$config['timeout']; - if($item->isAbstract()) - $bubbles[] = array('A', 'Abstract'); + if (($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout)) { + return (static::$timeout = (microtime(true) - $this->startTime)); + } - if($item->isFinal()) - $bubbles[] = array('F', 'Final'); + return false; + } - // php 5.4+ only - if(static::$env['is54'] && $item->isCloneable()) - $bubbles[] = array('C', 'Cloneable'); + /** + * 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($item->isIterateable()) - $bubbles[] = array('X', 'Iterateable'); - - } + if (isset($subject[static::MARKER_KEY])) { + unset($subject[static::MARKER_KEY]); + $this->fmt->text('array'); + $this->fmt->emptyGroup('recursion'); + return; + } - if($item->isInterface() && $single !== '') - $bubbles[] = array('I', 'Interface'); + // first recursion level detection; + // this is optional (used to print consistent recursion info) + foreach ($subject as $key => &$value) { - if($bubbles) - $this->fmt->bubbles($bubbles); + if (!is_array($value)) { + continue; + } - if($item->isInterface() && $single === '') - $name .= sprintf(' (%d)', count($item->getMethods())); + // save current value in a temporary variable + $buffer = $value; - $this->fmt->text('name', $name, $meta, $this->linkify($item)); - } + // assign new value + $value = ($value !== 1) ? 1 : 2; - $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'; - } + // if they're still equal, then we have a reference + if ($value === $subject) { + $value = $buffer; + $value[static::MARKER_KEY] = true; + $this->evaluate($value); + return; + } - // 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(); + // restoring original value + $value = $buffer; + } - // internal function/method/class/property/constant - if($parent->isInternal()){ - $args[] = $reflector->name; + $this->fmt->text('array'); + $count = count($subject); + if (!$this->fmt->startGroup($count)) { + return; + } - if(in_array($type, array('method', 'property'), true)) - $args[] = $reflector->getDeclaringClass()->getName(); + $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(); + } - $args = array_map(function($text){ - return str_replace('_', '-', ltrim(strtolower($text), '\\_')); - }, $args); + unset($subject[static::MARKER_KEY]); - // 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)); + $this->fmt->endGroup(); + return; - if($valid) - $url = vsprintf($phpNetSchemes[$type], $args); + // resource + case 'resource': + $meta = []; + $resType = get_resource_type($subject); - // custom - }else{ - switch(true){ + $this->fmt->text('resource', strval($subject)); - // 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; - } + if (!static::$config['showResourceInfo']) { + return $this->fmt->emptyGroup($resType); + } - // @todo: handle more apps - } + // @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 = [ + '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 = [ + '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 = []; + $query = @mysql_list_dbs($subject); + while ($row = @mysql_fetch_array($query)) { + $dbs[] = $row['Database']; + } + + $meta = [ + '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; - } + } - return $url; - } + if (!$meta) { + return $this->fmt->emptyGroup($resType); + } + if (!$this->fmt->startGroup($resType)) { + return; + } - public static function getTimeoutPoint(){ - return static::$timeout; - } + $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(['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) { + if (is_file($subject) || is_dir($subject)) { + try { + $file = new \SplFileInfo($subject); + $flags = []; + $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)){ + $timestamp = strtotime($subject); + if ($length < 128 && (bool)$timestamp && checkdate(date('m', $timestamp), date('d', $timestamp), date('Y', $timestamp)) && + 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 = [ + '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], ['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], ['{', '['], true); + + if ($isJson) { + $decodingJson++; + $json = json_decode($subject); + + if ($isJson = (json_last_error() === JSON_ERROR_NONE)) { + $this->fmt->startContain('json', true); + $this->evaluate($json); + $this->fmt->endContain(); + } + + $decodingJson--; + } + + // attempt to match a regex + if ($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 + } + + } + } + } - public static function getDebugInfo(){ - return static::$debug; - } + return; + } + // if we reached this point, $subject must be an object + // track objects to detect recursion + static $hashes = []; - protected function hasInstanceTimedOut(){ + // hash ID of this object + $hash = spl_object_hash($subject); + $recursion = isset($hashes[$hash]); - if(static::$timeout > 0) - return true; + // 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; + } - $timeout = static::$config['timeout']; + // check cache at this point + if (!$recursion && $this->fmt->didCache($hash)) { + static::$debug['cacheHits']++; + return; + } - if(($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout)) - return (static::$timeout = (microtime(true) - $this->startTime)); + $reflector = new \ReflectionObject($subject); + $this->fmt->startContain('class'); + $this->fromReflector($reflector); + $this->fmt->text('object', ' object'); + $this->fmt->endContain(); - return false; - } + // already been here? + if ($recursion) { + return $this->fmt->emptyGroup('recursion'); + } + $hashes[$hash] = 1; + $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED; - /** - * 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){ + if (static::$config['showPrivateMembers']) { + $flags |= \ReflectionProperty::IS_PRIVATE; + } - switch($type = gettype($subject)){ + $props = $reflector->getProperties($flags); + $methods = []; - // 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'); + if (static::$config['showMethods']) { + $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED; - // integer/double/float - case 'integer': - case 'double': - return $this->fmt->text($type, $subject, $type); + if (static::$config['showPrivateMembers']) { + $flags |= \ReflectionMethod::IS_PRIVATE; + } - // boolean - case 'boolean': - $text = $subject ? 'true' : 'false'; - return $this->fmt->text($text, $text, $type); + $methods = $reflector->getMethods($flags); + } - // arrays - case 'array': + $constants = $reflector->getConstants(); + $interfaces = $reflector->getInterfaces(); + $traits = static::$env['is54'] ? $reflector->getTraits() : []; + $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); + })); + } - // empty array? - if(empty($subject)){ - $this->fmt->text('array'); - return $this->fmt->emptyGroup(); - } + // no data to display? + if (!$props && !$methods && !$constants && !$interfaces && !$traits) { + unset($hashes[$hash]); + return $this->fmt->emptyGroup(); + } - if(isset($subject[static::MARKER_KEY])){ - unset($subject[static::MARKER_KEY]); - $this->fmt->text('array'); - $this->fmt->emptyGroup('recursion'); - return; - } + if (!$this->fmt->startGroup()) { + return; + } - // first recursion level detection; - // this is optional (used to print consistent recursion info) - foreach($subject as $key => &$value){ + // show contents for iterators + if (static::$config['showIteratorContents'] && $reflector->isIterateable()) { - if(!is_array($value)) - continue; + $itContents = iterator_to_array($subject); + $this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents))); - // save current value in a temporary variable - $buffer = $value; + 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); + } - // 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; - } + $this->fmt->startRow(); + $this->fmt->text(['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(); + } + } - // restoring original value - $value = $buffer; - } + // display the interfaces this objects' class implements + if ($interfaces) { + $items = []; + $this->fmt->sectionTitle('Implements'); + $this->fmt->startRow(); + $this->fmt->startContain('interfaces'); - $this->fmt->text('array'); - $count = count($subject); - if(!$this->fmt->startGroup($count)) - return; + $i = 0; + $count = count($interfaces); - $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(); - } + foreach ($interfaces as $name => $interface) { + $this->fromReflector($interface); - unset($subject[static::MARKER_KEY]); + if (++$i < $count) { + $this->fmt->sep(', '); + } + } - $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++; - $json = json_decode($subject); - - if($isJson = (json_last_error() === JSON_ERROR_NONE)){ - $this->fmt->startContain('json', true); - $this->evaluate($json); $this->fmt->endContain(); - } - - $decodingJson--; + $this->fmt->endRow(); } - // attempt to match a regex - if($length < 768){ - try{ - $components = $this->splitRegex($subject); - if($components){ - $regex = ''; + // traits this objects' class uses + if ($traits) { + $items = []; + $this->fmt->sectionTitle('Uses'); + $this->fmt->startRow(); + $this->fmt->startContain('traits'); + + $i = 0; + $count = count($traits); - $this->fmt->startContain('regex', true); - foreach($components as $component) - $this->fmt->text('regex-' . key($component), reset($component)); - $this->fmt->endContain(); - } + foreach ($traits as $name => $trait) { + $this->fromReflector($trait); - }catch(\Exception $e){ - // not a regex - } + if (++$i < $count) { + $this->fmt->sep(', '); + } + } + $this->fmt->endContain(); + $this->fmt->endRow(); } - } - } - return; - } - - // if we reached this point, $subject must be an object + // 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 = ['const']; + foreach ($parents as $parent) { + if ($parent->hasConstant($name)) { + if ($parent !== $reflector) { + $type[] = 'inherited'; + $meta = ['sub' => [['Prototype defined by', $parent->name]]]; + } + break; + } + } - // track objects to detect recursion - static $hashes = array(); + $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(); + } + } - // 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; - } + // object/class properties + if ($props) { + $this->fmt->sectionTitle('Properties'); - // 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(); + $max = 0; + foreach ($props as $idx => $prop) { + if (($propNameLen = static::strLen($prop->name)) > $max) { + $max = $propNameLen; + } + } - // already been here? - if($recursion) - return $this->fmt->emptyGroup('recursion'); + foreach ($props as $idx => $prop) { - $hashes[$hash] = 1; + if ($this->hasInstanceTimedOut()) { + break; + } - $flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED; + $bubbles = []; + $sourceClass = $prop->getDeclaringClass(); + $inherited = $reflector->getShortName() !== $sourceClass->getShortName(); + $meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment()); - if(static::$config['showPrivateMembers']) - $flags |= \ReflectionProperty::IS_PRIVATE; + if ($meta) { + if ($inherited) { + $meta['sub'] = [['Declared in', $sourceClass->getShortName()]]; + } - $props = $reflector->getProperties($flags); - $methods = array(); + if (isset($meta['tags']['var'][0])) { + $meta['left'] = $meta['tags']['var'][0][0]; + } - if(static::$config['showMethods']){ - $flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED; + unset($meta['tags']); + } - if(static::$config['showPrivateMembers']) - $flags |= \ReflectionMethod::IS_PRIVATE; + if ($prop->isProtected() || $prop->isPrivate()) { + $prop->setAccessible(true); + } - $methods = $reflector->getMethods($flags); - } + $value = $prop->getValue($subject); - $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(); - } + $this->fmt->startRow(); + $this->fmt->sep($prop->isStatic() ? '::' : '->'); + $this->fmt->colDiv(); - 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(); - } + $bubbles = []; + if ($prop->isProtected()) { + $bubbles[] = ['P', 'Protected']; + } - // 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(); - } - } + if ($prop->isPrivate()) { + $bubbles[] = ['!', 'Private']; + } - // object/class properties - if($props){ - $this->fmt->sectionTitle('Properties'); + $this->fmt->bubbles($bubbles); - $max = 0; - foreach($props as $idx => $prop) - if(($propNameLen = static::strLen($prop->name)) > $max) - $max = $propNameLen; + $type = ['prop']; - foreach($props as $idx => $prop){ + if ($inherited) { + $type[] = 'inherited'; + } - if($this->hasInstanceTimedOut()) - break; + if ($prop->isPrivate()) { + $type[] = 'private'; + } - $bubbles = array(); - $sourceClass = $prop->getDeclaringClass(); - $inherited = $reflector->getShortName() !== $sourceClass->getShortName(); - $meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment()); + $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(); + } + } - if($meta){ - if($inherited) - $meta['sub'] = array(array('Declared in', $sourceClass->getShortName())); + // class methods + if ($methods && !$this->hasInstanceTimedOut()) { - if(isset($meta['tags']['var'][0])) - $meta['left'] = $meta['tags']['var'][0][0]; + $this->fmt->sectionTitle('Methods'); + foreach ($methods as $idx => $method) { + $this->fmt->startRow(); + $this->fmt->sep($method->isStatic() ? '::' : '->'); + $this->fmt->colDiv(); - unset($meta['tags']); - } + $bubbles = []; + if ($method->isAbstract()) { + $bubbles[] = ['A', 'Abstract']; + } - if($prop->isProtected() || $prop->isPrivate()) - $prop->setAccessible(true); + if ($method->isFinal()) { + $bubbles[] = ['F', 'Final']; + } - $value = $prop->getValue($subject); + if ($method->isProtected()) { + $bubbles[] = ['P', 'Protected']; + } - $this->fmt->startRow(); - $this->fmt->sep($prop->isStatic() ? '::' : '->'); - $this->fmt->colDiv(); + if ($method->isPrivate()) { + $bubbles[] = ['!', 'Private']; + } - $bubbles = array(); - if($prop->isProtected()) - $bubbles[] = array('P', 'Protected'); + $this->fmt->bubbles($bubbles); - if($prop->isPrivate()) - $bubbles[] = array('!', 'Private'); + $this->fmt->colDiv(4 - count($bubbles)); - $this->fmt->bubbles($bubbles); + // is this method inherited? + $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName(); - $type = array('prop'); + $type = ['method']; - if($inherited) - $type[] = 'inherited'; + if ($inherited) { + $type[] = 'inherited'; + } - if($prop->isPrivate()) - $type[] = 'private'; + if ($method->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(); - } - } + $this->fmt->startContain($type); - // class methods - if($methods && !$this->hasInstanceTimedOut()){ + $name = $method->name; + if ($method->returnsReference()) { + $name = "&{$name}"; + } - $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'); + $this->fromReflector($method, $name, $reflector); - if($method->isFinal()) - $bubbles[] = array('F', 'Final'); + $paramCom = $method->isInternal() ? [] : static::parseComment($method->getDocComment(), 'tags'); + $paramCom = empty($paramCom['param']) ? [] : $paramCom['param']; + $paramCount = $method->getNumberOfParameters(); - if($method->isProtected()) - $bubbles[] = array('P', 'Protected'); + $this->fmt->sep('('); - if($method->isPrivate()) - $bubbles[] = array('!', 'Private'); + // process arguments + foreach ($method->getParameters() as $idx => $parameter) { + $meta = null; + $paramName = "\${$parameter->name}"; + $optional = $parameter->isOptional(); + $variadic = static::$env['is56'] && $parameter->isVariadic(); - $this->fmt->bubbles($bubbles); - - $this->fmt->colDiv(4 - count($bubbles)); + if ($parameter->isPassedByReference()) { + $paramName = "&{$paramName}"; + } - // is this method inherited? - $inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName(); + if ($variadic) { + $paramName = "...{$paramName}"; + } - $type = array('method'); + $type = ['param']; - if($inherited) - $type[] = 'inherited'; + if ($optional) { + $type[] = 'optional'; + } - if($method->isPrivate()) - $type[] = 'private'; + $this->fmt->startContain($type); - $this->fmt->startContain($type); + // attempt to build meta + foreach ($paramCom as $tag) { + list($pcTypes, $pcName, $pcDescription) = $tag; + if ($pcName !== $paramName) { + continue; + } - $name = $method->name; - if($method->returnsReference()) - $name = "&{$name}"; + $meta = ['title' => $pcDescription]; - $this->fromReflector($method, $name, $reflector); + if ($pcTypes) { + $meta['left'] = $pcTypes; + } - $paramCom = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags'); - $paramCom = empty($paramCom['param']) ? array() : $paramCom['param']; - $paramCount = $method->getNumberOfParameters(); + break; + } - $this->fmt->sep('('); + try { + $paramClass = $parameter->getClass(); + } catch (\Exception $e) { + // @see https://bugs.php.net/bug.php?id=32177&edit=1 + } - // process arguments - foreach($method->getParameters() as $idx => $parameter){ - $meta = null; - $paramName = "\${$parameter->name}"; - $optional = $parameter->isOptional(); - $variadic = static::$env['is56'] && $parameter->isVariadic(); + if (!empty($paramClass)) { + $this->fmt->startContain('hint'); + $this->fromReflector($paramClass, $paramClass->name); + $this->fmt->endContain(); + $this->fmt->sep(' '); + } - if($parameter->isPassedByReference()) - $paramName = "&{$paramName}"; + if ($parameter->isArray()) { + $this->fmt->text('hint', 'array'); + $this->fmt->sep(' '); + } - if($variadic) - $paramName = "...{$paramName}"; + $this->fmt->text('name', $paramName, $meta); - $type = array('param'); + if ($optional) { + $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; + $this->fmt->sep(' = '); - if($optional) - $type[] = 'optional'; + if (static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()) { + $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant'); - $this->fmt->startContain($type); - - // attempt to build meta - foreach($paramCom as $tag){ - list($pcTypes, $pcName, $pcDescription) = $tag; - if($pcName !== $paramName) - continue; + } else { + $this->evaluate($paramValue, true); + } + } - $meta = array('title' => $pcDescription); + $this->fmt->endContain(); - if($pcTypes) - $meta['left'] = $pcTypes; + if ($idx < $paramCount - 1) { + $this->fmt->sep(', '); + } + } + $this->fmt->sep(')'); + $this->fmt->endContain(); + $this->fmt->endRow(); + } + } - 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(' '); - } - - if($parameter->isArray()){ - $this->fmt->text('hint', 'array'); - $this->fmt->sep(' '); - } + unset($hashes[$hash]); + $this->fmt->endGroup(); - $this->fmt->text('name', $paramName, $meta); + $this->fmt->cacheLock($hash); + } - if($optional){ - $paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null; - $this->fmt->sep(' = '); + /** + * 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::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()){ - $this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant'); + if (static::strLen($expression) > 120) { + $expression = substr($expression, 0, 120).'...'; + } - }else{ - $this->evaluate($paramValue, true); - } - } + $this->fmt->sep('> '); - $this->fmt->endContain(); + if (strpos($expression, '(') === false) { + return $this->fmt->text('expTxt', $expression); + } - if($idx < $paramCount - 1) - $this->fmt->sep(', '); - } - $this->fmt->sep(')'); - $this->fmt->endContain(); - $this->fmt->endRow(); - } - } + $keywords = array_map('trim', explode('(', $expression, 2)); + $parts = []; - unset($hashes[$hash]); - $this->fmt->endGroup(); + // try to find out if this is a function + try { + $reflector = new \ReflectionFunction($keywords[0]); + $parts[] = [$keywords[0], $reflector, '']; - $this->fmt->cacheLock($hash); - } + } 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[] = [$cn[0], $reflector, '']; - /** - * 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){ + } catch (\Exception $e) { + $reflector = null; + $parts[] = $cn[0]; + } - if($expression === null) - return; + // class name... + try { + $reflector = new \ReflectionClass($cn[1]); + $parts[] = [$cn[1], $reflector, ' ']; - if(static::strLen($expression) > 120) - $expression = substr($expression, 0, 120) . '...'; + } catch (\Exception $e) { + $reflector = null; + $parts[] = $cn[1]; + } - $this->fmt->sep('> '); + } else { - if(strpos($expression, '(') === false) - return $this->fmt->text('expTxt', $expression); + // we can only linkify methods called statically + if (strpos($keywords[0], '::') === false) { + return $this->fmt->text('expTxt', $expression); + } - $keywords = array_map('trim', explode('(', $expression, 2)); - $parts = array(); + $cn = explode('::', $keywords[0], 2); - // try to find out if this is a function - try{ - $reflector = new \ReflectionFunction($keywords[0]); - $parts[] = array($keywords[0], $reflector, ''); - - }catch(\Exception $e){ + // attempt to linkify class name + try { + $reflector = new \ReflectionClass($cn[0]); + $parts[] = [$cn[0], $reflector, '']; - if(stripos($keywords[0], 'new ') === 0){ - $cn = explode(' ' , $keywords[0], 2); + } catch (\Exception $e) { + $reflector = null; + $parts[] = $cn[0]; + } - // linkify 'new keyword' (as constructor) - try{ - $reflector = new \ReflectionMethod($cn[1], '__construct'); - $parts[] = array($cn[0], $reflector, ''); + // perhaps it's a static class method; try to linkify method + try { + $reflector = new \ReflectionMethod($cn[0], $cn[1]); + $parts[] = [$cn[1], $reflector, '::']; - }catch(\Exception $e){ - $reflector = null; - $parts[] = $cn[0]; - } + } catch (\Exception $e) { + $reflector = null; + $parts[] = $cn[1]; + } + } + } - // class name... - try{ - $reflector = new \ReflectionClass($cn[1]); - $parts[] = array($cn[1], $reflector, ' '); + $parts[] = "({$keywords[1]}"; - }catch(\Exception $e){ - $reflector = null; - $parts[] = $cn[1]; - } + foreach ($parts as $element) { + if (!is_array($element)) { + $this->fmt->text('expTxt', $element); + continue; + } - }else{ + list($text, $reflector, $prefix) = $element; - // we can only linkify methods called statically - if(strpos($keywords[0], '::') === false) - return $this->fmt->text('expTxt', $expression); + if ($prefix !== '') { + $this->fmt->text('expTxt', $prefix); + } - $cn = explode('::', $keywords[0], 2); + $this->fromReflector($reflector, $text); + } - // attempt to linkify class name - try{ - $reflector = new \ReflectionClass($cn[0]); - $parts[] = array($cn[0], $reflector, ''); + } - }catch(\Exception $e){ - $reflector = null; - $parts[] = $cn[0]; - } + /** + * 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); + } - // perhaps it's a static class method; try to linkify method - try{ - $reflector = new \ReflectionMethod($cn[0], $cn[1]); - $parts[] = array($cn[1], $reflector, '::'); + /** + * 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); + } - }catch(\Exception $e){ - $reflector = null; - $parts[] = $cn[1]; - } - } } - $parts[] = "({$keywords[1]}"; + /** + * 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) + { + } - foreach($parts as $element){ - if(!is_array($element)){ - $this->fmt->text('expTxt', $element); - continue; - } + /** + * Generate container ending token + */ + public function endContain() + { + } - list($text, $reflector, $prefix) = $element; + /** + * Generate empty group token + * + * @param string $prefix + */ + public function emptyGroup($prefix = '') + { + } - if($prefix !== '') - $this->fmt->text('expTxt', $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 = '') + { + } - $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 + /** + * Generate group ending token + */ + public function endGroup() + { + } - /** - * Actual output - * - * @var string - */ - $out = '', + /** + * Generate section title + * + * @param string $title + */ + public function sectionTitle($title) + { + } - /** - * 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(), + /** + * Generate row start token + */ + public function startRow() + { + } - /** - * 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(), + /** + * Generate row ending token + */ + public function endRow() + { + } - /** - * Map of used HTML tag and attributes - * - * @var string - */ - $def = array(); + /** + * 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() + { + } - protected static + /** + * Input expression end + */ + public function endExp() + { + } - /** - * Instance counter - * - * @var int - */ - $counter = 0, + /** + * Root starting token + */ + public function startRoot() + { + } - /** - * Tracks style/jscript inclusion state - * - * @var bool - */ - $didAssets = false; - - - public function __construct(){ - - if(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', - ); + /** + * 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 = [], + + /** + * 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 = [], + + /** + * Map of used HTML tag and attributes + * + * @var string + */ + $def = []; + + protected static + + /** + * Instance counter + * + * @var int + */ + $counter = 0, + + /** + * Tracks style/jscript inclusion state + * + * @var bool + */ + $didAssets = false; + + public function __construct() + { + + if (ref::config('validHtml')) { + + $this->def = [ + '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 = [ + '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 flush() + { + print $this->out; + $this->out = ''; + $this->cache = []; + $this->tips = []; + } + public function didCache($id) + { - public function didCache($id){ + if (!isset($this->cache[$id])) { + $this->cache[$id] = []; + $this->cache[$id][] = strlen($this->out); + return false; + } - 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; + } - 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; + } - $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 !== ' ' ? '' . static::escape($label) . '' : $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" => '\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', - )); - } + public function cacheLock($id) + { + $this->cache[$id][] = strlen($this->out) - $this->cache[$id][0]; + } - // generate tooltip reference (probably the slowest part of the code ;) - if($meta !== null){ - $tipIdx = array_search($meta, $this->tips, true); + public function sep($label = ' ') + { + $this->out .= $label !== ' ' ? ''.static::escape($label).'' : $label; + } - if($tipIdx === false) - $tipIdx = array_push($this->tips, $meta) - 1; + public function text($type, $text = null, $meta = null, $uri = null) + { - $tip = " {$this->def['tipRef']}=\"{$tipIdx}\""; - //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx); - } + if (!is_array($type)) { + $type = (array)$type; + } - // wrap text in a link? - if($uri !== null) - $text = '' . $text . ''; + $tip = ''; + $text = ($text !== null) ? static::escape($text) : static::escape($type[0]); + + if (in_array('special', $type)) { + $text = strtr($text, [ + "\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', + ]); + } - $typeStr = ''; - foreach($type as $part) - $typeStr .= " data-{$part}"; + // generate tooltip reference (probably the slowest part of the code ;) + if ($meta !== null) { + $tipIdx = array_search($meta, $this->tips, true); - $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}def['base']}>"; - //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s', $this->def['base'], $typeStr, $tip, $text); - } + if ($tipIdx === false) { + $tipIdx = array_push($this->tips, $meta) - 1; + } - public function startContain($type, $label = false){ + $tip = " {$this->def['tipRef']}=\"{$tipIdx}\""; + //$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx); + } - if(!is_array($type)) - $type = (array)$type; + // wrap text in a link? + if ($uri !== null) { + $text = ''.$text.''; + } - if($label) - $this->out .= '
'; + $typeStr = ''; + foreach ($type as $part) { + $typeStr .= " data-{$part}"; + } - $typeStr = ''; - foreach($type as $part) - $typeStr .= " data-{$part}"; + $this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}def['base']}>"; + //$this->out .= sprintf('<%1$s%2$s %3$s>%4$s', $this->def['base'], $typeStr, $tip, $text); + } - $this->out .= "<{$this->def['base']}{$typeStr}>"; + public function startContain($type, $label = false) + { - if($label) - $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}def['base']}>"; - } + if (!is_array($type)) { + $type = (array)$type; + } - public function endContain(){ - $this->out .= "def['base']}>"; - } + if ($label) { + $this->out .= '
'; + } - public function emptyGroup($prefix = ''){ + $typeStr = ''; + foreach ($type as $part) { + $typeStr .= " data-{$part}"; + } - if($prefix !== '') - $prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "def['base']}>"; - - $this->out .= "({$prefix})"; - } + $this->out .= "<{$this->def['base']}{$typeStr}>"; + if ($label) { + $this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}def['base']}>"; + } + } - public function startGroup($prefix = ''){ + public function endContain() + { + $this->out .= "def['base']}>"; + } - $maxDepth = ref::config('maxDepth'); + public function emptyGroup($prefix = '') + { - if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){ - $this->emptyGroup('...'); - return false; - } + if ($prefix !== '') { + $prefix = "<{$this->def['base']} {$this->def['gLabel']}>".static::escape($prefix)."def['base']}>"; + } - $this->level++; + $this->out .= "({$prefix})"; + } - $expLvl = 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) . "def['base']}>"; - - $this->out .= "({$prefix}<{$this->def['base']} data-toggle{$exp}>def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>"; + public function startGroup($prefix = '') + { - return true; - } + $maxDepth = ref::config('maxDepth'); - public function endGroup(){ - $this->out .= "def['base']}>def['base']}>)"; - $this->level--; - } - - public function sectionTitle($title){ - $this->out .= "def['base']}><{$this->def['base']} data-tHead>{$title}def['base']}><{$this->def['base']} {$this->def['table']}>"; - } + if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) { + $this->emptyGroup('...'); + return false; + } - public function startRow(){ - $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>"; - } + $this->level++; - public function endRow(){ - $this->out .= "def['base']}>def['base']}>"; - } - - public function colDiv($padLen = null){ - $this->out .= "def['base']}><{$this->def['base']} {$this->def['cell']}>"; - } + $expLvl = ref::config('expLvl'); + $exp = ($expLvl < 0) || (($expLvl > 0) && ($this->level <= $expLvl)) ? ' data-exp' : ''; - public function bubbles(array $items){ + if ($prefix !== '') { + $prefix = "<{$this->def['base']} {$this->def['gLabel']}>".static::escape($prefix)."def['base']}>"; + } - if(!$items) - return; + $this->out .= "({$prefix}<{$this->def['base']} data-toggle{$exp}>def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>"; - $this->out .= "<{$this->def['base']} data-mod>"; + return true; + } - foreach($items as $info) - $this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]); + public function endGroup() + { + $this->out .= "def['base']}>def['base']}>)"; + $this->level--; + } - $this->out .= "def['base']}>"; - } - - public function startExp(){ - $this->out .= "<{$this->def['base']} data-input>"; - } + public function sectionTitle($title) + { + $this->out .= "def['base']}><{$this->def['base']} data-tHead>{$title}def['base']}><{$this->def['base']} {$this->def['table']}>"; + } - public function endExp(){ - if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())) - $this->out .= "<{$this->def['base']} data-backtrace>{$trace['file']}:{$trace['line']}def['base']}>"; + public function startRow() + { + $this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>"; + } - $this->out .= "def['base']}><{$this->def['base']} data-output>"; - } + public function endRow() + { + $this->out .= "def['base']}>def['base']}>"; + } - public function startRoot(){ - $this->out .= '
' . static::getAssets() . '
'; - } + public function colDiv($padLen = null) + { + $this->out .= "def['base']}><{$this->def['base']} {$this->def['cell']}>"; + } - public function endRoot(){ - $this->out .= "def['base']}>"; + public function bubbles(array $items) + { - // process tooltips - $tipHtml = ''; - foreach($this->tips as $idx => $meta){ + if (!$items) { + return; + } - $tip = ''; - if(!is_array($meta)) - $meta = array('title' => $meta); + $this->out .= "<{$this->def['base']} data-mod>"; - $meta += array( - 'title' => '', - 'left' => '', - 'description' => '', - 'tags' => array(), - 'sub' => array(), - ); + foreach ($items as $info) { + $this->out .= $this->text('mod-'.strtolower($info[1]), $info[0], $info[1]); + } - $meta = static::escape($meta); - $cols = array(); + $this->out .= "def['base']}>"; + } - if($meta['left']) - $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}def['base']}>"; + public function startExp() + { + $this->out .= "<{$this->def['base']} data-input>"; + } - $title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}def['base']}>" : ''; - $desc = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}def['base']}>" : ''; - $tags = ''; + public function endExp() + { + if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) { + $this->out .= "<{$this->def['base']} data-backtrace>{$trace['file']}:{$trace['line']}def['base']}>"; + } - 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("def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value; - $tags .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}def['base']}>def['base']}>"; - } - } - - if($tags) - $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}def['base']}>"; - - if($title || $desc || $tags) - $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}def['base']}>"; - - if($cols) - $tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "def['base']}>"; - - $sub = ''; - foreach($meta['sub'] as $line) - $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "def['base']}>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}def['base']}>def['base']}>def['base']}>"; + $this->out .= "def['base']}><{$this->def['base']} data-output>"; + } - if($tip) - $this->out .= "<{$this->def['tip']}>{$tip}def['tip']}>"; - } + public function startRoot() + { + $this->out .= '
'.static::getAssets().'
'; + } - if(($timeout = ref::getTimeoutPoint()) > 0) - $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fsdef['base']}>", $timeout); + public function endRoot() + { + $this->out .= "def['base']}>"; + + // process tooltips + $tipHtml = ''; + foreach ($this->tips as $idx => $meta) { + + $tip = ''; + if (!is_array($meta)) { + $meta = ['title' => $meta]; + } + + $meta += [ + 'title' => '', + 'left' => '', + 'description' => '', + 'tags' => [], + 'sub' => [], + ]; + + $meta = static::escape($meta); + $cols = []; + + if ($meta['left']) { + $cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}def['base']}>"; + } + + $title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}def['base']}>" : ''; + $desc = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}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("def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value; + $tags .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}def['base']}>def['base']}>"; + } + } - $this->out .= '
'; - } + if ($tags) { + $tags = "<{$this->def['base']} {$this->def['table']}>{$tags}def['base']}>"; + } + if ($title || $desc || $tags) { + $cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}def['base']}>"; + } + if ($cols) { + $tip = "<{$this->def['base']} {$this->def['row']}>".implode('', $cols)."def['base']}>"; + } - /** - * 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 ''; + $sub = ''; + foreach ($meta['sub'] as $line) { + $sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>". + implode("def['base']}><{$this->def['base']} {$this->def['cell']}>", $line)."def['base']}>def['base']}>"; + } - ob_start(); + if ($sub) { + $tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}def['base']}>def['base']}>def['base']}>"; + } - if(ref::config('stylePath') !== false){ - ?> - - out .= "<{$this->def['tip']}>{$tip}def['tip']}>"; + } + } - if(ref::config('scriptPath') !== false){ - ?> - - 0) { + $this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fsdef['base']}>", $timeout); + } - // 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); + $this->out .= '
'; + } - static::$didAssets = true; - return $output; - } + /** + * 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(); - /** - * 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); - } + if (ref::config('stylePath') !== false) { + ?> + + + + out; + $this->out = ''; + $this->cache = []; + } - /** - * Tracks current nesting level - * - * @var int - */ - $level = 0, + public function sep($label = ' ') + { + $this->out .= $label; + } - /** - * Current indenting level - * - * @var int - */ - $indent = 0, + public function text($type, $text = null, $meta = null, $uri = null) + { - $lastIdx = 0, - $lastLineSt = 0, - $levelPad = array(0); + if (!is_array($type)) { + $type = (array)$type; + } + if ($text === null) { + $text = $type[0]; + } + if (in_array('special', $type, true)) { + $text = strtr($text, [ + "\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; + } - public function flush(){ - print $this->out; - $this->out = ''; - $this->cache = array(); - } + $formatMap = [ + '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 = ''; + } - public function sep($label = ' '){ - $this->out .= $label; - } + $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text; + } - public function text($type, $text = null, $meta = null, $uri = null){ + public function startContain($type, $label = false) + { - if(!is_array($type)) - $type = (array)$type; + if (!is_array($type)) { + $type = (array)$type; + } - if($text === null) - $text = $type[0]; + if ($label) { + $this->out .= "\n".str_repeat(' ', $this->indent + $this->levelPad[$this->level])."┗ {$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', - )); + public function emptyGroup($prefix = '') + { + $this->out .= "({$prefix})"; + } - $this->out .= $text; - return; - } + public function startGroup($prefix = '') + { - $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]', - ); + $maxDepth = ref::config('maxDepth'); - if(!is_string($meta)) - $meta = ''; + if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) { + $this->emptyGroup('...'); + return false; + } - $this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text; - } + $this->level++; + $this->out .= '('; - public function startContain($type, $label = false){ + $this->indent += $this->levelPad[$this->level - 1]; + return true; + } - if(!is_array($type)) - $type = (array)$type; + public function endGroup() + { + $this->out .= "\n".str_repeat(' ', $this->indent).')'; + $this->indent -= $this->levelPad[$this->level - 1]; + $this->level--; + } - if($label) - $this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ "; - } + 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 emptyGroup($prefix = ''){ - $this->out .= "({$prefix})"; - } + public function startRow() + { + $this->out .= "\n ".str_repeat(' ', $this->indent); + $this->lastLineSt = strlen($this->out); + } - public function startGroup($prefix = ''){ + public function endRow() + { + } - $maxDepth = ref::config('maxDepth'); + public function colDiv($padLen = null) + { + $padLen = ($padLen !== null) ? $padLen + 1 : 1; + $this->out .= str_repeat(' ', $padLen); - if(($maxDepth > 0) && (($this->level + 1) > $maxDepth)){ - $this->emptyGroup('...'); - return false; - } + $this->lastIdx = strlen($this->out); + $this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2; + } - $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; - } + public function bubbles(array $items) + { - $this->out .= '<'; + if (!$items) { + $this->out .= ' '; + return; + } - foreach($items as $item) - $this->out .= $item[0]; + $this->out .= '<'; - $this->out .= '>'; - } + foreach ($items as $item) { + $this->out .= $item[0]; + } - public function endExp(){ + $this->out .= '>'; + } - if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())) - $this->out .= ' - ' . $trace['file'] . ':' . $trace['line']; + public function endExp() + { - $this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n"; - } - - public function startRoot(){ - $this->out .= "\n\n"; + if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) { + $this->out .= ' - '.$trace['file'].':'.$trace['line']; + } - } + $this->out .= "\n".str_repeat('=', strlen($this->out))."\n"; + } - public function endRoot(){ - $this->out .= "\n"; - if(($timeout = ref::getTimeoutPoint()) > 0) - $this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout); - } + public function startRoot() + { + $this->out .= "\n\n"; -} + } + public function endRoot() + { + $this->out .= "\n"; + if (($timeout = 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{ + /** + * 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 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 startExp() + { + $this->out .= "\x1b[1;44;96m "; + } - public function endExp(){ - if(ref::config('showBacktrace') && ($trace = ref::getBacktrace())) - $this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line']; + public function endExp() + { + if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) { + $this->out .= "\x1b[0m\x1b[44;36m ".$trace['file'].':'.$trace['line']; + } - $this->out .= " \x1b[0m\n"; - } + $this->out .= " \x1b[0m\n"; + } - public function endRoot(){ - $this->out .= "\n"; - if(($timeout = ref::getTimeoutPoint()) > 0) - $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout); - } + public function endRoot() + { + $this->out .= "\n"; + if (($timeout = ref::getTimeoutPoint()) > 0) { + $this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout); + } + } -} + }