3133 lines
88 KiB
PHP
3133 lines
88 KiB
PHP
<?php
|
|
|
|
/**
|
|
* Shortcut to ref, HTML mode
|
|
*
|
|
* @param mixed $args
|
|
* @return void|string
|
|
*/
|
|
function r() {
|
|
// arguments passed to this function
|
|
$args = func_get_args();
|
|
|
|
// options (operators) gathered by the expression parser;
|
|
// this variable gets passed as reference to getInputExpressions(), which will store the operators in it
|
|
$options = array();
|
|
|
|
// names of the arguments that were passed to this function
|
|
$expressions = ref::getInputExpressions($options);
|
|
$capture = in_array('@', $options, true);
|
|
|
|
// something went wrong while trying to parse the source expressions?
|
|
// if so, silently ignore this part and leave out the expression info
|
|
if(func_num_args() !== count($expressions)) {
|
|
$expressions = null;
|
|
}
|
|
|
|
// use HTML formatter only if we're not in CLI mode, or if return was requested
|
|
$format = (php_sapi_name() !== 'cli') || $capture ? 'html' : 'cliText';
|
|
|
|
// IE goes funky if there's no doctype
|
|
if(!$capture && ($format === 'html') && !headers_sent() && (!ob_get_level() || ini_get('output_buffering'))) {
|
|
echo('<!DOCTYPE HTML><html><head><title>REF</title><meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /></head><body>');
|
|
}
|
|
|
|
$ref = new ref($format);
|
|
|
|
// Observium specific paths
|
|
ref::config('stylePath', $GLOBALS['config']['html_dir'] . '/css/ref.css');
|
|
ref::config('scriptPath', $GLOBALS['config']['html_dir'] . '/js/ref.js');
|
|
|
|
if ($capture) {
|
|
ob_start();
|
|
}
|
|
|
|
foreach ($args as $index => $arg) {
|
|
$ref->query($arg, $expressions ? $expressions[$index] : null);
|
|
}
|
|
|
|
// return the results if this function was called with the error suppression operator
|
|
if ($capture) {
|
|
return ob_get_clean();
|
|
}
|
|
|
|
// stop the script if this function was called with the bitwise not operator
|
|
if (in_array('~', $options, true) && ($format === 'html')) {
|
|
echo('</body></html>');
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* 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 (func_num_args() !== count($expressions)) {
|
|
$expressions = null;
|
|
}
|
|
|
|
if (!headers_sent()) {
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
}
|
|
|
|
if ($capture) {
|
|
ob_start();
|
|
}
|
|
|
|
foreach ($args as $index => $arg) {
|
|
$ref->query($arg, $expressions ? $expressions[$index] : null);
|
|
}
|
|
|
|
if ($capture) {
|
|
return ob_get_clean();
|
|
}
|
|
|
|
if (in_array('~', $options, true)) {
|
|
exit(0);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* REF is a nicer alternative to PHP's print_r() / var_dump().
|
|
*
|
|
* @version 1.0
|
|
* @author digitalnature - http://digitalnature.eu
|
|
*/
|
|
class ref {
|
|
|
|
const
|
|
|
|
MARKER_KEY = '_phpRefArrayMarker_';
|
|
|
|
|
|
protected static
|
|
|
|
/**
|
|
* CPU time used for processing
|
|
*
|
|
* @var array
|
|
*/
|
|
$time = 0,
|
|
|
|
/**
|
|
* Configuration (+ default values)
|
|
*
|
|
* @var array
|
|
*/
|
|
$config = array(
|
|
|
|
// initially expanded levels (for HTML mode only)
|
|
'expLvl' => 1,
|
|
|
|
// depth limit (0 = no limit);
|
|
// this is not related to recursion
|
|
'maxDepth' => 6,
|
|
|
|
// show the place where r() has been called from
|
|
'showBacktrace' => true,
|
|
|
|
// if passed from high level function -> debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS)
|
|
'Backtrace' => NULL,
|
|
|
|
// display iterator contents
|
|
'showIteratorContents' => false,
|
|
|
|
// display extra information about resources
|
|
'showResourceInfo' => true,
|
|
|
|
// display method and parameter list on objects
|
|
'showMethods' => true,
|
|
|
|
// display private properties / methods
|
|
'showPrivateMembers' => false,
|
|
|
|
// peform string matches (date, file, functions, classes, json, serialized data, regex etc.)
|
|
// note: seriously slows down queries on large amounts of data
|
|
'showStringMatches' => true,
|
|
|
|
// shortcut functions used to access the query method below;
|
|
// if they are namespaced, the namespace must be present as well (methods are not supported)
|
|
'shortcutFunc' => array('r', 'rt'),
|
|
|
|
// custom/external formatters (as associative array: format => className)
|
|
'formatters' => array(),
|
|
|
|
// stylesheet path (for HTML only);
|
|
// 'false' means no styles
|
|
'stylePath' => '{:dir}/ref.css',
|
|
|
|
// javascript path (for HTML only);
|
|
// 'false' means no js
|
|
'scriptPath' => '{:dir}/ref.js',
|
|
|
|
// display url info via cURL
|
|
'showUrls' => false,
|
|
|
|
// stop evaluation after this amount of time (seconds)
|
|
'timeout' => 10,
|
|
|
|
// whether to produce W3c-valid HTML,
|
|
// or unintelligible, but optimized markup that takes less space
|
|
'validHtml' => false,
|
|
),
|
|
|
|
/**
|
|
* Some environment variables
|
|
* used to determine feature support
|
|
*
|
|
* @var array
|
|
*/
|
|
$env = array(),
|
|
|
|
/**
|
|
* Timeout point
|
|
*
|
|
* @var bool
|
|
*/
|
|
$timeout = -1,
|
|
|
|
$debug = array(
|
|
'cacheHits' => 0,
|
|
'objects' => 0,
|
|
'arrays' => 0,
|
|
'scalars' => 0,
|
|
);
|
|
|
|
|
|
protected
|
|
|
|
/**
|
|
* Output formatter of this instance
|
|
*
|
|
* @var RFormatter
|
|
*/
|
|
$fmt = null,
|
|
|
|
/**
|
|
* Start time of the current instance
|
|
*
|
|
* @var float
|
|
*/
|
|
$startTime = 0,
|
|
|
|
/**
|
|
* Internally created objects
|
|
*
|
|
* @var SplObjectStorage
|
|
*/
|
|
$intObjects = null;
|
|
|
|
|
|
/**
|
|
* Constructor
|
|
*
|
|
* @param string|RFormatter $format Output format ID, or formatter instance defaults to 'html'
|
|
*/
|
|
public function __construct($format = 'html') {
|
|
|
|
static $didIni = false;
|
|
|
|
if (!$didIni) {
|
|
$didIni = true;
|
|
foreach (array_keys(static::$config) as $key) {
|
|
$iniVal = get_cfg_var('ref.' . $key);
|
|
if ($iniVal !== false) {
|
|
static::$config[$key] = $iniVal;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
if ($format instanceof RFormatter) {
|
|
$this->fmt = $format;
|
|
|
|
} else {
|
|
$format = isset(static::$config['formatters'][$format]) ? static::$config['formatters'][$format] : 'R' . ucfirst($format) . 'Formatter';
|
|
|
|
if (!class_exists($format, false)) {
|
|
throw new \Exception(sprintf('%s class not found', $format));
|
|
}
|
|
|
|
$this->fmt = new $format();
|
|
}
|
|
|
|
if (static::$env) {
|
|
return;
|
|
}
|
|
|
|
static::$env = array(
|
|
|
|
// php 5.4+ ?
|
|
'is54' => version_compare(PHP_VERSION, '5.4') >= 0,
|
|
|
|
// php 5.4.6+ ?
|
|
'is546' => version_compare(PHP_VERSION, '5.4.6') >= 0,
|
|
|
|
// php 5.6+
|
|
'is56' => version_compare(PHP_VERSION, '5.6') >= 0,
|
|
|
|
// php 7.0+ ?
|
|
'is7' => version_compare(PHP_VERSION, '7.0') >= 0,
|
|
|
|
// curl extension running?
|
|
'curlActive' => function_exists('curl_version'),
|
|
|
|
// is the 'mbstring' extension active?
|
|
'mbStr' => function_exists('mb_detect_encoding'),
|
|
|
|
// @see: https://bugs.php.net/bug.php?id=52469
|
|
'supportsDate' => (strncasecmp(PHP_OS, 'WIN', 3) !== 0) || (version_compare(PHP_VERSION, '5.3.10') >= 0),
|
|
);
|
|
}
|
|
|
|
|
|
/**
|
|
* Enforce proper use of this class
|
|
*
|
|
* @param string $name
|
|
*/
|
|
public function __get($name) {
|
|
throw new \Exception(sprintf('No such property: %s', $name));
|
|
}
|
|
|
|
|
|
/**
|
|
* Enforce proper use of this class
|
|
*
|
|
* @param string $name
|
|
* @param mixed $value
|
|
*/
|
|
public function __set($name, $value) {
|
|
throw new \Exception(sprintf('Cannot set %s. Not allowed', $name));
|
|
}
|
|
|
|
|
|
/**
|
|
* Generate structured information about a variable/value/expression (subject)
|
|
*
|
|
* Output is flushed to the screen
|
|
*
|
|
* @param mixed $subject
|
|
* @param string $expression
|
|
*/
|
|
public function query($subject, $expression = null) {
|
|
|
|
if (static::$timeout > 0) {
|
|
return;
|
|
}
|
|
|
|
$this->startTime = microtime(true);
|
|
|
|
$this->intObjects = new \SplObjectStorage();
|
|
|
|
$this->fmt->startRoot();
|
|
$this->fmt->startExp();
|
|
$this->evaluateExp($expression);
|
|
$this->fmt->endExp();
|
|
$this->evaluate($subject);
|
|
$this->fmt->endRoot();
|
|
$this->fmt->flush();
|
|
|
|
static::$time += microtime(true) - $this->startTime;
|
|
}
|
|
|
|
|
|
/**
|
|
* Executes a function the given number of times and returns the elapsed time.
|
|
*
|
|
* Keep in mind that the returned time includes function call overhead (including
|
|
* microtime calls) x iteration count. This is why this is better suited for
|
|
* determining which of two or more functions is the fastest, rather than
|
|
* finding out how fast is a single function.
|
|
*
|
|
* @param int $iterations Number of times the function will be executed
|
|
* @param callable $function Function to execute
|
|
* @param mixed &$output If given, last return value will be available in this variable
|
|
* @return double Elapsed time
|
|
*/
|
|
public static function timeFunc($iterations, $function, &$output = null) {
|
|
|
|
$time = 0;
|
|
|
|
for ($i = 0; $i < $iterations; $i++) {
|
|
$start = microtime(true);
|
|
$output = call_user_func($function);
|
|
$time += microtime(true) - $start;
|
|
}
|
|
|
|
return round($time, 4);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Timer utility
|
|
*
|
|
* First call of this function will start the timer.
|
|
* The second call will stop the timer and return the elapsed time
|
|
* since the timer started.
|
|
*
|
|
* Multiple timers can be controlled simultaneously by specifying a timer ID.
|
|
*
|
|
* @since 1.0
|
|
* @param int $id Timer ID, optional
|
|
* @param int $precision Precision of the result, optional
|
|
* @return void|double Elapsed time, or void if the timer was just started
|
|
*/
|
|
public static function timer($id = 1, $precision = 4) {
|
|
|
|
static
|
|
$timers = array();
|
|
|
|
// check if this timer was started, and display the elapsed time if so
|
|
if (isset($timers[$id])) {
|
|
$elapsed = round(microtime(true) - $timers[$id], $precision);
|
|
unset($timers[$id]);
|
|
return $elapsed;
|
|
}
|
|
|
|
// ID doesn't exist, start new timer
|
|
$timers[$id] = microtime(true);
|
|
}
|
|
|
|
|
|
/**
|
|
* Parses a DocBlock comment into a data structure.
|
|
*
|
|
* @link http://pear.php.net/manual/en/standards.sample.php
|
|
* @param string $comment DocBlock comment (must start with /**)
|
|
* @param string|null $key Field to return (optional)
|
|
* @return array|string|null Array containing all fields, array/string with the contents of
|
|
* the requested field, or null if the comment is empty/invalid
|
|
*/
|
|
public static function parseComment($comment, $key = null) {
|
|
|
|
$description = '';
|
|
$tags = array();
|
|
$tag = null;
|
|
$pointer = '';
|
|
$padding = 0;
|
|
$comment = preg_split('/\r\n|\r|\n/', '* ' . trim($comment, "/* \t\n\r\0\x0B"));
|
|
|
|
// analyze each line
|
|
foreach ($comment as $line) {
|
|
|
|
// drop any wrapping spaces
|
|
$line = trim($line);
|
|
|
|
// drop "* "
|
|
if ($line !== '') {
|
|
$line = substr($line, 2);
|
|
}
|
|
|
|
if (strpos($line, '@') !== 0) {
|
|
|
|
// preserve formatting of tag descriptions,
|
|
// because they may span across multiple lines
|
|
if ($tag !== null) {
|
|
$trimmed = trim($line);
|
|
|
|
if ($padding !== 0) {
|
|
$trimmed = static::strPad($trimmed, static::strLen($line) - $padding, ' ', STR_PAD_LEFT);
|
|
} else {
|
|
$padding = static::strLen($line) - static::strLen($trimmed);
|
|
}
|
|
|
|
$pointer .= "\n{$trimmed}";
|
|
continue;
|
|
}
|
|
|
|
// tag definitions have not started yet; assume this is part of the description text
|
|
$description .= "\n{$line}";
|
|
continue;
|
|
}
|
|
|
|
$padding = 0;
|
|
$parts = explode(' ', $line, 2);
|
|
|
|
// invalid tag? (should we include it as an empty array?)
|
|
if (!isset($parts[1])) {
|
|
continue;
|
|
}
|
|
|
|
$tag = substr($parts[0], 1);
|
|
$line = ltrim($parts[1]);
|
|
|
|
// tags that have a single component (eg. link, license, author, throws...);
|
|
// note that @throws may have 2 components, however most people use it like "@throws ExceptionClass if whatever...",
|
|
// which, if broken into two values, leads to an inconsistent description sentence
|
|
if (!in_array($tag, array('global', 'param', 'return', 'var'))) {
|
|
$tags[$tag][] = $line;
|
|
end($tags[$tag]);
|
|
$pointer = &$tags[$tag][key($tags[$tag])];
|
|
continue;
|
|
}
|
|
|
|
// tags with 2 or 3 components (var, param, return);
|
|
$parts = explode(' ', $line, 2);
|
|
$parts[1] = isset($parts[1]) ? ltrim($parts[1]) : null;
|
|
$lastIdx = 1;
|
|
|
|
// expecting 3 components on the 'param' tag: type varName varDescription
|
|
if ($tag === 'param') {
|
|
$lastIdx = 2;
|
|
if ($parts[1][0] === '&' || $parts[1][0] === '$') {
|
|
$line = ltrim(array_pop($parts));
|
|
$parts = array_merge($parts, explode(' ', $line, 2));
|
|
$parts[2] = isset($parts[2]) ? ltrim($parts[2]) : null;
|
|
} else {
|
|
$parts[2] = $parts[1];
|
|
$parts[1] = null;
|
|
}
|
|
}
|
|
|
|
$tags[$tag][] = $parts;
|
|
end($tags[$tag]);
|
|
$pointer = &$tags[$tag][key($tags[$tag])][$lastIdx];
|
|
}
|
|
|
|
// split title from the description texts at the nearest 2x new-line combination
|
|
// (note: loose check because 0 isn't valid as well)
|
|
if (strpos($description, "\n\n")) {
|
|
list($title, $description) = explode("\n\n", $description, 2);
|
|
|
|
// if we don't have 2 new lines, try to extract first sentence
|
|
} else {
|
|
// in order for a sentence to be considered valid,
|
|
// the next one must start with an uppercase letter
|
|
$sentences = preg_split('/(?<=[.?!])\s+(?=[A-Z])/', $description, 2, PREG_SPLIT_NO_EMPTY);
|
|
|
|
// failed to detect a second sentence? then assume there's only title and no description text
|
|
$title = isset($sentences[0]) ? $sentences[0] : $description;
|
|
$description = isset($sentences[1]) ? $sentences[1] : '';
|
|
}
|
|
|
|
$title = ltrim($title);
|
|
$description = ltrim($description);
|
|
|
|
$data = compact('title', 'description', 'tags');
|
|
|
|
if (!array_filter($data)) {
|
|
return null;
|
|
}
|
|
|
|
if ($key !== null) {
|
|
return isset($data[$key]) ? $data[$key] : null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
|
|
/**
|
|
* Split a regex into its components
|
|
*
|
|
* Based on "Regex Colorizer" by Steven Levithan (this is a translation from javascript)
|
|
*
|
|
* @link https://github.com/slevithan/regex-colorizer
|
|
* @link https://github.com/symfony/Finder/blob/master/Expression/Regex.php#L64-74
|
|
* @param string $pattern
|
|
* @return array
|
|
*/
|
|
public static function splitRegex($pattern) {
|
|
|
|
// detection attempt code from the Symfony Finder component
|
|
$maybeValid = false;
|
|
if (preg_match('/^(.{3,}?)([imsxuADU]*)$/', $pattern, $m)) {
|
|
$start = substr($m[1], 0, 1);
|
|
$end = substr($m[1], -1);
|
|
|
|
if (($start === $end && !preg_match('/[*?[:alnum:] \\\\]/', $start)) || ($start === '{' && $end === '}')) {
|
|
$maybeValid = true;
|
|
}
|
|
}
|
|
|
|
if (!$maybeValid) {
|
|
throw new \Exception('Pattern does not appear to be a valid PHP regex');
|
|
}
|
|
|
|
$output = array();
|
|
$capturingGroupCount = 0;
|
|
$groupStyleDepth = 0;
|
|
$openGroups = array();
|
|
$lastIsQuant = false;
|
|
$lastType = 1; // 1 = none; 2 = alternator
|
|
$lastStyle = null;
|
|
|
|
preg_match_all('/\[\^?]?(?:[^\\\\\]]+|\\\\[\S\s]?)*]?|\\\\(?:0(?:[0-3][0-7]{0,2}|[4-7][0-7]?)?|[1-9][0-9]*|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)|\((?:\?[:=!]?)?|(?:[?*+]|\{[0-9]+(?:,[0-9]*)?\})\??|[^.?*+^${[()|\\\\]+|./', $pattern, $matches);
|
|
|
|
$matches = $matches[0];
|
|
|
|
$getTokenCharCode = function($token) {
|
|
if (strlen($token) > 1 && $token[0] === '\\') {
|
|
$t1 = substr($token, 1);
|
|
|
|
if (preg_match('/^c[A-Za-z]$/', $t1)) {
|
|
return strpos("ABCDEFGHIJKLMNOPQRSTUVWXYZ", strtoupper($t1[1])) + 1;
|
|
}
|
|
|
|
if (preg_match('/^(?:x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4})$/', $t1)) {
|
|
return intval(substr($t1, 1), 16);
|
|
}
|
|
|
|
if (preg_match('/^(?:[0-3][0-7]{0,2}|[4-7][0-7]?)$/', $t1)) {
|
|
return intval($t1, 8);
|
|
}
|
|
|
|
$len = strlen($t1);
|
|
|
|
if ($len === 1 && strpos('cuxDdSsWw', $t1) !== false) {
|
|
return null;
|
|
}
|
|
|
|
if ($len === 1) {
|
|
switch ($t1) {
|
|
case 'b': return 8;
|
|
case 'f': return 12;
|
|
case 'n': return 10;
|
|
case 'r': return 13;
|
|
case 't': return 9;
|
|
case 'v': return 11;
|
|
default: return $t1[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
return ($token !== '\\') ? $token[0] : null;
|
|
};
|
|
|
|
foreach ($matches as $m) {
|
|
|
|
if ($m[0] === '[') {
|
|
$lastCC = null;
|
|
$cLastRangeable = false;
|
|
$cLastType = 0; // 0 = none; 1 = range hyphen; 2 = short class
|
|
|
|
preg_match('/^(\[\^?)(]?(?:[^\\\\\]]+|\\\\[\S\s]?)*)(]?)$/', $m, $parts);
|
|
|
|
array_shift($parts);
|
|
list($opening, $content, $closing) = $parts;
|
|
|
|
if (!$closing) {
|
|
throw new \Exception('Unclosed character class');
|
|
}
|
|
|
|
preg_match_all('/[^\\\\-]+|-|\\\\(?:[0-3][0-7]{0,2}|[4-7][0-7]?|x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4}|c[A-Za-z]|[\S\s]?)/', $content, $ccTokens);
|
|
$ccTokens = $ccTokens[0];
|
|
$ccTokenCount = count($ccTokens);
|
|
$output[] = array('chr' => $opening);
|
|
|
|
foreach($ccTokens as $i => $cm) {
|
|
|
|
if ($cm[0] === '\\') {
|
|
if (preg_match('/^\\\\[cux]$/', $cm)) {
|
|
throw new \Exception('Incomplete regex token');
|
|
}
|
|
|
|
if (preg_match('/^\\\\[dsw]$/i', $cm)) {
|
|
$output[] = array('chr-meta' => $cm);
|
|
$cLastRangeable = ($cLastType !== 1);
|
|
$cLastType = 2;
|
|
|
|
} elseif ($cm === '\\') {
|
|
throw new \Exception('Incomplete regex token');
|
|
|
|
} else {
|
|
$output[] = array('chr-meta' => $cm);
|
|
$cLastRangeable = $cLastType !== 1;
|
|
$lastCC = $getTokenCharCode($cm);
|
|
}
|
|
|
|
} elseif($cm === '-') {
|
|
if ($cLastRangeable) {
|
|
$nextToken = ($i + 1 < $ccTokenCount) ? $ccTokens[$i + 1] : false;
|
|
|
|
if ($nextToken) {
|
|
$nextTokenCharCode = $getTokenCharCode($nextToken[0]);
|
|
|
|
if ((!is_null($nextTokenCharCode) && $lastCC > $nextTokenCharCode) || $cLastType === 2 || preg_match('/^\\\\[dsw]$/i', $nextToken[0])) {
|
|
throw new \Exception('Reversed or invalid range');
|
|
}
|
|
|
|
$output[] = array('chr-range' => '-');
|
|
$cLastRangeable = false;
|
|
$cLastType = 1;
|
|
|
|
} else {
|
|
$output[] = $closing ? array('chr' => '-') : array('chr-range' => '-');
|
|
}
|
|
|
|
} else {
|
|
$output[] = array('chr' => '-');
|
|
$cLastRangeable = ($cLastType !== 1);
|
|
}
|
|
|
|
} else {
|
|
$output[] = array('chr' => $cm);
|
|
$cLastRangeable = strlen($cm) > 1 || ($cLastType !== 1);
|
|
$lastCC = $cm[strlen($cm) - 1];
|
|
}
|
|
}
|
|
|
|
$output[] = array('chr' => $closing);
|
|
$lastIsQuant = true;
|
|
|
|
} elseif ($m[0] === '(') {
|
|
if (strlen($m) === 2) {
|
|
throw new \Exception('Invalid or unsupported group type');
|
|
}
|
|
|
|
if (strlen($m) === 1) {
|
|
$capturingGroupCount++;
|
|
}
|
|
|
|
$groupStyleDepth = ($groupStyleDepth !== 5) ? $groupStyleDepth + 1 : 1;
|
|
$openGroups[] = $m; // opening
|
|
$lastIsQuant = false;
|
|
$output[] = array("g{$groupStyleDepth}" => $m);
|
|
|
|
} elseif ($m[0] === ')') {
|
|
if (!count($openGroups)) {
|
|
throw new \Exception('No matching opening parenthesis');
|
|
}
|
|
|
|
$output[] = array('g' . $groupStyleDepth => ')');
|
|
$prevGroup = $openGroups[count($openGroups) - 1];
|
|
$prevGroup = isset($prevGroup[2]) ? $prevGroup[2] : '';
|
|
$lastIsQuant = !preg_match('/^[=!]/', $prevGroup);
|
|
$lastStyle = "g{$groupStyleDepth}";
|
|
$lastType = 0;
|
|
$groupStyleDepth = ($groupStyleDepth !== 1) ? $groupStyleDepth - 1 : 5;
|
|
|
|
array_pop($openGroups);
|
|
continue;
|
|
|
|
} elseif ($m[0] === '\\') {
|
|
if (isset($m[1]) && preg_match('/^[1-9]/', $m[1])) {
|
|
$nonBackrefDigits = '';
|
|
$num = substr((int)$m, 1);
|
|
|
|
while ($num > $capturingGroupCount) {
|
|
preg_match('/\d$/', $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])(\d*)/', $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('/^\{(\d+)(?:,(\d*))?/', $m, $interval);
|
|
|
|
if ($interval && ((int)$interval[1] > 65535 || (isset($interval[2]) && ((int)$interval[2] > 65535)))) {
|
|
throw new \Exception('Interval quantifier cannot use value over 65,535');
|
|
}
|
|
|
|
if ($interval && isset($interval[2]) && ((int)$interval[1] > (int)$interval[2])) {
|
|
throw new \Exception('Interval quantifier range is reversed');
|
|
}
|
|
|
|
$output[] = array($lastStyle ? $lastStyle : 'meta' => $m);
|
|
$lastIsQuant = false;
|
|
|
|
} elseif ($m === '|') {
|
|
if ($lastType === 1 || ($lastType === 2 && !count($openGroups))) {
|
|
throw new \Exception('Empty alternative effectively truncates the regex here');
|
|
}
|
|
|
|
$output[] = count($openGroups) ? array("g{$groupStyleDepth}" => '|') : array('meta' => '|');
|
|
$lastIsQuant = false;
|
|
$lastType = 2;
|
|
$lastStyle = '';
|
|
continue;
|
|
|
|
} elseif ($m === '^' || $m === '$') {
|
|
$output[] = array('meta' => $m);
|
|
$lastIsQuant = false;
|
|
|
|
} elseif ($m === '.') {
|
|
$output[] = array('meta' => '.');
|
|
$lastIsQuant = true;
|
|
|
|
} else {
|
|
$output[] = array('text' => $m);
|
|
$lastIsQuant = true;
|
|
}
|
|
|
|
$lastType = 0;
|
|
$lastStyle = '';
|
|
}
|
|
|
|
if ($openGroups) {
|
|
throw new \Exception('Unclosed grouping');
|
|
}
|
|
|
|
return $output;
|
|
}
|
|
|
|
|
|
/**
|
|
* Set or get configuration options
|
|
*
|
|
* @param string $key
|
|
* @param mixed|null $value
|
|
* @return mixed
|
|
*/
|
|
public static function config($key, $value = null) {
|
|
|
|
if (!array_key_exists($key, static::$config)) {
|
|
throw new \Exception(sprintf('Unrecognized option: "%s". Valid options are: %s', $key, implode(', ', array_keys(static::$config))));
|
|
}
|
|
|
|
if ($value === null) {
|
|
return static::$config[$key];
|
|
}
|
|
|
|
if (is_array(static::$config[$key])) {
|
|
return static::$config[$key] = (array)$value;
|
|
}
|
|
|
|
return static::$config[$key] = $value;
|
|
}
|
|
|
|
|
|
/**
|
|
* Total CPU time used by the class
|
|
*
|
|
* @param int precision
|
|
* @return double
|
|
*/
|
|
public static function getTime($precision = 4) {
|
|
return round(static::$time, $precision);
|
|
}
|
|
|
|
|
|
/**
|
|
* Get relevant backtrace info for last ref call
|
|
*
|
|
* @return array|false
|
|
*/
|
|
public static function getBacktrace() {
|
|
|
|
if (ref::config('showBacktrace')) {
|
|
// pull only basic info with php 5.3.6+ to save some memory
|
|
if (NULL !== ref::config('Backtrace')) {
|
|
// Observium hack for get original backtrace
|
|
$trace = ref::config('Backtrace');
|
|
} else {
|
|
$trace = defined('DEBUG_BACKTRACE_IGNORE_ARGS') ? debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) : debug_backtrace();
|
|
}
|
|
}
|
|
|
|
if (is_null($trace)) { return false; }
|
|
|
|
while ($callee = array_pop($trace)) {
|
|
|
|
// extract only the information we need
|
|
$callee = array_intersect_key($callee, array_fill_keys(array('file', 'function', 'line'), false));
|
|
extract($callee, EXTR_OVERWRITE);
|
|
|
|
// skip, if the called function doesn't match the shortcut function name
|
|
if (!$function || !in_array(strtolower((string)$function), static::$config['shortcutFunc'])) {
|
|
continue;
|
|
}
|
|
|
|
return compact('file', 'function', 'line');
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Determines the input expression(s) passed to the shortcut function
|
|
*
|
|
* @param array &$options Optional, options to gather (from operators)
|
|
* @return array Array of string expressions
|
|
*/
|
|
public static function getInputExpressions(array &$options = null) {
|
|
|
|
// used to determine the position of the current call,
|
|
// if more queries calls were made on the same line
|
|
static $lineInst = array();
|
|
|
|
$trace = static::getBacktrace();
|
|
|
|
if (!$trace) {
|
|
return array();
|
|
}
|
|
|
|
extract($trace);
|
|
|
|
$code = file($file);
|
|
$code = $code[$line - 1]; // multiline expressions not supported!
|
|
$instIndx = 0;
|
|
$tokens = token_get_all("<?php {$code}");
|
|
|
|
// locate the caller position in the line, and isolate argument tokens
|
|
foreach ($tokens as $i => $token) {
|
|
|
|
// match token with our shortcut function name
|
|
if (is_string($token) || ($token[0] !== T_STRING) || (strcasecmp($token[1], $function) !== 0)) {
|
|
continue;
|
|
}
|
|
|
|
// is this some method that happens to have the same name as the shortcut function?
|
|
if (isset($tokens[$i - 1]) && is_array($tokens[$i - 1]) && in_array($tokens[$i - 1][0], array(T_DOUBLE_COLON, T_OBJECT_OPERATOR), true)) {
|
|
continue;
|
|
}
|
|
|
|
// find argument definition start, just after '('
|
|
if (isset($tokens[$i + 1]) && ($tokens[$i + 1][0] === '(')) {
|
|
$instIndx++;
|
|
|
|
if (!isset($lineInst[$line])) {
|
|
$lineInst[$line] = 0;
|
|
}
|
|
|
|
if ($instIndx <= $lineInst[$line]) {
|
|
continue;
|
|
}
|
|
|
|
$lineInst[$line]++;
|
|
|
|
// gather options
|
|
if ($options !== null) {
|
|
$j = $i - 1;
|
|
while (isset($tokens[$j]) && is_string($tokens[$j]) && in_array($tokens[$j], array('@', '+', '-', '!', '~'))) {
|
|
$options[] = $tokens[$j--];
|
|
}
|
|
}
|
|
|
|
$lvl = $index = $curlies = 0;
|
|
$expressions = array();
|
|
|
|
// get the expressions
|
|
foreach (array_slice($tokens, $i + 2) as $token) {
|
|
|
|
if (is_array($token)) {
|
|
if ($token[0] !== T_COMMENT) {
|
|
$expressions[$index][] = ($token[0] !== T_WHITESPACE) ? $token[1] : ' ';
|
|
}
|
|
|
|
continue;
|
|
}
|
|
|
|
if ($token === '{') {
|
|
$curlies++;
|
|
}
|
|
|
|
if ($token === '}') {
|
|
$curlies--;
|
|
}
|
|
|
|
if ($token === '(') {
|
|
$lvl++;
|
|
}
|
|
|
|
if ($token === ')') {
|
|
$lvl--;
|
|
}
|
|
|
|
// assume next argument if a comma was encountered,
|
|
// and we're not insde a curly bracket or inner parentheses
|
|
if (($curlies < 1) && ($lvl === 0) && ($token === ',')) {
|
|
$index++;
|
|
continue;
|
|
}
|
|
|
|
// negative parentheses count means we reached the end of argument definitions
|
|
if ($lvl < 0) {
|
|
foreach($expressions as &$expression) {
|
|
$expression = trim(implode('', $expression));
|
|
}
|
|
|
|
return $expressions;
|
|
}
|
|
|
|
$expressions[$index][] = $token;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
return array();
|
|
}
|
|
|
|
|
|
/**
|
|
* Get all parent classes of a class
|
|
*
|
|
* @param Reflector $class Reflection object
|
|
* @return array Array of ReflectionClass objects (starts with the ancestor, ends with the given class)
|
|
*/
|
|
protected static function getParentClasses(\Reflector $class) {
|
|
|
|
$parents = array($class);
|
|
while (($class = $class->getParentClass()) !== false) {
|
|
$parents[] = $class;
|
|
}
|
|
|
|
return array_reverse($parents);
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Generate class / function info
|
|
*
|
|
* @param Reflector $reflector Class name or reflection object
|
|
* @param string $single Skip parent classes
|
|
* @param Reflector|null $context Object context (for methods)
|
|
* @return string
|
|
*/
|
|
protected function fromReflector(\Reflector $reflector, $single = '', \Reflector $context = null) {
|
|
|
|
// @todo: test this
|
|
$hash = var_export(func_get_args(), true);
|
|
//$hash = $reflector->getName() . ';' . $single . ';' . ($context ? $context->getName() : '');
|
|
|
|
if ($this->fmt->didCache($hash)) {
|
|
static::$debug['cacheHits']++;
|
|
return;
|
|
}
|
|
|
|
$items = array($reflector);
|
|
|
|
if (($single === '') && ($reflector instanceof \ReflectionClass)) {
|
|
$items = static::getParentClasses($reflector);
|
|
}
|
|
|
|
$first = true;
|
|
foreach ($items as $item) {
|
|
|
|
if (!$first) {
|
|
$this->fmt->sep(' :: ');
|
|
}
|
|
|
|
$first = false;
|
|
$name = ($single !== '') ? $single : $item->getName();
|
|
$comments = $item->isInternal() ? array() : static::parseComment($item->getDocComment());
|
|
$meta = array('sub' => array());
|
|
$bubbles = array();
|
|
|
|
if ($item->isInternal()) {
|
|
$extension = $item->getExtension();
|
|
$meta['title'] = ($extension instanceof \ReflectionExtension) ? sprintf('Internal - part of %s (%s)', $extension->getName(), $extension->getVersion()) : 'Internal';
|
|
|
|
} else {
|
|
$comments = static::parseComment($item->getDocComment());
|
|
|
|
if ($comments) {
|
|
$meta += $comments;
|
|
}
|
|
|
|
$meta['sub'][] = array('Defined in', basename($item->getFileName()) . ':' . $item->getStartLine());
|
|
}
|
|
|
|
if (($item instanceof \ReflectionFunction) || ($item instanceof \ReflectionMethod)) {
|
|
if (($context !== null) && ($context->getShortName() !== $item->getDeclaringClass()->getShortName())) {
|
|
$meta['sub'][] = array('Inherited from', $item->getDeclaringClass()->getShortName());
|
|
}
|
|
|
|
// @note: PHP 7 seems to crash when calling getPrototype on Closure::__invoke()
|
|
if (($item instanceof \ReflectionMethod) && !$item->isInternal()) {
|
|
try {
|
|
$proto = $item->getPrototype();
|
|
$meta['sub'][] = array('Prototype defined by', $proto->class);
|
|
} catch(\Exception $e) {}
|
|
}
|
|
|
|
$this->fmt->text('name', $name, $meta, $this->linkify($item));
|
|
continue;
|
|
}
|
|
|
|
// @todo: maybe - list interface methods
|
|
if (!($item->isInterface() || (static::$env['is54'] && $item->isTrait()))) {
|
|
|
|
if ($item->isAbstract()) {
|
|
$bubbles[] = array('A', 'Abstract');
|
|
}
|
|
|
|
if (static::$env['is7'] && $item->isAnonymous()) {
|
|
$bubbles[] = array('?', 'Anonymous');
|
|
}
|
|
|
|
if ($item->isFinal()) {
|
|
$bubbles[] = array('F', 'Final');
|
|
}
|
|
|
|
// php 5.4+ only
|
|
if (static::$env['is54'] && $item->isCloneable()) {
|
|
$bubbles[] = array('C', 'Cloneable');
|
|
}
|
|
|
|
if ($item->isIterateable()) {
|
|
$bubbles[] = array('X', 'Iterateable');
|
|
}
|
|
|
|
}
|
|
|
|
if ($item->isInterface() && $single !== '') {
|
|
$bubbles[] = array('I', 'Interface');
|
|
}
|
|
|
|
if ($bubbles) {
|
|
$this->fmt->bubbles($bubbles);
|
|
}
|
|
|
|
if ($item->isInterface() && $single === '') {
|
|
$name .= sprintf(' (%d)', count($item->getMethods()));
|
|
}
|
|
|
|
$this->fmt->text('name', $name, $meta, $this->linkify($item));
|
|
}
|
|
|
|
$this->fmt->cacheLock($hash);
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates an URL that points to the documentation page relevant for the requested context
|
|
*
|
|
* For internal functions and classes, the URI will point to the local PHP manual
|
|
* if installed and configured, otherwise to php.net/manual (the english one)
|
|
*
|
|
* @param Reflector $reflector Reflector object (used to determine the URL scheme for internal stuff)
|
|
* @param string|null $constant Constant name, if this is a request to linkify a constant
|
|
* @return string|null URL
|
|
*/
|
|
protected function linkify(\Reflector $reflector, $constant = null) {
|
|
|
|
static $docRefRoot = null, $docRefExt = null;
|
|
|
|
// most people don't have this set
|
|
if (!$docRefRoot) {
|
|
$docRefRoot = ($docRefRoot = rtrim(ini_get('docref_root'), '/')) ? $docRefRoot : 'http://php.net/manual/en';
|
|
}
|
|
|
|
if (!$docRefExt) {
|
|
$docRefExt = ($docRefExt = ini_get('docref_ext')) ? $docRefExt : '.php';
|
|
}
|
|
|
|
$phpNetSchemes = array(
|
|
'class' => $docRefRoot . '/class.%s' . $docRefExt,
|
|
'function' => $docRefRoot . '/function.%s' . $docRefExt,
|
|
'method' => $docRefRoot . '/%2$s.%1$s' . $docRefExt,
|
|
'property' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.props.%1$s',
|
|
'constant' => $docRefRoot . '/class.%2$s' . $docRefExt . '#%2$s.constants.%1$s',
|
|
);
|
|
|
|
$url = null;
|
|
$args = array();
|
|
|
|
// determine scheme
|
|
if ($constant !== null) {
|
|
$type = 'constant';
|
|
$args[] = $constant;
|
|
|
|
} else {
|
|
$type = explode('\\', get_class($reflector));
|
|
$type = strtolower(ltrim(end($type), 'Reflection'));
|
|
|
|
if ($type === 'object') {
|
|
$type = 'class';
|
|
}
|
|
}
|
|
|
|
// properties don't have the internal flag;
|
|
// also note that many internal classes use some kind of magic as properties (eg. DateTime);
|
|
// these will only get linkifed if the declared class is internal one, and not an extension :(
|
|
$parent = ($type !== 'property') ? $reflector : $reflector->getDeclaringClass();
|
|
|
|
// internal function/method/class/property/constant
|
|
if ($parent->isInternal()) {
|
|
$args[] = $reflector->name;
|
|
|
|
if (in_array($type, array('method', 'property'), true)) {
|
|
$args[] = $reflector->getDeclaringClass()->getName();
|
|
}
|
|
|
|
$args = array_map(
|
|
function ($text) {
|
|
return str_replace('_', '-', ltrim(strtolower($text), '\\_'));
|
|
}, $args);
|
|
|
|
// check for some special cases that have no links
|
|
$valid = (($type === 'method') || (strcasecmp($parent->name, 'stdClass') !== 0))
|
|
&& (($type !== 'method') || (($reflector->name === '__construct') || strpos($reflector->name, '__') !== 0));
|
|
|
|
if ($valid) {
|
|
$url = vsprintf($phpNetSchemes[$type], $args);
|
|
}
|
|
|
|
// custom
|
|
} else {
|
|
switch (true) {
|
|
|
|
// WordPress function;
|
|
// like pretty much everything else in WordPress, API links are inconsistent as well;
|
|
// so we're using queryposts.com as doc source for API
|
|
case ($type === 'function') && class_exists('WP', false) && defined('ABSPATH') && defined('WPINC'):
|
|
if (strpos($reflector->getFileName(), realpath(ABSPATH . WPINC)) === 0) {
|
|
$url = sprintf('http://queryposts.com/function/%s', urlencode(strtolower($reflector->getName())));
|
|
break;
|
|
}
|
|
|
|
// @todo: handle more apps
|
|
}
|
|
|
|
}
|
|
|
|
return $url;
|
|
}
|
|
|
|
|
|
public static function getTimeoutPoint() {
|
|
return static::$timeout;
|
|
}
|
|
|
|
|
|
public static function getDebugInfo() {
|
|
return static::$debug;
|
|
}
|
|
|
|
|
|
|
|
protected function hasInstanceTimedOut() {
|
|
|
|
if (static::$timeout > 0) {
|
|
return true;
|
|
}
|
|
|
|
$timeout = static::$config['timeout'];
|
|
|
|
if (($timeout > 0) && ((microtime(true) - $this->startTime) > $timeout)) {
|
|
return (static::$timeout = (microtime(true) - $this->startTime));
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
|
|
/**
|
|
* Evaluates the given variable
|
|
*
|
|
* @param mixed &$subject Variable to query
|
|
* @param bool $specialStr Should this be interpreted as a special string?
|
|
* @return mixed Result (both HTML and text modes generate strings)
|
|
*/
|
|
protected function evaluate(&$subject, $specialStr = false) {
|
|
|
|
switch ($type = gettype($subject)) {
|
|
|
|
// https://github.com/digitalnature/php-ref/issues/13
|
|
case 'unknown type':
|
|
return $this->fmt->text('unknown');
|
|
|
|
// null value
|
|
case 'NULL':
|
|
return $this->fmt->text('null');
|
|
|
|
// integer/double/float
|
|
case 'integer':
|
|
case 'double':
|
|
return $this->fmt->text($type, $subject, $type);
|
|
|
|
// boolean
|
|
case 'boolean':
|
|
$text = $subject ? 'true' : 'false';
|
|
return $this->fmt->text($text, $text, $type);
|
|
|
|
// arrays
|
|
case 'array':
|
|
|
|
// empty array?
|
|
if (empty($subject)) {
|
|
$this->fmt->text('array');
|
|
return $this->fmt->emptyGroup();
|
|
}
|
|
|
|
if (isset($subject[static::MARKER_KEY])) {
|
|
unset($subject[static::MARKER_KEY]);
|
|
$this->fmt->text('array');
|
|
$this->fmt->emptyGroup('recursion');
|
|
return;
|
|
}
|
|
|
|
// first recursion level detection;
|
|
// this is optional (used to print consistent recursion info)
|
|
foreach ($subject as $key => &$value) {
|
|
|
|
if (!is_array($value)) {
|
|
continue;
|
|
}
|
|
|
|
// save current value in a temporary variable
|
|
$buffer = $value;
|
|
|
|
// assign new value
|
|
$value = ($value !== 1) ? 1 : 2;
|
|
|
|
// if they're still equal, then we have a reference
|
|
if ($value === $subject) {
|
|
$value = $buffer;
|
|
$value[static::MARKER_KEY] = true;
|
|
$this->evaluate($value);
|
|
return;
|
|
}
|
|
|
|
// restoring original value
|
|
$value = $buffer;
|
|
}
|
|
|
|
$this->fmt->text('array');
|
|
$count = count($subject);
|
|
if (!$this->fmt->startGroup($count)) {
|
|
return;
|
|
}
|
|
|
|
$max = max(array_map('static::strLen', array_keys($subject)));
|
|
$subject[static::MARKER_KEY] = true;
|
|
|
|
foreach ($subject as $key => &$value) {
|
|
|
|
// ignore our temporary marker
|
|
if ($key === static::MARKER_KEY) {
|
|
continue;
|
|
}
|
|
|
|
if ($this->hasInstanceTimedOut()) {
|
|
break;
|
|
}
|
|
|
|
$keyInfo = gettype($key);
|
|
|
|
if ($keyInfo === 'string') {
|
|
$encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
|
|
$keyLen = static::strLen($key);
|
|
$keyLenInfo = $encoding && ($encoding !== 'ASCII') ? $keyLen . '; ' . $encoding : $keyLen;
|
|
$keyInfo = "{$keyInfo}({$keyLenInfo})";
|
|
} else {
|
|
$keyLen = strlen($key);
|
|
}
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->text('key', $key, "Key: {$keyInfo}");
|
|
$this->fmt->colDiv($max - $keyLen);
|
|
$this->fmt->sep('=>');
|
|
$this->fmt->colDiv();
|
|
$this->evaluate($value, $specialStr);
|
|
$this->fmt->endRow();
|
|
}
|
|
|
|
unset($subject[static::MARKER_KEY]);
|
|
|
|
$this->fmt->endGroup();
|
|
return;
|
|
|
|
// resource
|
|
case 'resource':
|
|
case 'resource (closed)':
|
|
$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();
|
|
}
|
|
|
|
}
|
|
|
|
// ip (just do not detect is as date)..
|
|
$isIp = filter_var($subject, FILTER_VALIDATE_IP) !== FALSE;
|
|
// if ($isIp) {
|
|
// $ver = strpos($subject, '.') ? 'ipv4' : 'ipv6';
|
|
// //$this->fmt->startContain('IPv' . $ver, true);
|
|
// $this->fmt->text($ver, $subject, $ver);
|
|
// //$this->fmt->endContain();
|
|
// }
|
|
|
|
// date
|
|
if (($length < 128) && static::$env['supportsDate'] && !$isIp &&
|
|
!preg_match('/[^A-Za-z0-9.:+\s\-\/]/', $subject)) {
|
|
try {
|
|
$date = new \DateTime($subject);
|
|
$errors = \DateTime::getLastErrors();
|
|
|
|
if (is_array($errors) &&
|
|
($errors['warning_count'] < 1) && ($errors['error_count'] < 1)) {
|
|
$now = new \Datetime('now');
|
|
$nowUtc = new \Datetime('now', new \DateTimeZone('UTC'));
|
|
$diff = $now->diff($date);
|
|
|
|
$map = array(
|
|
'y' => 'yr',
|
|
'm' => 'mo',
|
|
'd' => 'da',
|
|
'h' => 'hr',
|
|
'i' => 'min',
|
|
's' => 'sec',
|
|
);
|
|
|
|
$timeAgo = 'now';
|
|
foreach ($map as $k => $label) {
|
|
if ($diff->{$k} > 0) {
|
|
$timeAgo = $diff->format("%R%{$k}{$label}");
|
|
break;
|
|
}
|
|
}
|
|
|
|
$tz = $date->getTimezone();
|
|
$offs = round($tz->getOffset($nowUtc) / 3600);
|
|
|
|
if ($offs > 0) {
|
|
$offs = "+{$offs}";
|
|
}
|
|
|
|
$timeAgo .= ((int)$offs !== 0) ? ' ' . sprintf('%s (UTC%s)', $tz->getName(), $offs) : ' UTC';
|
|
$this->fmt->startContain('date', true);
|
|
$this->fmt->text('date', $timeAgo);
|
|
$this->fmt->endContain();
|
|
|
|
}
|
|
} catch(\Exception $e) {
|
|
// not a date
|
|
}
|
|
|
|
}
|
|
|
|
// attempt to detect if this is a serialized string
|
|
static $unserializing = 0;
|
|
$isSerialized = ($unserializing < 3)
|
|
&& (($subject[$length - 1] === ';') || ($subject[$length - 1] === '}'))
|
|
&& in_array($subject[0], array('s', 'a', 'O'), true)
|
|
&& ((($subject[0] === 's') && ($subject[$length - 2] !== '"')) || preg_match("/^{$subject[0]}:[0-9]+:/s", $subject))
|
|
&& (($unserialized = @unserialize($subject)) !== false);
|
|
|
|
if ($isSerialized) {
|
|
$unserializing++;
|
|
$this->fmt->startContain('serialized', true);
|
|
$this->evaluate($unserialized);
|
|
$this->fmt->endContain();
|
|
$unserializing--;
|
|
}
|
|
|
|
// try to find out if it's a json-encoded string;
|
|
// only do this for json-encoded arrays or objects, because other types have too generic formats
|
|
static $decodingJson = 0;
|
|
$isJson = !$isSerialized && ($decodingJson < 3) && in_array($subject[0], array('{', '['), true);
|
|
|
|
if ($isJson) {
|
|
$decodingJson++;
|
|
$data = json_decode($subject);
|
|
|
|
// ensure created objects live enough for PHP to provide a unique hash
|
|
if (is_object($data)) {
|
|
$this->intObjects->attach($data);
|
|
}
|
|
|
|
if ($isJson = (json_last_error() === JSON_ERROR_NONE)) {
|
|
$this->fmt->startContain('json', true);
|
|
$this->evaluate($data);
|
|
$this->fmt->endContain();
|
|
}
|
|
|
|
$decodingJson--;
|
|
}
|
|
|
|
// attempt to match a regex
|
|
if (!$isSerialized && !$isJson && !$isIp && $length < 768) {
|
|
try {
|
|
$components = $this->splitRegex($subject);
|
|
if ($components) {
|
|
$regex = '';
|
|
|
|
$this->fmt->startContain('regex', true);
|
|
foreach ($components as $component) {
|
|
$this->fmt->text('regex-' . key($component), reset($component));
|
|
}
|
|
$this->fmt->endContain();
|
|
}
|
|
|
|
} catch(\Exception $e) {
|
|
// not a regex
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
// if we reached this point, $subject must be an object
|
|
|
|
// track objects to detect recursion
|
|
static $hashes = array();
|
|
|
|
// hash ID of this object
|
|
$hash = spl_object_hash($subject);
|
|
$recursion = isset($hashes[$hash]);
|
|
|
|
// sometimes incomplete objects may be created from string unserialization,
|
|
// if the class to which the object belongs wasn't included until the unserialization stage...
|
|
if ($subject instanceof \__PHP_Incomplete_Class) {
|
|
$this->fmt->text('object');
|
|
$this->fmt->emptyGroup('incomplete');
|
|
return;
|
|
}
|
|
|
|
// check cache at this point
|
|
if (!$recursion && $this->fmt->didCache($hash)) {
|
|
static::$debug['cacheHits']++;
|
|
return;
|
|
}
|
|
|
|
$reflector = new \ReflectionObject($subject);
|
|
$this->fmt->startContain('class');
|
|
$this->fromReflector($reflector);
|
|
$this->fmt->text('object', ' object');
|
|
$this->fmt->endContain();
|
|
|
|
// already been here?
|
|
if ($recursion) {
|
|
return $this->fmt->emptyGroup('recursion');
|
|
}
|
|
|
|
$hashes[$hash] = 1;
|
|
|
|
$flags = \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED;
|
|
|
|
if (static::$config['showPrivateMembers']) {
|
|
$flags |= \ReflectionProperty::IS_PRIVATE;
|
|
}
|
|
|
|
$props = $magicProps = $methods = array();
|
|
|
|
if($reflector->hasMethod('__debugInfo')){
|
|
$magicProps = $subject->__debugInfo();
|
|
}else{
|
|
$props = $reflector->getProperties($flags);
|
|
}
|
|
|
|
if (static::$config['showMethods']) {
|
|
$flags = \ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED;
|
|
|
|
if (static::$config['showPrivateMembers']) {
|
|
$flags |= \ReflectionMethod::IS_PRIVATE;
|
|
}
|
|
|
|
$methods = $reflector->getMethods($flags);
|
|
}
|
|
|
|
$constants = $reflector->getConstants();
|
|
$interfaces = $reflector->getInterfaces();
|
|
$traits = static::$env['is54'] ? $reflector->getTraits() : array();
|
|
$parents = static::getParentClasses($reflector);
|
|
|
|
// work-around for https://bugs.php.net/bug.php?id=49154
|
|
// @see http://stackoverflow.com/questions/15672287/strange-behavior-of-reflectiongetproperties-with-numeric-keys
|
|
if (!static::$env['is54']) {
|
|
$props = array_values(array_filter($props, function($prop) use($subject) {
|
|
return !$prop->isPublic() || property_exists($subject, $prop->name);
|
|
}));
|
|
}
|
|
|
|
// no data to display?
|
|
if (!$props && !$methods && !$constants && !$interfaces && !$traits) {
|
|
unset($hashes[$hash]);
|
|
return $this->fmt->emptyGroup();
|
|
}
|
|
|
|
if (!$this->fmt->startGroup()) {
|
|
return;
|
|
}
|
|
|
|
// show contents for iterators
|
|
if (static::$config['showIteratorContents'] && $reflector->isIterateable()) {
|
|
|
|
$itContents = iterator_to_array($subject);
|
|
$this->fmt->sectionTitle(sprintf('Contents (%d)', count($itContents)));
|
|
|
|
foreach ($itContents as $key => $value) {
|
|
$keyInfo = gettype($key);
|
|
if ($keyInfo === 'string') {
|
|
$encoding = static::$env['mbStr'] ? mb_detect_encoding($key) : '';
|
|
$length = $encoding && ($encoding !== 'ASCII') ? static::strLen($key) . '; ' . $encoding : static::strLen($key);
|
|
$keyInfo = sprintf('%s(%s)', $keyInfo, $length);
|
|
}
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->text(array('key', 'iterator'), $key, sprintf('Iterator key: %s', $keyInfo));
|
|
$this->fmt->colDiv();
|
|
$this->fmt->sep('=>');
|
|
$this->fmt->colDiv();
|
|
$this->evaluate($value);
|
|
//$this->evaluate($value instanceof \Traversable ? ((count($value) > 0) ? $value : (string)$value) : $value);
|
|
$this->fmt->endRow();
|
|
}
|
|
}
|
|
|
|
// display the interfaces this objects' class implements
|
|
if ($interfaces) {
|
|
$items = array();
|
|
$this->fmt->sectionTitle('Implements');
|
|
$this->fmt->startRow();
|
|
$this->fmt->startContain('interfaces');
|
|
|
|
$i = 0;
|
|
$count = count($interfaces);
|
|
|
|
foreach ($interfaces as $name => $interface) {
|
|
$this->fromReflector($interface);
|
|
|
|
if (++$i < $count) {
|
|
$this->fmt->sep(', ');
|
|
}
|
|
}
|
|
|
|
$this->fmt->endContain();
|
|
$this->fmt->endRow();
|
|
}
|
|
|
|
// traits this objects' class uses
|
|
if ($traits) {
|
|
$items = array();
|
|
$this->fmt->sectionTitle('Uses');
|
|
$this->fmt->startRow();
|
|
$this->fmt->startContain('traits');
|
|
|
|
$i = 0;
|
|
$count = count($traits);
|
|
|
|
foreach ($traits as $name => $trait) {
|
|
$this->fromReflector($trait);
|
|
|
|
if (++$i < $count) {
|
|
$this->fmt->sep(', ');
|
|
}
|
|
}
|
|
|
|
$this->fmt->endContain();
|
|
$this->fmt->endRow();
|
|
}
|
|
|
|
// class constants
|
|
if ($constants) {
|
|
$this->fmt->sectionTitle('Constants');
|
|
$max = max(array_map('static::strLen', array_keys($constants)));
|
|
foreach ($constants as $name => $value) {
|
|
$meta = null;
|
|
$type = array('const');
|
|
foreach ($parents as $parent) {
|
|
if ($parent->hasConstant($name)) {
|
|
if ($parent !== $reflector) {
|
|
$type[] = 'inherited';
|
|
$meta = array('sub' => array(array('Prototype defined by', $parent->name)));
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->sep('::');
|
|
$this->fmt->colDiv();
|
|
$this->fmt->startContain($type);
|
|
$this->fmt->text('name', $name, $meta, $this->linkify($parent, $name));
|
|
$this->fmt->endContain();
|
|
$this->fmt->colDiv($max - static::strLen($name));
|
|
$this->fmt->sep('=');
|
|
$this->fmt->colDiv();
|
|
$this->evaluate($value);
|
|
$this->fmt->endRow();
|
|
}
|
|
}
|
|
|
|
// object/class properties
|
|
if ($props) {
|
|
$this->fmt->sectionTitle('Properties');
|
|
|
|
$max = 0;
|
|
foreach ($props as $idx => $prop) {
|
|
if (($propNameLen = static::strLen($prop->name)) > $max) {
|
|
$max = $propNameLen;
|
|
}
|
|
}
|
|
|
|
foreach($props as $idx => $prop) {
|
|
|
|
if ($this->hasInstanceTimedOut()) {
|
|
break;
|
|
}
|
|
|
|
$bubbles = array();
|
|
$sourceClass = $prop->getDeclaringClass();
|
|
$inherited = $reflector->getShortName() !== $sourceClass->getShortName();
|
|
$meta = $sourceClass->isInternal() ? null : static::parseComment($prop->getDocComment());
|
|
|
|
if ($meta) {
|
|
if ($inherited) {
|
|
$meta['sub'] = array(array('Declared in', $sourceClass->getShortName()));
|
|
}
|
|
|
|
if (isset($meta['tags']['var'][0])) {
|
|
$meta['left'] = $meta['tags']['var'][0][0];
|
|
}
|
|
|
|
unset($meta['tags']);
|
|
}
|
|
|
|
if ($prop->isProtected() || $prop->isPrivate()) {
|
|
$prop->setAccessible(true);
|
|
}
|
|
|
|
$value = $prop->getValue($subject);
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->sep($prop->isStatic() ? '::' : '->');
|
|
$this->fmt->colDiv();
|
|
|
|
$bubbles = array();
|
|
if ($prop->isProtected()) {
|
|
$bubbles[] = array('P', 'Protected');
|
|
}
|
|
|
|
if ($prop->isPrivate()) {
|
|
$bubbles[] = array('!', 'Private');
|
|
}
|
|
|
|
$this->fmt->bubbles($bubbles);
|
|
|
|
$type = array('prop');
|
|
|
|
if ($inherited) {
|
|
$type[] = 'inherited';
|
|
}
|
|
|
|
if ($prop->isPrivate()) {
|
|
$type[] = 'private';
|
|
}
|
|
|
|
$this->fmt->colDiv(2 - count($bubbles));
|
|
$this->fmt->startContain($type);
|
|
$this->fmt->text('name', $prop->name, $meta, $this->linkify($prop));
|
|
$this->fmt->endContain();
|
|
$this->fmt->colDiv($max - static::strLen($prop->name));
|
|
$this->fmt->sep('=');
|
|
$this->fmt->colDiv();
|
|
$this->evaluate($value);
|
|
$this->fmt->endRow();
|
|
}
|
|
}
|
|
|
|
// __debugInfo()
|
|
if($magicProps){
|
|
$this->fmt->sectionTitle('Properties (magic)');
|
|
|
|
$max = 0;
|
|
foreach($magicProps as $name => $value) {
|
|
if(($propNameLen = static::strLen($name)) > $max) {
|
|
$max = $propNameLen;
|
|
}
|
|
}
|
|
|
|
foreach($magicProps as $name => $value){
|
|
|
|
if($this->hasInstanceTimedOut()) {
|
|
break;
|
|
}
|
|
|
|
// attempt to pull out doc comment from the "regular" property definition
|
|
try{
|
|
$prop = $reflector->getProperty($name);
|
|
$meta = static::parseComment($prop->getDocComment());
|
|
|
|
}catch(\Exception $e){
|
|
$meta = null;
|
|
}
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->sep('->');
|
|
$this->fmt->colDiv();
|
|
|
|
$type = array('prop');
|
|
|
|
$this->fmt->startContain($type);
|
|
$this->fmt->text('name', $name, $meta);
|
|
$this->fmt->endContain();
|
|
$this->fmt->colDiv($max - static::strLen($name));
|
|
$this->fmt->sep('=');
|
|
$this->fmt->colDiv();
|
|
$this->evaluate($value);
|
|
$this->fmt->endRow();
|
|
}
|
|
}
|
|
|
|
// class methods
|
|
if ($methods && !$this->hasInstanceTimedOut()) {
|
|
|
|
$this->fmt->sectionTitle('Methods');
|
|
foreach ($methods as $idx => $method) {
|
|
|
|
$this->fmt->startRow();
|
|
$this->fmt->sep($method->isStatic() ? '::' : '->');
|
|
$this->fmt->colDiv();
|
|
|
|
$bubbles = array();
|
|
if ($method->isAbstract()) {
|
|
$bubbles[] = array('A', 'Abstract');
|
|
}
|
|
|
|
if ($method->isFinal()) {
|
|
$bubbles[] = array('F', 'Final');
|
|
}
|
|
|
|
if ($method->isProtected()) {
|
|
$bubbles[] = array('P', 'Protected');
|
|
}
|
|
|
|
if ($method->isPrivate()) {
|
|
$bubbles[] = array('!', 'Private');
|
|
}
|
|
|
|
$this->fmt->bubbles($bubbles);
|
|
|
|
$this->fmt->colDiv(4 - count($bubbles));
|
|
|
|
// is this method inherited?
|
|
$inherited = $reflector->getShortName() !== $method->getDeclaringClass()->getShortName();
|
|
|
|
$type = array('method');
|
|
|
|
if ($inherited) {
|
|
$type[] = 'inherited';
|
|
}
|
|
|
|
if ($method->isPrivate()) {
|
|
$type[] = 'private';
|
|
}
|
|
|
|
$this->fmt->startContain($type);
|
|
|
|
$name = $method->name;
|
|
if ($method->returnsReference()) {
|
|
$name = "&{$name}";
|
|
}
|
|
|
|
$this->fromReflector($method, $name, $reflector);
|
|
|
|
$paramCom = $method->isInternal() ? array() : static::parseComment($method->getDocComment(), 'tags');
|
|
$paramCom = empty($paramCom['param']) ? array() : $paramCom['param'];
|
|
$paramCount = $method->getNumberOfParameters();
|
|
|
|
$this->fmt->sep('(');
|
|
|
|
// process arguments
|
|
foreach ($method->getParameters() as $idx => $parameter) {
|
|
$meta = null;
|
|
$paramName = "\${$parameter->name}";
|
|
$optional = $parameter->isOptional();
|
|
$variadic = static::$env['is56'] && $parameter->isVariadic();
|
|
|
|
if ($parameter->isPassedByReference()) {
|
|
$paramName = "&{$paramName}";
|
|
}
|
|
|
|
if ($variadic) {
|
|
$paramName = "...{$paramName}";
|
|
}
|
|
|
|
$type = array('param');
|
|
|
|
if ($optional) {
|
|
$type[] = 'optional';
|
|
}
|
|
|
|
$this->fmt->startContain($type);
|
|
|
|
// attempt to build meta
|
|
foreach ($paramCom as $tag) {
|
|
list($pcTypes, $pcName, $pcDescription) = $tag;
|
|
if ($pcName !== $paramName) {
|
|
continue;
|
|
}
|
|
|
|
$meta = array('title' => $pcDescription);
|
|
|
|
if ($pcTypes) {
|
|
$meta['left'] = $pcTypes;
|
|
}
|
|
|
|
break;
|
|
}
|
|
|
|
try {
|
|
$paramClass = $parameter->getClass();
|
|
} catch(\Exception $e) {
|
|
// @see https://bugs.php.net/bug.php?id=32177&edit=1
|
|
}
|
|
|
|
if (!empty($paramClass)) {
|
|
$this->fmt->startContain('hint');
|
|
$this->fromReflector($paramClass, $paramClass->name);
|
|
$this->fmt->endContain();
|
|
$this->fmt->sep(' ');
|
|
|
|
} elseif ($parameter->isArray()) {
|
|
$this->fmt->text('hint', 'array');
|
|
$this->fmt->sep(' ');
|
|
|
|
} else {
|
|
$hasType = static::$env['is7'] && $parameter->hasType();
|
|
if ($hasType) {
|
|
$type = $parameter->getType();
|
|
$this->fmt->text('hint', (string)$type);
|
|
$this->fmt->sep(' ');
|
|
}
|
|
}
|
|
|
|
$this->fmt->text('name', $paramName, $meta);
|
|
|
|
if ($optional) {
|
|
try{
|
|
$paramValue = $parameter->isDefaultValueAvailable() ? $parameter->getDefaultValue() : null;
|
|
if ($paramValue !== null) {
|
|
$this->fmt->sep(' = ');
|
|
|
|
if (static::$env['is546'] && !$parameter->getDeclaringFunction()->isInternal() && $parameter->isDefaultValueConstant()) {
|
|
$this->fmt->text('constant', $parameter->getDefaultValueConstantName(), 'Constant');
|
|
|
|
} else {
|
|
$this->evaluate($paramValue, true);
|
|
}
|
|
}
|
|
|
|
}catch(\Exception $e){
|
|
// unable to retrieve default value?
|
|
}
|
|
|
|
}
|
|
|
|
$this->fmt->endContain();
|
|
|
|
if ($idx < $paramCount - 1) {
|
|
$this->fmt->sep(', ');
|
|
}
|
|
}
|
|
$this->fmt->sep(')');
|
|
$this->fmt->endContain();
|
|
|
|
$hasReturnType = static::$env['is7'] && $method->hasReturnType();
|
|
if ($hasReturnType) {
|
|
$type = $method->getReturnType();
|
|
$this->fmt->startContain('ret');
|
|
$this->fmt->sep(':');
|
|
$this->fmt->text('hint', (string)$type);
|
|
$this->fmt->endContain();
|
|
}
|
|
|
|
$this->fmt->endRow();
|
|
}
|
|
}
|
|
|
|
unset($hashes[$hash]);
|
|
$this->fmt->endGroup();
|
|
|
|
$this->fmt->cacheLock($hash);
|
|
}
|
|
|
|
|
|
/**
|
|
* Scans for known classes and functions inside the provided expression,
|
|
* and linkifies them when possible
|
|
*
|
|
* @param string $expression Expression to format
|
|
* @return string Formatted output
|
|
*/
|
|
protected function evaluateExp($expression = null) {
|
|
|
|
if ($expression === null) {
|
|
return;
|
|
}
|
|
|
|
if (static::strLen($expression) > 120) {
|
|
$expression = substr($expression, 0, 120) . '...';
|
|
}
|
|
|
|
$this->fmt->sep('> ');
|
|
|
|
if (strpos($expression, '(') === false) {
|
|
return $this->fmt->text('expTxt', $expression);
|
|
}
|
|
|
|
$keywords = array_map('trim', explode('(', $expression, 2));
|
|
$parts = array();
|
|
|
|
// try to find out if this is a function
|
|
try {
|
|
$reflector = new \ReflectionFunction($keywords[0]);
|
|
$parts[] = array($keywords[0], $reflector, '');
|
|
|
|
} catch(\Exception $e) {
|
|
|
|
if (stripos($keywords[0], 'new ') === 0) {
|
|
$cn = explode(' ' , $keywords[0], 2);
|
|
|
|
// linkify 'new keyword' (as constructor)
|
|
try {
|
|
$reflector = new \ReflectionMethod($cn[1], '__construct');
|
|
$parts[] = array($cn[0], $reflector, '');
|
|
|
|
} catch(\Exception $e) {
|
|
$reflector = null;
|
|
$parts[] = $cn[0];
|
|
}
|
|
|
|
// class name...
|
|
try {
|
|
$reflector = new \ReflectionClass($cn[1]);
|
|
$parts[] = array($cn[1], $reflector, ' ');
|
|
|
|
} catch(\Exception $e) {
|
|
$reflector = null;
|
|
$parts[] = $cn[1];
|
|
}
|
|
|
|
} else {
|
|
|
|
// we can only linkify methods called statically
|
|
if (strpos($keywords[0], '::') === false) {
|
|
return $this->fmt->text('expTxt', $expression);
|
|
}
|
|
|
|
$cn = explode('::', $keywords[0], 2);
|
|
|
|
// attempt to linkify class name
|
|
try {
|
|
$reflector = new \ReflectionClass($cn[0]);
|
|
$parts[] = array($cn[0], $reflector, '');
|
|
|
|
} catch(\Exception $e) {
|
|
$reflector = null;
|
|
$parts[] = $cn[0];
|
|
}
|
|
|
|
// perhaps it's a static class method; try to linkify method
|
|
try {
|
|
$reflector = new \ReflectionMethod($cn[0], $cn[1]);
|
|
$parts[] = array($cn[1], $reflector, '::');
|
|
|
|
} catch(\Exception $e) {
|
|
$reflector = null;
|
|
$parts[] = $cn[1];
|
|
}
|
|
}
|
|
}
|
|
|
|
$parts[] = "({$keywords[1]}";
|
|
|
|
foreach ($parts as $element) {
|
|
if (!is_array($element)) {
|
|
$this->fmt->text('expTxt', $element);
|
|
continue;
|
|
}
|
|
|
|
list($text, $reflector, $prefix) = $element;
|
|
|
|
if ($prefix !== '') {
|
|
$this->fmt->text('expTxt', $prefix);
|
|
}
|
|
|
|
$this->fromReflector($reflector, $text);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Calculates real string length
|
|
*
|
|
* @param string $string
|
|
* @return int
|
|
*/
|
|
protected static function strLen($string) {
|
|
$encoding = function_exists('mb_detect_encoding') ? mb_detect_encoding($string) : false;
|
|
return $encoding ? mb_strlen($string, $encoding) : strlen($string);
|
|
}
|
|
|
|
|
|
/**
|
|
* Safe str_pad alternative
|
|
*
|
|
* @param string $string
|
|
* @param int $padLen
|
|
* @param string $padStr
|
|
* @param int $padType
|
|
* @return string
|
|
*/
|
|
protected static function strPad($input, $padLen, $padStr = ' ', $padType = STR_PAD_RIGHT) {
|
|
$diff = strlen($input) - static::strLen($input);
|
|
return str_pad($input, $padLen + $diff, $padStr, $padType);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Formatter abstraction
|
|
*/
|
|
abstract class RFormatter {
|
|
|
|
/**
|
|
* Flush output and send contents to the output device
|
|
*/
|
|
abstract public function flush();
|
|
|
|
/**
|
|
* Generate a base entity
|
|
*
|
|
* @param string|array $type
|
|
* @param string|null $text
|
|
* @param string|array|null $meta
|
|
* @param string|null $uri
|
|
*/
|
|
abstract public function text($type, $text = null, $meta = null, $uri = null);
|
|
|
|
/**
|
|
* Generate container start token
|
|
*
|
|
* @param string|array $type
|
|
* @param string|bool $label
|
|
*/
|
|
public function startContain($type, $label = false){}
|
|
|
|
/**
|
|
* Generate container ending token
|
|
*/
|
|
public function endContain(){}
|
|
|
|
/**
|
|
* Generate empty group token
|
|
*
|
|
* @param string $prefix
|
|
*/
|
|
public function emptyGroup($prefix = ''){}
|
|
|
|
/**
|
|
* Generate group start token
|
|
*
|
|
* This method must return boolean TRUE on success, false otherwise (eg. max depth reached).
|
|
* The evaluator will skip this group on FALSE
|
|
*
|
|
* @param string $prefix
|
|
* @return bool
|
|
*/
|
|
public function startGroup($prefix = ''){}
|
|
|
|
/**
|
|
* Generate group ending token
|
|
*/
|
|
public function endGroup(){}
|
|
|
|
/**
|
|
* Generate section title
|
|
*
|
|
* @param string $title
|
|
*/
|
|
public function sectionTitle($title){}
|
|
|
|
/**
|
|
* Generate row start token
|
|
*/
|
|
public function startRow(){}
|
|
|
|
/**
|
|
* Generate row ending token
|
|
*/
|
|
public function endRow(){}
|
|
|
|
/**
|
|
* Column divider (cell delimiter)
|
|
*
|
|
* @param int $padLen
|
|
*/
|
|
public function colDiv($padLen = null){}
|
|
|
|
/**
|
|
* Generate modifier tokens
|
|
*
|
|
* @param array $items
|
|
*/
|
|
public function bubbles(array $items){}
|
|
|
|
/**
|
|
* Input expression start
|
|
*/
|
|
public function startExp(){}
|
|
|
|
/**
|
|
* Input expression end
|
|
*/
|
|
public function endExp(){}
|
|
|
|
/**
|
|
* Root starting token
|
|
*/
|
|
public function startRoot(){}
|
|
|
|
/**
|
|
* Root ending token
|
|
*/
|
|
public function endRoot(){}
|
|
|
|
/**
|
|
* Separator token
|
|
*
|
|
* @param string $label
|
|
*/
|
|
public function sep($label = ' '){}
|
|
|
|
/**
|
|
* Resolve cache request
|
|
*
|
|
* If the ID is not present in the cache, then a new cache entry is created
|
|
* for the given ID, and string offsets are captured until cacheLock is called
|
|
*
|
|
* This method must return TRUE if the ID exists in the cache, and append the cached item
|
|
* to the output, FALSE otherwise.
|
|
*
|
|
* @param string $id
|
|
* @return bool
|
|
*/
|
|
public function didCache($id) {
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Ends cache capturing for the given ID
|
|
*
|
|
* @param string $id
|
|
*/
|
|
public function cacheLock($id){}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Generates the output in HTML5 format
|
|
*
|
|
*/
|
|
class RHtmlFormatter extends RFormatter{
|
|
|
|
protected
|
|
|
|
/**
|
|
* Actual output
|
|
*
|
|
* @var string
|
|
*/
|
|
$out = '',
|
|
|
|
/**
|
|
* Tracks current nesting level
|
|
*
|
|
* @var int
|
|
*/
|
|
$level = 0,
|
|
|
|
/**
|
|
* Stores tooltip content for all entries
|
|
*
|
|
* To avoid having duplicate tooltip data in the HTML, we generate them once,
|
|
* and use references (the Q index) to pull data when required;
|
|
* this improves performance significantly
|
|
*
|
|
* @var array
|
|
*/
|
|
$tips = array(),
|
|
|
|
/**
|
|
* Used to cache output to speed up processing.
|
|
*
|
|
* Contains hashes as keys and string offsets as values.
|
|
* Cached objects will not be processed again in the same query
|
|
*
|
|
* @var array
|
|
*/
|
|
$cache = array(),
|
|
|
|
/**
|
|
* Map of used HTML tag and attributes
|
|
*
|
|
* @var string
|
|
*/
|
|
$def = array();
|
|
|
|
|
|
|
|
protected static
|
|
|
|
/**
|
|
* Instance counter
|
|
*
|
|
* @var int
|
|
*/
|
|
$counter = 0,
|
|
|
|
/**
|
|
* Tracks style/jscript inclusion state
|
|
*
|
|
* @var bool
|
|
*/
|
|
$didAssets = false;
|
|
|
|
|
|
public function __construct() {
|
|
|
|
if (ref::config('validHtml')) {
|
|
|
|
$this->def = array(
|
|
'base' => 'span',
|
|
'tip' => 'div',
|
|
'cell' => 'data-cell',
|
|
'table' => 'data-table',
|
|
'row' => 'data-row',
|
|
'group' => 'data-group',
|
|
'gLabel' => 'data-gLabel',
|
|
'match' => 'data-match',
|
|
'tipRef' => 'data-tip',
|
|
);
|
|
|
|
|
|
} else {
|
|
|
|
$this->def = array(
|
|
'base' => 'r',
|
|
'tip' => 't',
|
|
'cell' => 'c',
|
|
'table' => 't',
|
|
'row' => 'r',
|
|
'group' => 'g',
|
|
'gLabel' => 'gl',
|
|
'match' => 'm',
|
|
'tipRef' => 'h',
|
|
);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public function flush() {
|
|
print $this->out;
|
|
$this->out = '';
|
|
$this->cache = array();
|
|
$this->tips = array();
|
|
}
|
|
|
|
|
|
public function didCache($id) {
|
|
|
|
if (!isset($this->cache[$id])) {
|
|
$this->cache[$id] = array();
|
|
$this->cache[$id][] = strlen($this->out);
|
|
return false;
|
|
}
|
|
|
|
if (!isset($this->cache[$id][1])) {
|
|
$this->cache[$id][0] = strlen($this->out);
|
|
return false;
|
|
}
|
|
|
|
$this->out .= substr($this->out, $this->cache[$id][0], $this->cache[$id][1]);
|
|
return true;
|
|
}
|
|
|
|
public function cacheLock($id) {
|
|
$this->cache[$id][] = strlen($this->out) - $this->cache[$id][0];
|
|
}
|
|
|
|
public function sep($label = ' ') {
|
|
$this->out .= $label !== ' ' ? '<i>' . static::escape($label) . '</i>' : $label;
|
|
}
|
|
|
|
public function text($type, $text = null, $meta = null, $uri = null) {
|
|
|
|
if (!is_array($type)) {
|
|
$type = (array)$type;
|
|
}
|
|
|
|
$tip = '';
|
|
$text = ($text !== null) ? static::escape($text) : static::escape($type[0]);
|
|
|
|
if (in_array('special', $type)) {
|
|
$text = strtr($text, array(
|
|
"\r" => '<i>\r</i>', // carriage return
|
|
"\t" => '<i>\t</i>', // horizontal tab
|
|
"\n" => '<i>\n</i>', // linefeed (new line)
|
|
"\v" => '<i>\v</i>', // vertical tab
|
|
"\e" => '<i>\e</i>', // escape
|
|
"\f" => '<i>\f</i>', // form feed
|
|
"\0" => '<i>\0</i>',
|
|
));
|
|
}
|
|
|
|
// generate tooltip reference (probably the slowest part of the code ;)
|
|
if ($meta !== null) {
|
|
$tipIdx = array_search($meta, $this->tips, true);
|
|
|
|
if ($tipIdx === false) {
|
|
$tipIdx = array_push($this->tips, $meta) - 1;
|
|
}
|
|
|
|
$tip = " {$this->def['tipRef']}=\"{$tipIdx}\"";
|
|
//$tip = sprintf('%s="%d"', $this->def['tipRef'], $tipIdx);
|
|
}
|
|
|
|
// wrap text in a link?
|
|
if ($uri !== null) {
|
|
$text = '<a href="' . $uri . '" target="_blank">' . $text . '</a>';
|
|
}
|
|
|
|
$typeStr = '';
|
|
foreach ($type as $part) {
|
|
$typeStr .= " data-{$part}";
|
|
}
|
|
|
|
$this->out .= "<{$this->def['base']}{$typeStr}{$tip}>{$text}</{$this->def['base']}>";
|
|
//$this->out .= sprintf('<%1$s%2$s %3$s>%4$s</%1$s>', $this->def['base'], $typeStr, $tip, $text);
|
|
}
|
|
|
|
public function startContain($type, $label = false) {
|
|
|
|
if (!is_array($type)) {
|
|
$type = (array)$type;
|
|
}
|
|
|
|
if ($label) {
|
|
$this->out .= '<br>';
|
|
}
|
|
|
|
$typeStr = '';
|
|
foreach ($type as $part) {
|
|
$typeStr .= " data-{$part}";
|
|
}
|
|
|
|
$this->out .= "<{$this->def['base']}{$typeStr}>";
|
|
|
|
if ($label) {
|
|
$this->out .= "<{$this->def['base']} {$this->def['match']}>{$type[0]}</{$this->def['base']}>";
|
|
}
|
|
}
|
|
|
|
public function endContain() {
|
|
$this->out .= "</{$this->def['base']}>";
|
|
}
|
|
|
|
public function emptyGroup($prefix = '') {
|
|
|
|
if ($prefix !== '') {
|
|
$prefix = "<{$this->def['base']} {$this->def['gLabel']}>" . static::escape($prefix) . "</{$this->def['base']}>";
|
|
}
|
|
|
|
$this->out .= "<i>(</i>{$prefix}<i>)</i>";
|
|
}
|
|
|
|
|
|
public function startGroup($prefix = '') {
|
|
|
|
$maxDepth = ref::config('maxDepth');
|
|
|
|
if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) {
|
|
$this->emptyGroup('...');
|
|
return false;
|
|
}
|
|
|
|
$this->level++;
|
|
|
|
$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) . "</{$this->def['base']}>";
|
|
}
|
|
|
|
$this->out .= "<i>(</i>{$prefix}<{$this->def['base']} data-toggle{$exp}></{$this->def['base']}><{$this->def['base']} {$this->def['group']}><{$this->def['base']} {$this->def['table']}>";
|
|
|
|
return true;
|
|
}
|
|
|
|
public function endGroup() {
|
|
$this->out .= "</{$this->def['base']}></{$this->def['base']}><i>)</i>";
|
|
$this->level--;
|
|
}
|
|
|
|
public function sectionTitle($title) {
|
|
$this->out .= "</{$this->def['base']}><{$this->def['base']} data-tHead>{$title}</{$this->def['base']}><{$this->def['base']} {$this->def['table']}>";
|
|
}
|
|
|
|
public function startRow() {
|
|
$this->out .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>";
|
|
}
|
|
|
|
public function endRow() {
|
|
$this->out .= "</{$this->def['base']}></{$this->def['base']}>";
|
|
}
|
|
|
|
public function colDiv($padLen = null) {
|
|
$this->out .= "</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>";
|
|
}
|
|
|
|
public function bubbles(array $items) {
|
|
|
|
if (!$items) {
|
|
return;
|
|
}
|
|
|
|
$this->out .= "<{$this->def['base']} data-mod>";
|
|
|
|
foreach ($items as $info) {
|
|
$this->out .= $this->text('mod-' . strtolower($info[1]), $info[0], $info[1]);
|
|
}
|
|
|
|
$this->out .= "</{$this->def['base']}>";
|
|
}
|
|
|
|
public function startExp() {
|
|
$this->out .= "<{$this->def['base']} data-input>";
|
|
}
|
|
|
|
public function endExp() {
|
|
if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) {
|
|
$docRoot = isset($_SERVER['DOCUMENT_ROOT']) ? $_SERVER['DOCUMENT_ROOT'] : '';
|
|
$path = strpos($trace['file'], $docRoot) !== 0 ? $trace['file'] : ltrim(str_replace($docRoot, '', $trace['file']), '/');
|
|
$this->out .= "<{$this->def['base']} data-backtrace>{$path}:{$trace['line']}</{$this->def['base']}>";
|
|
}
|
|
|
|
$this->out .= "</{$this->def['base']}><{$this->def['base']} data-output>";
|
|
}
|
|
|
|
public function startRoot() {
|
|
$this->out .= '<!-- ref#' . ++static::$counter . ' --><div>' . static::getAssets() . '<div class="ref">';
|
|
}
|
|
|
|
public function endRoot() {
|
|
$this->out .= "</{$this->def['base']}>";
|
|
|
|
// process tooltips
|
|
$tipHtml = '';
|
|
foreach ($this->tips as $idx => $meta) {
|
|
|
|
$tip = '';
|
|
if (!is_array($meta)) {
|
|
$meta = array('title' => $meta);
|
|
}
|
|
|
|
$meta += array(
|
|
'title' => '',
|
|
'left' => '',
|
|
'description' => '',
|
|
'tags' => array(),
|
|
'sub' => array(),
|
|
);
|
|
|
|
$meta = static::escape($meta);
|
|
$cols = array();
|
|
|
|
if ($meta['left']) {
|
|
$cols[] = "<{$this->def['base']} {$this->def['cell']} data-varType>{$meta['left']}</{$this->def['base']}>";
|
|
}
|
|
|
|
$title = $meta['title'] ? "<{$this->def['base']} data-title>{$meta['title']}</{$this->def['base']}>" : '';
|
|
$desc = $meta['description'] ? "<{$this->def['base']} data-desc>{$meta['description']}</{$this->def['base']}>" : '';
|
|
$tags = '';
|
|
|
|
foreach ($meta['tags'] as $tag => $values) {
|
|
foreach ($values as $value) {
|
|
if ($tag === 'param') {
|
|
$value[0] = "{$value[0]} {$value[1]}";
|
|
unset($value[1]);
|
|
}
|
|
|
|
$value = is_array($value) ? implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $value) : $value;
|
|
$tags .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>@{$tag}</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>{$value}</{$this->def['base']}></{$this->def['base']}>";
|
|
}
|
|
}
|
|
|
|
if ($tags) {
|
|
$tags = "<{$this->def['base']} {$this->def['table']}>{$tags}</{$this->def['base']}>";
|
|
}
|
|
|
|
if ($title || $desc || $tags) {
|
|
$cols[] = "<{$this->def['base']} {$this->def['cell']}>{$title}{$desc}{$tags}</{$this->def['base']}>";
|
|
}
|
|
|
|
if ($cols) {
|
|
$tip = "<{$this->def['base']} {$this->def['row']}>" . implode('', $cols) . "</{$this->def['base']}>";
|
|
}
|
|
|
|
$sub = '';
|
|
foreach ($meta['sub'] as $line) {
|
|
$sub .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']}>" . implode("</{$this->def['base']}><{$this->def['base']} {$this->def['cell']}>", $line) . "</{$this->def['base']}></{$this->def['base']}>";
|
|
}
|
|
|
|
if ($sub) {
|
|
$tip .= "<{$this->def['base']} {$this->def['row']}><{$this->def['base']} {$this->def['cell']} data-sub><{$this->def['base']} {$this->def['table']}>{$sub}</{$this->def['base']}></{$this->def['base']}></{$this->def['base']}>";
|
|
}
|
|
|
|
if ($tip) {
|
|
$this->out .= "<{$this->def['tip']}>{$tip}</{$this->def['tip']}>";
|
|
}
|
|
}
|
|
|
|
if (($timeout = ref::getTimeoutPoint()) > 0) {
|
|
$this->out .= sprintf("<{$this->def['base']} data-error>Listing incomplete. Timed-out after %4.2fs</{$this->def['base']}>", $timeout);
|
|
}
|
|
|
|
$this->out .= '</div></div><!-- /ref#' . static::$counter . ' -->';
|
|
}
|
|
|
|
|
|
/**
|
|
* Get styles and javascript (only generated for the 1st call)
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function getAssets() {
|
|
|
|
// first call? include styles and javascript
|
|
if (static::$didAssets) {
|
|
return '';
|
|
}
|
|
|
|
ob_start();
|
|
|
|
if (ref::config('stylePath') !== false) {
|
|
?>
|
|
<style>
|
|
<?php readfile(str_replace('{:dir}', __DIR__, ref::config('stylePath'))); ?>
|
|
</style>
|
|
<?php
|
|
}
|
|
|
|
if (ref::config('scriptPath') !== false) {
|
|
?>
|
|
<script>
|
|
<?php readfile(str_replace('{:dir}', __DIR__, ref::config('scriptPath'))); ?>
|
|
</script>
|
|
<?php
|
|
}
|
|
|
|
// normalize space and remove comments
|
|
$output = preg_replace('/\s+/', ' ', trim(ob_get_clean()));
|
|
$output = preg_replace('!/\*.*?\*/!s', '', $output);
|
|
$output = preg_replace('/\n\s*\n/', "\n", $output);
|
|
|
|
static::$didAssets = true;
|
|
return $output;
|
|
}
|
|
|
|
|
|
/**
|
|
* Escapes variable for HTML output
|
|
*
|
|
* @param string|array $var
|
|
* @return string|array
|
|
*/
|
|
protected static function escape($var) {
|
|
return is_array($var) ? array_map('static::escape', $var) : htmlspecialchars($var, ENT_QUOTES);
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
* Generates the output in plain text format
|
|
*
|
|
*/
|
|
class RTextFormatter extends RFormatter {
|
|
|
|
protected
|
|
|
|
/**
|
|
* Actual output
|
|
*
|
|
* @var string
|
|
*/
|
|
$out = '',
|
|
|
|
/**
|
|
* Tracks current nesting level
|
|
*
|
|
* @var int
|
|
*/
|
|
$level = 0,
|
|
|
|
/**
|
|
* Current indenting level
|
|
*
|
|
* @var int
|
|
*/
|
|
$indent = 0,
|
|
|
|
$lastIdx = 0,
|
|
$lastLineSt = 0,
|
|
$levelPad = array(0);
|
|
|
|
|
|
public function flush() {
|
|
print $this->out;
|
|
$this->out = '';
|
|
$this->cache = array();
|
|
}
|
|
|
|
public function sep($label = ' ') {
|
|
$this->out .= $label;
|
|
}
|
|
|
|
public function text($type, $text = null, $meta = null, $uri = null) {
|
|
|
|
if (!is_array($type)) {
|
|
$type = (array)$type;
|
|
}
|
|
|
|
if ($text === null) {
|
|
$text = $type[0];
|
|
}
|
|
|
|
if (in_array('special', $type, true)) {
|
|
$text = strtr($text, array(
|
|
"\r" => '\r', // carriage return
|
|
"\t" => '\t', // horizontal tab
|
|
"\n" => '\n', // linefeed (new line)
|
|
"\v" => '\v', // vertical tab
|
|
"\e" => '\e', // escape
|
|
"\f" => '\f', // form feed
|
|
"\0" => '\0',
|
|
));
|
|
|
|
$this->out .= $text;
|
|
return;
|
|
}
|
|
|
|
$formatMap = array(
|
|
'string' => '%3$s "%2$s"',
|
|
'integer' => 'int(%2$s)',
|
|
'double' => 'double(%2$s)',
|
|
'true' => 'bool(%2$s)',
|
|
'false' => 'bool(%2$s)',
|
|
'key' => '[%2$s]',
|
|
//'ipv4' => ' IPv4', //'IPv4 "%2$s"',
|
|
//'ipv6' => ' IPv6', //'IPv6 "%2$s"',
|
|
);
|
|
|
|
if (!is_string($meta)) {
|
|
$meta = '';
|
|
}
|
|
|
|
$this->out .= isset($formatMap[$type[0]]) ? sprintf($formatMap[$type[0]], $type[0], $text, $meta) : $text;
|
|
}
|
|
|
|
public function startContain($type, $label = false) {
|
|
|
|
if (!is_array($type)) {
|
|
$type = (array)$type;
|
|
}
|
|
|
|
if ($label) {
|
|
$this->out .= "\n" . str_repeat(' ', $this->indent + $this->levelPad[$this->level]) . "┗ {$type[0]} ~ ";
|
|
}
|
|
}
|
|
|
|
public function emptyGroup($prefix = '') {
|
|
$this->out .= "({$prefix})";
|
|
}
|
|
|
|
public function startGroup($prefix = '') {
|
|
|
|
$maxDepth = ref::config('maxDepth');
|
|
|
|
if (($maxDepth > 0) && (($this->level + 1) > $maxDepth)) {
|
|
$this->emptyGroup('...');
|
|
return false;
|
|
}
|
|
|
|
$this->level++;
|
|
$this->out .= '(';
|
|
|
|
$this->indent += $this->levelPad[$this->level - 1];
|
|
return true;
|
|
}
|
|
|
|
public function endGroup() {
|
|
$this->out .= "\n" . str_repeat(' ', $this->indent) . ')';
|
|
$this->indent -= $this->levelPad[$this->level - 1];
|
|
$this->level--;
|
|
}
|
|
|
|
public function sectionTitle($title) {
|
|
$pad = str_repeat(' ', $this->indent + 2);
|
|
$this->out .= sprintf("\n\n%s%s\n%s%s", $pad, $title, $pad, str_repeat('-', strlen($title)));
|
|
}
|
|
|
|
public function startRow() {
|
|
$this->out .= "\n " . str_repeat(' ', $this->indent);
|
|
$this->lastLineSt = strlen($this->out);
|
|
}
|
|
|
|
public function endRow(){
|
|
}
|
|
|
|
public function colDiv($padLen = null) {
|
|
$padLen = ($padLen !== null) ? $padLen + 1 : 1;
|
|
$this->out .= str_repeat(' ', $padLen);
|
|
|
|
$this->lastIdx = strlen($this->out);
|
|
$this->levelPad[$this->level] = $this->lastIdx - $this->lastLineSt + 2;
|
|
}
|
|
|
|
public function bubbles(array $items) {
|
|
|
|
if (!$items) {
|
|
$this->out .= ' ';
|
|
return;
|
|
}
|
|
|
|
$this->out .= '<';
|
|
|
|
foreach ($items as $item) {
|
|
$this->out .= $item[0];
|
|
}
|
|
|
|
$this->out .= '>';
|
|
}
|
|
|
|
public function endExp() {
|
|
|
|
if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) {
|
|
$this->out .= ' - ' . $trace['file'] . ':' . $trace['line'];
|
|
}
|
|
|
|
$this->out .= "\n" . str_repeat('=', strlen($this->out)) . "\n";
|
|
}
|
|
|
|
public function startRoot() {
|
|
$this->out .= "\n\n";
|
|
|
|
}
|
|
|
|
public function endRoot() {
|
|
$this->out .= "\n";
|
|
if (($timeout = ref::getTimeoutPoint()) > 0) {
|
|
$this->out .= sprintf("\n-- Listing incomplete. Timed-out after %4.2fs -- \n", $timeout);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Text formatter with color support for CLI -- unfinished
|
|
*
|
|
*/
|
|
class RCliTextFormatter extends RTextFormatter {
|
|
|
|
public function sectionTitle($title) {
|
|
$pad = str_repeat(' ', $this->indent + 2);
|
|
$this->out .= sprintf("\n\n%s\x1b[4;97m%s\x1b[0m", $pad, $title);
|
|
}
|
|
|
|
public function startExp() {
|
|
$this->out .= "\x1b[1;44;96m ";
|
|
}
|
|
|
|
public function endExp() {
|
|
if (ref::config('showBacktrace') && ($trace = ref::getBacktrace())) {
|
|
$this->out .= "\x1b[0m\x1b[44;36m " . $trace['file'] . ':' . $trace['line'];
|
|
}
|
|
|
|
$this->out .= " \x1b[0m\n";
|
|
}
|
|
|
|
public function endRoot() {
|
|
$this->out .= "\n";
|
|
if (($timeout = ref::getTimeoutPoint()) > 0) {
|
|
$this->out .= sprintf("\n\x1b[3;91m-- Listing incomplete. Timed-out after %4.2fs --\x1b[0m\n", $timeout);
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
// EOF
|