Commit version 24.12.13800
This commit is contained in:
396
libs/Nette/Tracy/Dumper/Describer.php
Normal file
396
libs/Nette/Tracy/Dumper/Describer.php
Normal file
@ -0,0 +1,396 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Dumper;
|
||||
|
||||
use Tracy;
|
||||
use Tracy\Helpers;
|
||||
|
||||
|
||||
/**
|
||||
* Converts PHP values to internal representation.
|
||||
* @internal
|
||||
*/
|
||||
final class Describer
|
||||
{
|
||||
public const HiddenValue = '*****';
|
||||
|
||||
// Number.MAX_SAFE_INTEGER
|
||||
private const JsSafeInteger = 1 << 53 - 1;
|
||||
|
||||
/** @var int */
|
||||
public $maxDepth = 7;
|
||||
|
||||
/** @var int */
|
||||
public $maxLength = 150;
|
||||
|
||||
/** @var int */
|
||||
public $maxItems = 100;
|
||||
|
||||
/** @var Value[] */
|
||||
public $snapshot = [];
|
||||
|
||||
/** @var bool */
|
||||
public $debugInfo = false;
|
||||
|
||||
/** @var array */
|
||||
public $keysToHide = [];
|
||||
|
||||
/** @var callable|null fn(string $key, mixed $val): bool */
|
||||
public $scrubber;
|
||||
|
||||
/** @var bool */
|
||||
public $location = false;
|
||||
|
||||
/** @var callable[] */
|
||||
public $resourceExposers;
|
||||
|
||||
/** @var array<string,callable> */
|
||||
public $objectExposers;
|
||||
|
||||
/** @var (int|\stdClass)[] */
|
||||
public $references = [];
|
||||
|
||||
|
||||
public function describe($var): \stdClass
|
||||
{
|
||||
uksort($this->objectExposers, function ($a, $b): int {
|
||||
return $b === '' || (class_exists($a, false) && is_subclass_of($a, $b)) ? -1 : 1;
|
||||
});
|
||||
|
||||
try {
|
||||
return (object) [
|
||||
'value' => $this->describeVar($var),
|
||||
'snapshot' => $this->snapshot,
|
||||
'location' => $this->location ? self::findLocation() : null,
|
||||
];
|
||||
|
||||
} finally {
|
||||
$free = [[], []];
|
||||
$this->snapshot = &$free[0];
|
||||
$this->references = &$free[1];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return mixed
|
||||
*/
|
||||
private function describeVar($var, int $depth = 0, ?int $refId = null)
|
||||
{
|
||||
if ($var === null || is_bool($var)) {
|
||||
return $var;
|
||||
}
|
||||
|
||||
$m = 'describe' . explode(' ', gettype($var))[0];
|
||||
return $this->$m($var, $depth, $refId);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Value|int
|
||||
*/
|
||||
private function describeInteger(int $num)
|
||||
{
|
||||
return $num <= self::JsSafeInteger && $num >= -self::JsSafeInteger
|
||||
? $num
|
||||
: new Value(Value::TypeNumber, "$num");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Value|float
|
||||
*/
|
||||
private function describeDouble(float $num)
|
||||
{
|
||||
if (!is_finite($num)) {
|
||||
return new Value(Value::TypeNumber, (string) $num);
|
||||
}
|
||||
|
||||
$js = json_encode($num);
|
||||
return strpos($js, '.')
|
||||
? $num
|
||||
: new Value(Value::TypeNumber, "$js.0"); // to distinct int and float in JS
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Value|string
|
||||
*/
|
||||
private function describeString(string $s, int $depth = 0)
|
||||
{
|
||||
$encoded = Helpers::encodeString($s, $depth ? $this->maxLength : null);
|
||||
if ($encoded === $s) {
|
||||
return $encoded;
|
||||
} elseif (Helpers::isUtf8($s)) {
|
||||
return new Value(Value::TypeStringHtml, $encoded, Helpers::utf8Length($s));
|
||||
} else {
|
||||
return new Value(Value::TypeBinaryHtml, $encoded, strlen($s));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Value|array
|
||||
*/
|
||||
private function describeArray(array $arr, int $depth = 0, ?int $refId = null)
|
||||
{
|
||||
if ($refId) {
|
||||
$res = new Value(Value::TypeRef, 'p' . $refId);
|
||||
$value = &$this->snapshot[$res->value];
|
||||
if ($value && $value->depth <= $depth) {
|
||||
return $res;
|
||||
}
|
||||
|
||||
$value = new Value(Value::TypeArray);
|
||||
$value->id = $res->value;
|
||||
$value->depth = $depth;
|
||||
if ($this->maxDepth && $depth >= $this->maxDepth) {
|
||||
$value->length = count($arr);
|
||||
return $res;
|
||||
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
|
||||
$value->length = count($arr);
|
||||
$arr = array_slice($arr, 0, $this->maxItems, true);
|
||||
}
|
||||
|
||||
$items = &$value->items;
|
||||
|
||||
} elseif ($arr && $this->maxDepth && $depth >= $this->maxDepth) {
|
||||
return new Value(Value::TypeArray, null, count($arr));
|
||||
|
||||
} elseif ($depth && $this->maxItems && count($arr) > $this->maxItems) {
|
||||
$res = new Value(Value::TypeArray, null, count($arr));
|
||||
$res->depth = $depth;
|
||||
$items = &$res->items;
|
||||
$arr = array_slice($arr, 0, $this->maxItems, true);
|
||||
}
|
||||
|
||||
$items = [];
|
||||
foreach ($arr as $k => $v) {
|
||||
$refId = $this->getReferenceId($arr, $k);
|
||||
$items[] = [
|
||||
$this->describeVar($k, $depth + 1),
|
||||
$this->isSensitive((string) $k, $v)
|
||||
? new Value(Value::TypeText, self::hideValue($v))
|
||||
: $this->describeVar($v, $depth + 1, $refId),
|
||||
] + ($refId ? [2 => $refId] : []);
|
||||
}
|
||||
|
||||
return $res ?? $items;
|
||||
}
|
||||
|
||||
|
||||
private function describeObject(object $obj, int $depth = 0): Value
|
||||
{
|
||||
$id = spl_object_id($obj);
|
||||
$value = &$this->snapshot[$id];
|
||||
if ($value && $value->depth <= $depth) {
|
||||
return new Value(Value::TypeRef, $id);
|
||||
}
|
||||
|
||||
$value = new Value(Value::TypeObject, Helpers::getClass($obj));
|
||||
$value->id = $id;
|
||||
$value->depth = $depth;
|
||||
$value->holder = $obj; // to be not released by garbage collector in collecting mode
|
||||
if ($this->location) {
|
||||
$rc = $obj instanceof \Closure
|
||||
? new \ReflectionFunction($obj)
|
||||
: new \ReflectionClass($obj);
|
||||
if ($rc->getFileName() && ($editor = Helpers::editorUri($rc->getFileName(), $rc->getStartLine()))) {
|
||||
$value->editor = (object) ['file' => $rc->getFileName(), 'line' => $rc->getStartLine(), 'url' => $editor];
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->maxDepth && $depth < $this->maxDepth) {
|
||||
$value->items = [];
|
||||
$props = $this->exposeObject($obj, $value);
|
||||
foreach ($props ?? [] as $k => $v) {
|
||||
$this->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual, $this->getReferenceId($props, $k));
|
||||
}
|
||||
}
|
||||
|
||||
return new Value(Value::TypeRef, $id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param resource $resource
|
||||
*/
|
||||
private function describeResource($resource, int $depth = 0): Value
|
||||
{
|
||||
$id = 'r' . (int) $resource;
|
||||
$value = &$this->snapshot[$id];
|
||||
if (!$value) {
|
||||
$type = is_resource($resource) ? get_resource_type($resource) : 'closed';
|
||||
$value = new Value(Value::TypeResource, $type . ' resource');
|
||||
$value->id = $id;
|
||||
$value->depth = $depth;
|
||||
$value->items = [];
|
||||
if (isset($this->resourceExposers[$type])) {
|
||||
foreach (($this->resourceExposers[$type])($resource) as $k => $v) {
|
||||
$value->items[] = [htmlspecialchars($k), $this->describeVar($v, $depth + 1)];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new Value(Value::TypeRef, $id);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return Value|string
|
||||
*/
|
||||
public function describeKey(string $key)
|
||||
{
|
||||
if (preg_match('#^[\w!\#$%&*+./;<>?@^{|}~-]{1,50}$#D', $key) && !preg_match('#^(true|false|null)$#iD', $key)) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
$value = $this->describeString($key);
|
||||
return is_string($value) // ensure result is Value
|
||||
? new Value(Value::TypeStringHtml, $key, Helpers::utf8Length($key))
|
||||
: $value;
|
||||
}
|
||||
|
||||
|
||||
public function addPropertyTo(
|
||||
Value $value,
|
||||
string $k,
|
||||
$v,
|
||||
$type = Value::PropertyVirtual,
|
||||
?int $refId = null,
|
||||
?string $class = null
|
||||
) {
|
||||
if ($value->depth && $this->maxItems && count($value->items ?? []) >= $this->maxItems) {
|
||||
$value->length = ($value->length ?? count($value->items)) + 1;
|
||||
return;
|
||||
}
|
||||
|
||||
$class = $class ?? $value->value;
|
||||
$value->items[] = [
|
||||
$this->describeKey($k),
|
||||
$type !== Value::PropertyVirtual && $this->isSensitive($k, $v, $class)
|
||||
? new Value(Value::TypeText, self::hideValue($v))
|
||||
: $this->describeVar($v, $value->depth + 1, $refId),
|
||||
$type === Value::PropertyPrivate ? $class : $type,
|
||||
] + ($refId ? [3 => $refId] : []);
|
||||
}
|
||||
|
||||
|
||||
private function exposeObject(object $obj, Value $value): ?array
|
||||
{
|
||||
foreach ($this->objectExposers as $type => $dumper) {
|
||||
if (!$type || $obj instanceof $type) {
|
||||
return $dumper($obj, $value, $this);
|
||||
}
|
||||
}
|
||||
|
||||
if ($this->debugInfo && method_exists($obj, '__debugInfo')) {
|
||||
return $obj->__debugInfo();
|
||||
}
|
||||
|
||||
Exposer::exposeObject($obj, $value, $this);
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
private function isSensitive(string $key, $val, ?string $class = null): bool
|
||||
{
|
||||
return $val instanceof \SensitiveParameterValue
|
||||
|| ($this->scrubber !== null && ($this->scrubber)($key, $val, $class))
|
||||
|| isset($this->keysToHide[strtolower($key)])
|
||||
|| isset($this->keysToHide[strtolower($class . '::$' . $key)]);
|
||||
}
|
||||
|
||||
|
||||
private static function hideValue($val): string
|
||||
{
|
||||
if ($val instanceof \SensitiveParameterValue) {
|
||||
$val = $val->getValue();
|
||||
}
|
||||
|
||||
return self::HiddenValue . ' (' . (is_object($val) ? Helpers::getClass($val) : gettype($val)) . ')';
|
||||
}
|
||||
|
||||
|
||||
public function getReferenceId($arr, $key): ?int
|
||||
{
|
||||
if (PHP_VERSION_ID >= 70400) {
|
||||
if ((!$rr = \ReflectionReference::fromArrayElement($arr, $key))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$tmp = &$this->references[$rr->getId()];
|
||||
if ($tmp === null) {
|
||||
return $tmp = count($this->references);
|
||||
}
|
||||
|
||||
return $tmp;
|
||||
}
|
||||
|
||||
$uniq = new \stdClass;
|
||||
$copy = $arr;
|
||||
$orig = $copy[$key];
|
||||
$copy[$key] = $uniq;
|
||||
if ($arr[$key] !== $uniq) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$res = array_search($uniq, $this->references, true);
|
||||
$copy[$key] = $orig;
|
||||
if ($res === false) {
|
||||
$this->references[] = &$arr[$key];
|
||||
return count($this->references);
|
||||
}
|
||||
|
||||
return $res + 1;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds the location where dump was called. Returns [file, line, code]
|
||||
*/
|
||||
private static function findLocation(): ?array
|
||||
{
|
||||
foreach (debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) as $item) {
|
||||
if (isset($item['class']) && ($item['class'] === self::class || $item['class'] === Tracy\Dumper::class)) {
|
||||
$location = $item;
|
||||
continue;
|
||||
} elseif (isset($item['function'])) {
|
||||
try {
|
||||
$reflection = isset($item['class'])
|
||||
? new \ReflectionMethod($item['class'], $item['function'])
|
||||
: new \ReflectionFunction($item['function']);
|
||||
if (
|
||||
$reflection->isInternal()
|
||||
|| preg_match('#\s@tracySkipLocation\s#', (string) $reflection->getDocComment())
|
||||
) {
|
||||
$location = $item;
|
||||
continue;
|
||||
}
|
||||
} catch (\ReflectionException $e) {
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
if (isset($location['file'], $location['line']) && @is_file($location['file'])) { // @ - may trigger error
|
||||
$lines = file($location['file']);
|
||||
$line = $lines[$location['line'] - 1];
|
||||
return [
|
||||
$location['file'],
|
||||
$location['line'],
|
||||
trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line),
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
250
libs/Nette/Tracy/Dumper/Dumper.php
Normal file
250
libs/Nette/Tracy/Dumper/Dumper.php
Normal file
@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy;
|
||||
|
||||
use Ds;
|
||||
use Tracy\Dumper\Describer;
|
||||
use Tracy\Dumper\Exposer;
|
||||
use Tracy\Dumper\Renderer;
|
||||
|
||||
|
||||
/**
|
||||
* Dumps a variable.
|
||||
*/
|
||||
class Dumper
|
||||
{
|
||||
public const
|
||||
DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 7)
|
||||
TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150)
|
||||
ITEMS = 'items', // how many items in array/object display? (defaults to 100)
|
||||
COLLAPSE = 'collapse', // collapse top array/object or how big are collapsed? (defaults to 14)
|
||||
COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed in non-lazy mode? (defaults to 7)
|
||||
LOCATION = 'location', // show location string? (defaults to 0)
|
||||
OBJECT_EXPORTERS = 'exporters', // custom exporters for objects (defaults to Dumper::$objectexporters)
|
||||
LAZY = 'lazy', // lazy-loading via JavaScript? true=full, false=none, null=collapsed parts (defaults to null/false)
|
||||
LIVE = 'live', // use static $liveSnapshot (used by Bar)
|
||||
SNAPSHOT = 'snapshot', // array used for shared snapshot for lazy-loading via JavaScript
|
||||
DEBUGINFO = 'debuginfo', // use magic method __debugInfo if exists (defaults to false)
|
||||
KEYS_TO_HIDE = 'keystohide', // sensitive keys not displayed (defaults to [])
|
||||
SCRUBBER = 'scrubber', // detects sensitive keys not to be displayed
|
||||
THEME = 'theme', // color theme (defaults to light)
|
||||
HASH = 'hash'; // show object and reference hashes (defaults to true)
|
||||
|
||||
public const
|
||||
LOCATION_CLASS = 0b0001, // shows where classes are defined
|
||||
LOCATION_SOURCE = 0b0011, // additionally shows where dump was called
|
||||
LOCATION_LINK = self::LOCATION_SOURCE; // deprecated
|
||||
|
||||
public const HIDDEN_VALUE = Describer::HiddenValue;
|
||||
|
||||
/** @var Dumper\Value[] */
|
||||
public static $liveSnapshot = [];
|
||||
|
||||
/** @var array */
|
||||
public static $terminalColors = [
|
||||
'bool' => '1;33',
|
||||
'null' => '1;33',
|
||||
'number' => '1;32',
|
||||
'string' => '1;36',
|
||||
'array' => '1;31',
|
||||
'public' => '1;37',
|
||||
'protected' => '1;37',
|
||||
'private' => '1;37',
|
||||
'dynamic' => '1;37',
|
||||
'virtual' => '1;37',
|
||||
'object' => '1;31',
|
||||
'resource' => '1;37',
|
||||
'indent' => '1;30',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
public static $resources = [
|
||||
'stream' => 'stream_get_meta_data',
|
||||
'stream-context' => 'stream_context_get_options',
|
||||
'curl' => 'curl_getinfo',
|
||||
];
|
||||
|
||||
/** @var array */
|
||||
public static $objectExporters = [
|
||||
\Closure::class => [Exposer::class, 'exposeClosure'],
|
||||
\UnitEnum::class => [Exposer::class, 'exposeEnum'],
|
||||
\ArrayObject::class => [Exposer::class, 'exposeArrayObject'],
|
||||
\SplFileInfo::class => [Exposer::class, 'exposeSplFileInfo'],
|
||||
\SplObjectStorage::class => [Exposer::class, 'exposeSplObjectStorage'],
|
||||
\__PHP_Incomplete_Class::class => [Exposer::class, 'exposePhpIncompleteClass'],
|
||||
\Generator::class => [Exposer::class, 'exposeGenerator'],
|
||||
\Fiber::class => [Exposer::class, 'exposeFiber'],
|
||||
\DOMNode::class => [Exposer::class, 'exposeDOMNode'],
|
||||
\DOMNodeList::class => [Exposer::class, 'exposeDOMNodeList'],
|
||||
\DOMNamedNodeMap::class => [Exposer::class, 'exposeDOMNodeList'],
|
||||
Ds\Collection::class => [Exposer::class, 'exposeDsCollection'],
|
||||
Ds\Map::class => [Exposer::class, 'exposeDsMap'],
|
||||
];
|
||||
|
||||
/** @var Describer */
|
||||
private $describer;
|
||||
|
||||
/** @var Renderer */
|
||||
private $renderer;
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to the output.
|
||||
* @return mixed variable
|
||||
*/
|
||||
public static function dump($var, array $options = [])
|
||||
{
|
||||
if (Helpers::isCli()) {
|
||||
$useColors = self::$terminalColors && Helpers::detectColors();
|
||||
$dumper = new self($options);
|
||||
fwrite(STDOUT, $dumper->asTerminal($var, $useColors ? self::$terminalColors : []));
|
||||
|
||||
} elseif (Helpers::isHtmlMode()) {
|
||||
$options[self::LOCATION] = $options[self::LOCATION] ?? true;
|
||||
self::renderAssets();
|
||||
echo self::toHtml($var, $options);
|
||||
|
||||
} else {
|
||||
echo self::toText($var, $options);
|
||||
}
|
||||
|
||||
return $var;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to HTML.
|
||||
*/
|
||||
public static function toHtml($var, array $options = [], $key = null): string
|
||||
{
|
||||
return (new self($options))->asHtml($var, $key);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to plain text.
|
||||
*/
|
||||
public static function toText($var, array $options = []): string
|
||||
{
|
||||
return (new self($options))->asTerminal($var);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to x-terminal.
|
||||
*/
|
||||
public static function toTerminal($var, array $options = []): string
|
||||
{
|
||||
return (new self($options))->asTerminal($var, self::$terminalColors);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Renders <script> & <style>
|
||||
*/
|
||||
public static function renderAssets(): void
|
||||
{
|
||||
static $sent;
|
||||
if (Debugger::$productionMode === true || $sent) {
|
||||
return;
|
||||
}
|
||||
|
||||
$sent = true;
|
||||
|
||||
$nonce = Helpers::getNonce();
|
||||
$nonceAttr = $nonce ? ' nonce="' . Helpers::escapeHtml($nonce) . '"' : '';
|
||||
$s = file_get_contents(__DIR__ . '/../assets/toggle.css')
|
||||
. file_get_contents(__DIR__ . '/assets/dumper-light.css')
|
||||
. file_get_contents(__DIR__ . '/assets/dumper-dark.css');
|
||||
echo "<style{$nonceAttr}>", str_replace('</', '<\/', Helpers::minifyCss($s)), "</style>\n";
|
||||
|
||||
if (!Debugger::isEnabled()) {
|
||||
$s = '(function(){' . file_get_contents(__DIR__ . '/../assets/toggle.js') . '})();'
|
||||
. '(function(){' . file_get_contents(__DIR__ . '/../Dumper/assets/dumper.js') . '})();';
|
||||
echo "<script{$nonceAttr}>", str_replace(['<!--', '</s'], ['<\!--', '<\/s'], Helpers::minifyJs($s)), "</script>\n";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function __construct(array $options = [])
|
||||
{
|
||||
$location = $options[self::LOCATION] ?? 0;
|
||||
$location = $location === true ? ~0 : (int) $location;
|
||||
|
||||
$describer = $this->describer = new Describer;
|
||||
$describer->maxDepth = (int) ($options[self::DEPTH] ?? $describer->maxDepth);
|
||||
$describer->maxLength = (int) ($options[self::TRUNCATE] ?? $describer->maxLength);
|
||||
$describer->maxItems = (int) ($options[self::ITEMS] ?? $describer->maxItems);
|
||||
$describer->debugInfo = (bool) ($options[self::DEBUGINFO] ?? $describer->debugInfo);
|
||||
$describer->scrubber = $options[self::SCRUBBER] ?? $describer->scrubber;
|
||||
$describer->keysToHide = array_flip(array_map('strtolower', $options[self::KEYS_TO_HIDE] ?? []));
|
||||
$describer->resourceExposers = ($options['resourceExporters'] ?? []) + self::$resources;
|
||||
$describer->objectExposers = ($options[self::OBJECT_EXPORTERS] ?? []) + self::$objectExporters;
|
||||
$describer->location = (bool) $location;
|
||||
if ($options[self::LIVE] ?? false) {
|
||||
$tmp = &self::$liveSnapshot;
|
||||
} elseif (isset($options[self::SNAPSHOT])) {
|
||||
$tmp = &$options[self::SNAPSHOT];
|
||||
}
|
||||
|
||||
if (isset($tmp)) {
|
||||
$tmp[0] = $tmp[0] ?? [];
|
||||
$tmp[1] = $tmp[1] ?? [];
|
||||
$describer->snapshot = &$tmp[0];
|
||||
$describer->references = &$tmp[1];
|
||||
}
|
||||
|
||||
$renderer = $this->renderer = new Renderer;
|
||||
$renderer->collapseTop = $options[self::COLLAPSE] ?? $renderer->collapseTop;
|
||||
$renderer->collapseSub = $options[self::COLLAPSE_COUNT] ?? $renderer->collapseSub;
|
||||
$renderer->collectingMode = isset($options[self::SNAPSHOT]) || !empty($options[self::LIVE]);
|
||||
$renderer->lazy = $renderer->collectingMode
|
||||
? true
|
||||
: ($options[self::LAZY] ?? $renderer->lazy);
|
||||
$renderer->sourceLocation = !(~$location & self::LOCATION_SOURCE);
|
||||
$renderer->classLocation = !(~$location & self::LOCATION_CLASS);
|
||||
$renderer->theme = $options[self::THEME] ?? $renderer->theme;
|
||||
$renderer->hash = $options[self::HASH] ?? true;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to HTML.
|
||||
*/
|
||||
private function asHtml($var, $key = null): string
|
||||
{
|
||||
if ($key === null) {
|
||||
$model = $this->describer->describe($var);
|
||||
} else {
|
||||
$model = $this->describer->describe([$key => $var]);
|
||||
$model->value = $model->value[0][1];
|
||||
}
|
||||
|
||||
return $this->renderer->renderAsHtml($model);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dumps variable to x-terminal.
|
||||
*/
|
||||
private function asTerminal($var, array $colors = []): string
|
||||
{
|
||||
$model = $this->describer->describe($var);
|
||||
return $this->renderer->renderAsText($model, $colors);
|
||||
}
|
||||
|
||||
|
||||
public static function formatSnapshotAttribute(array &$snapshot): string
|
||||
{
|
||||
$res = "'" . Renderer::jsonEncode($snapshot[0] ?? []) . "'";
|
||||
$snapshot = [];
|
||||
return $res;
|
||||
}
|
||||
}
|
251
libs/Nette/Tracy/Dumper/Exposer.php
Normal file
251
libs/Nette/Tracy/Dumper/Exposer.php
Normal file
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Dumper;
|
||||
use Ds;
|
||||
|
||||
|
||||
/**
|
||||
* Exposes internal PHP objects.
|
||||
* @internal
|
||||
*/
|
||||
final class Exposer
|
||||
{
|
||||
public static function exposeObject(object $obj, Value $value, Describer $describer): void
|
||||
{
|
||||
$tmp = (array) $obj;
|
||||
$values = $tmp; // bug #79477, PHP < 7.4.6
|
||||
$props = self::getProperties(get_class($obj));
|
||||
|
||||
foreach (array_diff_key($values, $props) as $k => $v) {
|
||||
$describer->addPropertyTo(
|
||||
$value,
|
||||
(string) $k,
|
||||
$v,
|
||||
Value::PropertyDynamic,
|
||||
$describer->getReferenceId($values, $k)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ($props as $k => [$name, $class, $type]) {
|
||||
if (array_key_exists($k, $values)) {
|
||||
$describer->addPropertyTo(
|
||||
$value,
|
||||
$name,
|
||||
$values[$k],
|
||||
$type,
|
||||
$describer->getReferenceId($values, $k),
|
||||
$class
|
||||
);
|
||||
} else {
|
||||
$value->items[] = [
|
||||
$name,
|
||||
new Value(Value::TypeText, 'unset'),
|
||||
$type === Value::PropertyPrivate ? $class : $type,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function getProperties($class): array
|
||||
{
|
||||
static $cache;
|
||||
if (isset($cache[$class])) {
|
||||
return $cache[$class];
|
||||
}
|
||||
|
||||
$rc = new \ReflectionClass($class);
|
||||
$parentProps = $rc->getParentClass() ? self::getProperties($rc->getParentClass()->getName()) : [];
|
||||
$props = [];
|
||||
|
||||
foreach ($rc->getProperties() as $prop) {
|
||||
$name = $prop->getName();
|
||||
if ($prop->isStatic() || $prop->getDeclaringClass()->getName() !== $class) {
|
||||
// nothing
|
||||
} elseif ($prop->isPrivate()) {
|
||||
$props["\x00" . $class . "\x00" . $name] = [$name, $class, Value::PropertyPrivate];
|
||||
} elseif ($prop->isProtected()) {
|
||||
$props["\x00*\x00" . $name] = [$name, $class, Value::PropertyProtected];
|
||||
} else {
|
||||
$props[$name] = [$name, $class, Value::PropertyPublic];
|
||||
unset($parentProps["\x00*\x00" . $name]);
|
||||
}
|
||||
}
|
||||
|
||||
return $cache[$class] = $props + $parentProps;
|
||||
}
|
||||
|
||||
|
||||
public static function exposeClosure(\Closure $obj, Value $value, Describer $describer): void
|
||||
{
|
||||
$rc = new \ReflectionFunction($obj);
|
||||
if ($describer->location) {
|
||||
$describer->addPropertyTo($value, 'file', $rc->getFileName() . ':' . $rc->getStartLine());
|
||||
}
|
||||
|
||||
$params = [];
|
||||
foreach ($rc->getParameters() as $param) {
|
||||
$params[] = '$' . $param->getName();
|
||||
}
|
||||
|
||||
$value->value .= '(' . implode(', ', $params) . ')';
|
||||
|
||||
$uses = [];
|
||||
$useValue = new Value(Value::TypeObject);
|
||||
$useValue->depth = $value->depth + 1;
|
||||
foreach ($rc->getStaticVariables() as $name => $v) {
|
||||
$uses[] = '$' . $name;
|
||||
$describer->addPropertyTo($useValue, '$' . $name, $v);
|
||||
}
|
||||
|
||||
if ($uses) {
|
||||
$useValue->value = implode(', ', $uses);
|
||||
$useValue->collapsed = true;
|
||||
$value->items[] = ['use', $useValue];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function exposeEnum(\UnitEnum $enum, Value $value, Describer $describer): void
|
||||
{
|
||||
$value->value = get_class($enum) . '::' . $enum->name;
|
||||
if ($enum instanceof \BackedEnum) {
|
||||
$describer->addPropertyTo($value, 'value', $enum->value);
|
||||
$value->collapsed = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function exposeArrayObject(\ArrayObject $obj, Value $value, Describer $describer): void
|
||||
{
|
||||
$flags = $obj->getFlags();
|
||||
$obj->setFlags(\ArrayObject::STD_PROP_LIST);
|
||||
self::exposeObject($obj, $value, $describer);
|
||||
$obj->setFlags($flags);
|
||||
$describer->addPropertyTo($value, 'storage', $obj->getArrayCopy(), Value::PropertyPrivate, null, \ArrayObject::class);
|
||||
}
|
||||
|
||||
|
||||
public static function exposeDOMNode(\DOMNode $obj, Value $value, Describer $describer): void
|
||||
{
|
||||
$props = preg_match_all('#^\s*\[([^\]]+)\] =>#m', print_r($obj, true), $tmp) ? $tmp[1] : [];
|
||||
sort($props);
|
||||
foreach ($props as $p) {
|
||||
$describer->addPropertyTo($value, $p, $obj->$p, Value::PropertyPublic);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param \DOMNodeList|\DOMNamedNodeMap $obj
|
||||
*/
|
||||
public static function exposeDOMNodeList($obj, Value $value, Describer $describer): void
|
||||
{
|
||||
$describer->addPropertyTo($value, 'length', $obj->length, Value::PropertyPublic);
|
||||
$describer->addPropertyTo($value, 'items', iterator_to_array($obj));
|
||||
}
|
||||
|
||||
|
||||
public static function exposeGenerator(\Generator $gen, Value $value, Describer $describer): void
|
||||
{
|
||||
try {
|
||||
$r = new \ReflectionGenerator($gen);
|
||||
$describer->addPropertyTo($value, 'file', $r->getExecutingFile() . ':' . $r->getExecutingLine());
|
||||
$describer->addPropertyTo($value, 'this', $r->getThis());
|
||||
} catch (\ReflectionException $e) {
|
||||
$value->value = get_class($gen) . ' (terminated)';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function exposeFiber(\Fiber $fiber, Value $value, Describer $describer): void
|
||||
{
|
||||
if ($fiber->isTerminated()) {
|
||||
$value->value = get_class($fiber) . ' (terminated)';
|
||||
} elseif (!$fiber->isStarted()) {
|
||||
$value->value = get_class($fiber) . ' (not started)';
|
||||
} else {
|
||||
$r = new \ReflectionFiber($fiber);
|
||||
$describer->addPropertyTo($value, 'file', $r->getExecutingFile() . ':' . $r->getExecutingLine());
|
||||
$describer->addPropertyTo($value, 'callable', $r->getCallable());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function exposeSplFileInfo(\SplFileInfo $obj): array
|
||||
{
|
||||
return ['path' => $obj->getPathname()];
|
||||
}
|
||||
|
||||
|
||||
public static function exposeSplObjectStorage(\SplObjectStorage $obj): array
|
||||
{
|
||||
$res = [];
|
||||
foreach (clone $obj as $item) {
|
||||
$res[] = ['object' => $item, 'data' => $obj[$item]];
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
|
||||
|
||||
public static function exposePhpIncompleteClass(
|
||||
\__PHP_Incomplete_Class $obj,
|
||||
Value $value,
|
||||
Describer $describer
|
||||
): void
|
||||
{
|
||||
$values = (array) $obj;
|
||||
$class = $values['__PHP_Incomplete_Class_Name'];
|
||||
unset($values['__PHP_Incomplete_Class_Name']);
|
||||
foreach ($values as $k => $v) {
|
||||
$refId = $describer->getReferenceId($values, $k);
|
||||
if (isset($k[0]) && $k[0] === "\x00") {
|
||||
$info = explode("\00", $k);
|
||||
$k = end($info);
|
||||
$type = $info[1] === '*' ? Value::PropertyProtected : Value::PropertyPrivate;
|
||||
$decl = $type === Value::PropertyPrivate ? $info[1] : null;
|
||||
} else {
|
||||
$type = Value::PropertyPublic;
|
||||
$k = (string) $k;
|
||||
$decl = null;
|
||||
}
|
||||
|
||||
$describer->addPropertyTo($value, $k, $v, $type, $refId, $decl);
|
||||
}
|
||||
|
||||
$value->value = $class . ' (Incomplete Class)';
|
||||
}
|
||||
|
||||
|
||||
public static function exposeDsCollection(
|
||||
Ds\Collection $obj,
|
||||
Value $value,
|
||||
Describer $describer
|
||||
): void
|
||||
{
|
||||
foreach (clone $obj as $k => $v) {
|
||||
$describer->addPropertyTo($value, (string) $k, $v, Value::PropertyVirtual);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function exposeDsMap(
|
||||
Ds\Map $obj,
|
||||
Value $value,
|
||||
Describer $describer
|
||||
): void
|
||||
{
|
||||
$i = 0;
|
||||
foreach ($obj as $k => $v) {
|
||||
$describer->addPropertyTo($value, (string) $i++, new Ds\Pair($k, $v), Value::PropertyVirtual);
|
||||
}
|
||||
}
|
||||
}
|
501
libs/Nette/Tracy/Dumper/Renderer.php
Normal file
501
libs/Nette/Tracy/Dumper/Renderer.php
Normal file
@ -0,0 +1,501 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Dumper;
|
||||
|
||||
use Tracy\Helpers;
|
||||
|
||||
|
||||
/**
|
||||
* Visualisation of internal representation.
|
||||
* @internal
|
||||
*/
|
||||
final class Renderer
|
||||
{
|
||||
private const TypeArrayKey = 'array';
|
||||
|
||||
/** @var int|bool */
|
||||
public $collapseTop = 14;
|
||||
|
||||
/** @var int */
|
||||
public $collapseSub = 7;
|
||||
|
||||
/** @var bool */
|
||||
public $classLocation = false;
|
||||
|
||||
/** @var bool */
|
||||
public $sourceLocation = false;
|
||||
|
||||
/** @var bool|null lazy-loading via JavaScript? true=full, false=none, null=collapsed parts */
|
||||
public $lazy;
|
||||
|
||||
/** @var bool */
|
||||
public $hash = true;
|
||||
|
||||
/** @var string */
|
||||
public $theme = 'light';
|
||||
|
||||
/** @var bool */
|
||||
public $collectingMode = false;
|
||||
|
||||
/** @var Value[] */
|
||||
private $snapshot = [];
|
||||
|
||||
/** @var Value[]|null */
|
||||
private $snapshotSelection;
|
||||
|
||||
/** @var array */
|
||||
private $parents = [];
|
||||
|
||||
/** @var array */
|
||||
private $above = [];
|
||||
|
||||
|
||||
public function renderAsHtml(\stdClass $model): string
|
||||
{
|
||||
try {
|
||||
$value = $model->value;
|
||||
$this->snapshot = $model->snapshot;
|
||||
|
||||
if ($this->lazy === false) { // no lazy-loading
|
||||
$html = $this->renderVar($value);
|
||||
$json = $snapshot = null;
|
||||
|
||||
} elseif ($this->lazy && (is_array($value) && $value || is_object($value))) { // full lazy-loading
|
||||
$html = '';
|
||||
$snapshot = $this->collectingMode ? null : $this->snapshot;
|
||||
$json = $value;
|
||||
|
||||
} else { // lazy-loading of collapsed parts
|
||||
$html = $this->renderVar($value);
|
||||
$snapshot = $this->snapshotSelection;
|
||||
$json = null;
|
||||
}
|
||||
} finally {
|
||||
$this->parents = $this->snapshot = $this->above = [];
|
||||
$this->snapshotSelection = null;
|
||||
}
|
||||
|
||||
$location = null;
|
||||
if ($model->location && $this->sourceLocation) {
|
||||
[$file, $line, $code] = $model->location;
|
||||
$uri = Helpers::editorUri($file, $line);
|
||||
$location = Helpers::formatHtml(
|
||||
'<a href="%" class="tracy-dump-location" title="in file % on line %%">',
|
||||
$uri ?? '#',
|
||||
$file,
|
||||
$line,
|
||||
$uri ? "\nClick to open in editor" : ''
|
||||
) . Helpers::encodeString($code, 50) . " 📍</a\n>";
|
||||
}
|
||||
|
||||
return '<pre class="tracy-dump' . ($this->theme ? ' tracy-' . htmlspecialchars($this->theme) : '')
|
||||
. ($json && $this->collapseTop === true ? ' tracy-collapsed' : '') . '"'
|
||||
. ($snapshot !== null ? " data-tracy-snapshot='" . self::jsonEncode($snapshot) . "'" : '')
|
||||
. ($json ? " data-tracy-dump='" . self::jsonEncode($json) . "'" : '')
|
||||
. ($location || strlen($html) > 100 ? "\n" : '')
|
||||
. '>'
|
||||
. $location
|
||||
. $html
|
||||
. "</pre>\n";
|
||||
}
|
||||
|
||||
|
||||
public function renderAsText(\stdClass $model, array $colors = []): string
|
||||
{
|
||||
try {
|
||||
$this->snapshot = $model->snapshot;
|
||||
$this->lazy = false;
|
||||
$s = $this->renderVar($model->value);
|
||||
} finally {
|
||||
$this->parents = $this->snapshot = $this->above = [];
|
||||
}
|
||||
|
||||
$s = $colors ? self::htmlToAnsi($s, $colors) : $s;
|
||||
$s = htmlspecialchars_decode(strip_tags($s), ENT_QUOTES | ENT_HTML5);
|
||||
$s = str_replace('…', '...', $s);
|
||||
$s .= substr($s, -1) === "\n" ? '' : "\n";
|
||||
|
||||
if ($this->sourceLocation && ([$file, $line] = $model->location)) {
|
||||
$s .= "in $file:$line\n";
|
||||
}
|
||||
|
||||
return $s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @param string|int|null $keyType
|
||||
*/
|
||||
private function renderVar($value, int $depth = 0, $keyType = null): string
|
||||
{
|
||||
switch (true) {
|
||||
case $value === null:
|
||||
return '<span class="tracy-dump-null">null</span>';
|
||||
|
||||
case is_bool($value):
|
||||
return '<span class="tracy-dump-bool">' . ($value ? 'true' : 'false') . '</span>';
|
||||
|
||||
case is_int($value):
|
||||
return '<span class="tracy-dump-number">' . $value . '</span>';
|
||||
|
||||
case is_float($value):
|
||||
return '<span class="tracy-dump-number">' . self::jsonEncode($value) . '</span>';
|
||||
|
||||
case is_string($value):
|
||||
return $this->renderString($value, $depth, $keyType);
|
||||
|
||||
case is_array($value):
|
||||
case $value->type === Value::TypeArray:
|
||||
return $this->renderArray($value, $depth);
|
||||
|
||||
case $value->type === Value::TypeRef:
|
||||
return $this->renderVar($this->snapshot[$value->value], $depth, $keyType);
|
||||
|
||||
case $value->type === Value::TypeObject:
|
||||
return $this->renderObject($value, $depth);
|
||||
|
||||
case $value->type === Value::TypeNumber:
|
||||
return '<span class="tracy-dump-number">' . Helpers::escapeHtml($value->value) . '</span>';
|
||||
|
||||
case $value->type === Value::TypeText:
|
||||
return '<span class="tracy-dump-virtual">' . Helpers::escapeHtml($value->value) . '</span>';
|
||||
|
||||
case $value->type === Value::TypeStringHtml:
|
||||
case $value->type === Value::TypeBinaryHtml:
|
||||
return $this->renderString($value, $depth, $keyType);
|
||||
|
||||
case $value->type === Value::TypeResource:
|
||||
return $this->renderResource($value, $depth);
|
||||
|
||||
default:
|
||||
throw new \Exception('Unknown type');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param string|Value $str
|
||||
* @param string|int|null $keyType
|
||||
*/
|
||||
private function renderString($str, int $depth, $keyType): string
|
||||
{
|
||||
if ($keyType === self::TypeArrayKey) {
|
||||
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth - 1) . ' </span>';
|
||||
return '<span class="tracy-dump-string">'
|
||||
. "<span class='tracy-dump-lq'>'</span>"
|
||||
. (is_string($str) ? Helpers::escapeHtml($str) : str_replace("\n", "\n" . $indent, $str->value))
|
||||
. "<span>'</span>"
|
||||
. '</span>';
|
||||
|
||||
} elseif ($keyType !== null) {
|
||||
$classes = [
|
||||
Value::PropertyPublic => 'tracy-dump-public',
|
||||
Value::PropertyProtected => 'tracy-dump-protected',
|
||||
Value::PropertyDynamic => 'tracy-dump-dynamic',
|
||||
Value::PropertyVirtual => 'tracy-dump-virtual',
|
||||
];
|
||||
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth - 1) . ' </span>';
|
||||
$title = is_string($keyType)
|
||||
? ' title="declared in ' . Helpers::escapeHtml($keyType) . '"'
|
||||
: null;
|
||||
return '<span class="'
|
||||
. ($title ? 'tracy-dump-private' : $classes[$keyType]) . '"' . $title . '>'
|
||||
. (is_string($str)
|
||||
? Helpers::escapeHtml($str)
|
||||
: "<span class='tracy-dump-lq'>'</span>" . str_replace("\n", "\n" . $indent, $str->value) . "<span>'</span>")
|
||||
. '</span>';
|
||||
|
||||
} elseif (is_string($str)) {
|
||||
$len = Helpers::utf8Length($str);
|
||||
return '<span class="tracy-dump-string"'
|
||||
. ($len > 1 ? ' title="' . $len . ' characters"' : '')
|
||||
. '>'
|
||||
. "<span>'</span>"
|
||||
. Helpers::escapeHtml($str)
|
||||
. "<span>'</span>"
|
||||
. '</span>';
|
||||
|
||||
} else {
|
||||
$unit = $str->type === Value::TypeStringHtml ? 'characters' : 'bytes';
|
||||
$count = substr_count($str->value, "\n");
|
||||
if ($count) {
|
||||
$collapsed = $indent1 = $toggle = null;
|
||||
$indent = '<span class="tracy-dump-indent"> </span>';
|
||||
if ($depth) {
|
||||
$collapsed = $count >= $this->collapseSub;
|
||||
$indent1 = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
|
||||
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . ' </span>';
|
||||
$toggle = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '">string</span>' . "\n";
|
||||
}
|
||||
|
||||
return $toggle
|
||||
. '<div class="tracy-dump-string' . ($collapsed ? ' tracy-collapsed' : '')
|
||||
. '" title="' . $str->length . ' ' . $unit . '">'
|
||||
. $indent1
|
||||
. '<span' . ($count ? ' class="tracy-dump-lq"' : '') . ">'</span>"
|
||||
. str_replace("\n", "\n" . $indent, $str->value)
|
||||
. "<span>'</span>"
|
||||
. ($depth ? "\n" : '')
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
return '<span class="tracy-dump-string"'
|
||||
. ($str->length > 1 ? " title=\"{$str->length} $unit\"" : '')
|
||||
. '>'
|
||||
. "<span>'</span>"
|
||||
. $str->value
|
||||
. "<span>'</span>"
|
||||
. '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param array|Value $array
|
||||
*/
|
||||
private function renderArray($array, int $depth): string
|
||||
{
|
||||
$out = '<span class="tracy-dump-array">array</span> (';
|
||||
|
||||
if (is_array($array)) {
|
||||
$items = $array;
|
||||
$count = count($items);
|
||||
$out .= $count . ')';
|
||||
} elseif ($array->items === null) {
|
||||
return $out . $array->length . ') …';
|
||||
} else {
|
||||
$items = $array->items;
|
||||
$count = $array->length ?? count($items);
|
||||
$out .= $count . ')';
|
||||
if ($array->id && isset($this->parents[$array->id])) {
|
||||
return $out . ' <i>RECURSION</i>';
|
||||
|
||||
} elseif ($array->id && ($array->depth < $depth || isset($this->above[$array->id]))) {
|
||||
if ($this->lazy !== false) {
|
||||
$ref = new Value(Value::TypeRef, $array->id);
|
||||
$this->copySnapshot($ref);
|
||||
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
|
||||
|
||||
} elseif ($this->hash) {
|
||||
return $out . (isset($this->above[$array->id]) ? ' <i>see above</i>' : ' <i>see below</i>');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$count) {
|
||||
return $out;
|
||||
}
|
||||
|
||||
$collapsed = $depth
|
||||
? ($this->lazy === false || $depth === 1 ? $count >= $this->collapseSub : true)
|
||||
: (is_int($this->collapseTop) ? $count >= $this->collapseTop : $this->collapseTop);
|
||||
|
||||
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
|
||||
|
||||
if ($collapsed && $this->lazy !== false) {
|
||||
$array = isset($array->id) ? new Value(Value::TypeRef, $array->id) : $array;
|
||||
$this->copySnapshot($array);
|
||||
return $span . " data-tracy-dump='" . self::jsonEncode($array) . "'>" . $out . '</span>';
|
||||
}
|
||||
|
||||
$out = $span . '>' . $out . "</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
|
||||
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
|
||||
$this->parents[$array->id ?? null] = $this->above[$array->id ?? null] = true;
|
||||
|
||||
foreach ($items as $info) {
|
||||
[$k, $v, $ref] = $info + [2 => null];
|
||||
$out .= $indent
|
||||
. $this->renderVar($k, $depth + 1, self::TypeArrayKey)
|
||||
. ' => '
|
||||
. ($ref && $this->hash ? '<span class="tracy-dump-hash">&' . $ref . '</span> ' : '')
|
||||
. ($tmp = $this->renderVar($v, $depth + 1))
|
||||
. (substr($tmp, -6) === '</div>' ? '' : "\n");
|
||||
}
|
||||
|
||||
if ($count > count($items)) {
|
||||
$out .= $indent . "…\n";
|
||||
}
|
||||
|
||||
unset($this->parents[$array->id ?? null]);
|
||||
return $out . '</div>';
|
||||
}
|
||||
|
||||
|
||||
private function renderObject(Value $object, int $depth): string
|
||||
{
|
||||
$editorAttributes = '';
|
||||
if ($this->classLocation && $object->editor) {
|
||||
$editorAttributes = Helpers::formatHtml(
|
||||
' title="Declared in file % on line %%%" data-tracy-href="%"',
|
||||
$object->editor->file,
|
||||
$object->editor->line,
|
||||
$object->editor->url ? "\nCtrl-Click to open in editor" : '',
|
||||
"\nAlt-Click to expand/collapse all child nodes",
|
||||
$object->editor->url
|
||||
);
|
||||
}
|
||||
|
||||
$pos = strrpos($object->value, '\\');
|
||||
$out = '<span class="tracy-dump-object"' . $editorAttributes . '>'
|
||||
. ($pos
|
||||
? Helpers::escapeHtml(substr($object->value, 0, $pos + 1)) . '<b>' . Helpers::escapeHtml(substr($object->value, $pos + 1)) . '</b>'
|
||||
: Helpers::escapeHtml($object->value))
|
||||
. '</span>'
|
||||
. ($object->id && $this->hash ? ' <span class="tracy-dump-hash">#' . $object->id . '</span>' : '');
|
||||
|
||||
if ($object->items === null) {
|
||||
return $out . ' …';
|
||||
|
||||
} elseif (!$object->items) {
|
||||
return $out;
|
||||
|
||||
} elseif ($object->id && isset($this->parents[$object->id])) {
|
||||
return $out . ' <i>RECURSION</i>';
|
||||
|
||||
} elseif ($object->id && ($object->depth < $depth || isset($this->above[$object->id]))) {
|
||||
if ($this->lazy !== false) {
|
||||
$ref = new Value(Value::TypeRef, $object->id);
|
||||
$this->copySnapshot($ref);
|
||||
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
|
||||
|
||||
} elseif ($this->hash) {
|
||||
return $out . (isset($this->above[$object->id]) ? ' <i>see above</i>' : ' <i>see below</i>');
|
||||
}
|
||||
}
|
||||
|
||||
$collapsed = $object->collapsed ?? ($depth
|
||||
? ($this->lazy === false || $depth === 1 ? count($object->items) >= $this->collapseSub : true)
|
||||
: (is_int($this->collapseTop) ? count($object->items) >= $this->collapseTop : $this->collapseTop));
|
||||
|
||||
$span = '<span class="tracy-toggle' . ($collapsed ? ' tracy-collapsed' : '') . '"';
|
||||
|
||||
if ($collapsed && $this->lazy !== false) {
|
||||
$value = $object->id ? new Value(Value::TypeRef, $object->id) : $object;
|
||||
$this->copySnapshot($value);
|
||||
return $span . " data-tracy-dump='" . self::jsonEncode($value) . "'>" . $out . '</span>';
|
||||
}
|
||||
|
||||
$out = $span . '>' . $out . "</span>\n" . '<div' . ($collapsed ? ' class="tracy-collapsed"' : '') . '>';
|
||||
$indent = '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>';
|
||||
$this->parents[$object->id] = $this->above[$object->id] = true;
|
||||
|
||||
foreach ($object->items as $info) {
|
||||
[$k, $v, $type, $ref] = $info + [2 => Value::PropertyVirtual, null];
|
||||
$out .= $indent
|
||||
. $this->renderVar($k, $depth + 1, $type)
|
||||
. ': '
|
||||
. ($ref && $this->hash ? '<span class="tracy-dump-hash">&' . $ref . '</span> ' : '')
|
||||
. ($tmp = $this->renderVar($v, $depth + 1))
|
||||
. (substr($tmp, -6) === '</div>' ? '' : "\n");
|
||||
}
|
||||
|
||||
if ($object->length > count($object->items)) {
|
||||
$out .= $indent . "…\n";
|
||||
}
|
||||
|
||||
unset($this->parents[$object->id]);
|
||||
return $out . '</div>';
|
||||
}
|
||||
|
||||
|
||||
private function renderResource(Value $resource, int $depth): string
|
||||
{
|
||||
$out = '<span class="tracy-dump-resource">' . Helpers::escapeHtml($resource->value) . '</span> '
|
||||
. ($this->hash ? '<span class="tracy-dump-hash">@' . substr($resource->id, 1) . '</span>' : '');
|
||||
|
||||
if (!$resource->items) {
|
||||
return $out;
|
||||
|
||||
} elseif (isset($this->above[$resource->id])) {
|
||||
if ($this->lazy !== false) {
|
||||
$ref = new Value(Value::TypeRef, $resource->id);
|
||||
$this->copySnapshot($ref);
|
||||
return '<span class="tracy-toggle tracy-collapsed" data-tracy-dump=\'' . json_encode($ref) . "'>" . $out . '</span>';
|
||||
}
|
||||
|
||||
return $out . ' <i>see above</i>';
|
||||
|
||||
} else {
|
||||
$this->above[$resource->id] = true;
|
||||
$out = "<span class=\"tracy-toggle tracy-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">";
|
||||
foreach ($resource->items as [$k, $v]) {
|
||||
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $depth) . '</span>'
|
||||
. $this->renderVar($k, $depth + 1, Value::PropertyVirtual)
|
||||
. ': '
|
||||
. ($tmp = $this->renderVar($v, $depth + 1))
|
||||
. (substr($tmp, -6) === '</div>' ? '' : "\n");
|
||||
}
|
||||
|
||||
return $out . '</div>';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private function copySnapshot($value): void
|
||||
{
|
||||
if ($this->collectingMode) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($this->snapshotSelection === null) {
|
||||
$this->snapshotSelection = [];
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as [, $v]) {
|
||||
$this->copySnapshot($v);
|
||||
}
|
||||
} elseif ($value instanceof Value && $value->type === Value::TypeRef) {
|
||||
if (!isset($this->snapshotSelection[$value->value])) {
|
||||
$ref = $this->snapshotSelection[$value->value] = $this->snapshot[$value->value];
|
||||
$this->copySnapshot($ref);
|
||||
}
|
||||
} elseif ($value instanceof Value && $value->items) {
|
||||
foreach ($value->items as [, $v]) {
|
||||
$this->copySnapshot($v);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static function jsonEncode($snapshot): string
|
||||
{
|
||||
$old = @ini_set('serialize_precision', '-1'); // @ may be disabled
|
||||
try {
|
||||
return json_encode($snapshot, JSON_HEX_APOS | JSON_HEX_AMP | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
} finally {
|
||||
if ($old !== false) {
|
||||
ini_set('serialize_precision', $old);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static function htmlToAnsi(string $s, array $colors): string
|
||||
{
|
||||
$stack = ['0'];
|
||||
$s = preg_replace_callback(
|
||||
'#<\w+(?: class="tracy-dump-(\w+)")?[^>]*>|</\w+>#',
|
||||
function ($m) use ($colors, &$stack): string {
|
||||
if ($m[0][1] === '/') {
|
||||
array_pop($stack);
|
||||
} else {
|
||||
$stack[] = isset($m[1], $colors[$m[1]]) ? $colors[$m[1]] : '0';
|
||||
}
|
||||
|
||||
return "\033[" . end($stack) . 'm';
|
||||
},
|
||||
$s
|
||||
);
|
||||
$s = preg_replace('/\e\[0m(\n*)(?=\e)/', '$1', $s);
|
||||
return $s;
|
||||
}
|
||||
}
|
82
libs/Nette/Tracy/Dumper/Value.php
Normal file
82
libs/Nette/Tracy/Dumper/Value.php
Normal file
@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tracy\Dumper;
|
||||
|
||||
|
||||
/**
|
||||
* @internal
|
||||
*/
|
||||
final class Value implements \JsonSerializable
|
||||
{
|
||||
public const
|
||||
TypeArray = 'array',
|
||||
TypeBinaryHtml = 'bin',
|
||||
TypeNumber = 'number',
|
||||
TypeObject = 'object',
|
||||
TypeRef = 'ref',
|
||||
TypeResource = 'resource',
|
||||
TypeStringHtml = 'string',
|
||||
TypeText = 'text';
|
||||
|
||||
public const
|
||||
PropertyPublic = 0,
|
||||
PropertyProtected = 1,
|
||||
PropertyPrivate = 2,
|
||||
PropertyDynamic = 3,
|
||||
PropertyVirtual = 4;
|
||||
|
||||
/** @var string */
|
||||
public $type;
|
||||
|
||||
/** @var string|int */
|
||||
public $value;
|
||||
|
||||
/** @var ?int */
|
||||
public $length;
|
||||
|
||||
/** @var ?int */
|
||||
public $depth;
|
||||
|
||||
/** @var int|string */
|
||||
public $id;
|
||||
|
||||
/** @var object */
|
||||
public $holder;
|
||||
|
||||
/** @var ?array */
|
||||
public $items;
|
||||
|
||||
/** @var ?\stdClass */
|
||||
public $editor;
|
||||
|
||||
/** @var ?bool */
|
||||
public $collapsed;
|
||||
|
||||
|
||||
public function __construct(string $type, $value = null, ?int $length = null)
|
||||
{
|
||||
$this->type = $type;
|
||||
$this->value = $value;
|
||||
$this->length = $length;
|
||||
}
|
||||
|
||||
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
$res = [$this->type => $this->value];
|
||||
foreach (['length', 'editor', 'items', 'collapsed'] as $k) {
|
||||
if ($this->$k !== null) {
|
||||
$res[$k] = $this->$k;
|
||||
}
|
||||
}
|
||||
|
||||
return $res;
|
||||
}
|
||||
}
|
145
libs/Nette/Tracy/Dumper/assets/dumper-dark.css
Normal file
145
libs/Nette/Tracy/Dumper/assets/dumper-dark.css
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
.tracy-dump.tracy-dark {
|
||||
text-align: left;
|
||||
color: #f8f8f2;
|
||||
background: #29292e;
|
||||
border-radius: 4px;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-dark div {
|
||||
padding-left: 2.5ex;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-dark div div {
|
||||
border-left: 1px solid rgba(255, 255, 255, .1);
|
||||
margin-left: .5ex;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-dark div div:hover {
|
||||
border-left-color: rgba(255, 255, 255, .25);
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-location {
|
||||
color: silver;
|
||||
font-size: 80%;
|
||||
text-decoration: none;
|
||||
background: none;
|
||||
opacity: .5;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-location:hover,
|
||||
.tracy-dark .tracy-dump-location:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-array,
|
||||
.tracy-dark .tracy-dump-object {
|
||||
color: #f69c2e;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-string {
|
||||
color: #3cdfef;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.tracy-dark div.tracy-dump-string {
|
||||
position: relative;
|
||||
padding-left: 3.5ex;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-lq {
|
||||
margin-left: calc(-1ex - 1px);
|
||||
}
|
||||
|
||||
.tracy-dark div.tracy-dump-string:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(3ex - 1px);
|
||||
top: 1.5em;
|
||||
bottom: 0;
|
||||
border-left: 1px solid rgba(255, 255, 255, .1);
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-virtual span,
|
||||
.tracy-dark .tracy-dump-dynamic span,
|
||||
.tracy-dark .tracy-dump-string span {
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-virtual i,
|
||||
.tracy-dark .tracy-dump-dynamic i,
|
||||
.tracy-dark .tracy-dump-string i {
|
||||
font-size: 80%;
|
||||
font-style: normal;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-number {
|
||||
color: #77d285;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-null,
|
||||
.tracy-dark .tracy-dump-bool {
|
||||
color: #f3cb44;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-virtual {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-public::after {
|
||||
content: ' pub';
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-protected::after {
|
||||
content: ' pro';
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-private::after {
|
||||
content: ' pri';
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-public::after,
|
||||
.tracy-dark .tracy-dump-protected::after,
|
||||
.tracy-dark .tracy-dump-private::after,
|
||||
.tracy-dark .tracy-dump-hash {
|
||||
font-size: 85%;
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-indent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-highlight {
|
||||
background: #C22;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
margin: 0 -2px;
|
||||
}
|
||||
|
||||
span[data-tracy-href] {
|
||||
border-bottom: 1px dotted rgba(255, 255, 255, .2);
|
||||
}
|
||||
|
||||
.tracy-dark .tracy-dump-flash {
|
||||
animation: tracy-dump-flash .2s ease;
|
||||
}
|
||||
|
||||
@keyframes tracy-dump-flash {
|
||||
0% {
|
||||
background: #c0c0c033;
|
||||
}
|
||||
}
|
145
libs/Nette/Tracy/Dumper/assets/dumper-light.css
Normal file
145
libs/Nette/Tracy/Dumper/assets/dumper-light.css
Normal file
@ -0,0 +1,145 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
.tracy-dump.tracy-light {
|
||||
text-align: left;
|
||||
color: #444;
|
||||
background: #fdf9e2;
|
||||
border-radius: 4px;
|
||||
padding: 1em;
|
||||
margin: 1em 0;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-light div {
|
||||
padding-left: 2.5ex;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-light div div {
|
||||
border-left: 1px solid rgba(0, 0, 0, .1);
|
||||
margin-left: .5ex;
|
||||
}
|
||||
|
||||
.tracy-dump.tracy-light div div:hover {
|
||||
border-left-color: rgba(0, 0, 0, .25);
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-location {
|
||||
color: gray;
|
||||
font-size: 80%;
|
||||
text-decoration: none;
|
||||
background: none;
|
||||
opacity: .5;
|
||||
float: right;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-location:hover,
|
||||
.tracy-light .tracy-dump-location:focus {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-array,
|
||||
.tracy-light .tracy-dump-object {
|
||||
color: #C22;
|
||||
user-select: text;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-string {
|
||||
color: #35D;
|
||||
white-space: break-spaces;
|
||||
}
|
||||
|
||||
.tracy-light div.tracy-dump-string {
|
||||
position: relative;
|
||||
padding-left: 3.5ex;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-lq {
|
||||
margin-left: calc(-1ex - 1px);
|
||||
}
|
||||
|
||||
.tracy-light div.tracy-dump-string:before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: calc(3ex - 1px);
|
||||
top: 1.5em;
|
||||
bottom: 0;
|
||||
border-left: 1px solid rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-virtual span,
|
||||
.tracy-light .tracy-dump-dynamic span,
|
||||
.tracy-light .tracy-dump-string span {
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-virtual i,
|
||||
.tracy-light .tracy-dump-dynamic i,
|
||||
.tracy-light .tracy-dump-string i {
|
||||
font-size: 80%;
|
||||
font-style: normal;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-number {
|
||||
color: #090;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-null,
|
||||
.tracy-light .tracy-dump-bool {
|
||||
color: #850;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-virtual {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-public::after {
|
||||
content: ' pub';
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-protected::after {
|
||||
content: ' pro';
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-private::after {
|
||||
content: ' pri';
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-public::after,
|
||||
.tracy-light .tracy-dump-protected::after,
|
||||
.tracy-light .tracy-dump-private::after,
|
||||
.tracy-light .tracy-dump-hash {
|
||||
font-size: 85%;
|
||||
color: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-indent {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-highlight {
|
||||
background: #C22;
|
||||
color: white;
|
||||
border-radius: 2px;
|
||||
padding: 0 2px;
|
||||
margin: 0 -2px;
|
||||
}
|
||||
|
||||
span[data-tracy-href] {
|
||||
border-bottom: 1px dotted rgba(0, 0, 0, .2);
|
||||
}
|
||||
|
||||
.tracy-light .tracy-dump-flash {
|
||||
animation: tracy-dump-flash .2s ease;
|
||||
}
|
||||
|
||||
@keyframes tracy-dump-flash {
|
||||
0% {
|
||||
background: #c0c0c033;
|
||||
}
|
||||
}
|
393
libs/Nette/Tracy/Dumper/assets/dumper.js
Normal file
393
libs/Nette/Tracy/Dumper/assets/dumper.js
Normal file
@ -0,0 +1,393 @@
|
||||
/**
|
||||
* This file is part of the Tracy (https://tracy.nette.org)
|
||||
*/
|
||||
|
||||
const
|
||||
COLLAPSE_COUNT = 7,
|
||||
COLLAPSE_COUNT_TOP = 14,
|
||||
TYPE_ARRAY = 'a',
|
||||
TYPE_OBJECT = 'o',
|
||||
TYPE_RESOURCE = 'r',
|
||||
PROP_VIRTUAL = 4,
|
||||
PROP_PRIVATE = 2;
|
||||
|
||||
const
|
||||
HINT_CTRL = 'Ctrl-Click to open in editor',
|
||||
HINT_ALT = 'Alt-Click to expand/collapse all child nodes';
|
||||
|
||||
class Dumper
|
||||
{
|
||||
static init(context) {
|
||||
// full lazy
|
||||
(context || document).querySelectorAll('[data-tracy-snapshot][data-tracy-dump]').forEach((pre) => { // <pre>
|
||||
let snapshot = JSON.parse(pre.getAttribute('data-tracy-snapshot'));
|
||||
pre.removeAttribute('data-tracy-snapshot');
|
||||
pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
|
||||
pre.removeAttribute('data-tracy-dump');
|
||||
pre.classList.remove('tracy-collapsed');
|
||||
});
|
||||
|
||||
// snapshots
|
||||
(context || document).querySelectorAll('meta[itemprop=tracy-snapshot]').forEach((meta) => {
|
||||
let snapshot = JSON.parse(meta.getAttribute('content'));
|
||||
meta.parentElement.querySelectorAll('[data-tracy-dump]').forEach((pre) => { // <pre>
|
||||
if (pre.closest('[data-tracy-snapshot]')) { // ignore unrelated <span data-tracy-dump>
|
||||
return;
|
||||
}
|
||||
pre.appendChild(build(JSON.parse(pre.getAttribute('data-tracy-dump')), snapshot, pre.classList.contains('tracy-collapsed')));
|
||||
pre.removeAttribute('data-tracy-dump');
|
||||
pre.classList.remove('tracy-collapsed');
|
||||
});
|
||||
// <meta> must be left for debug bar panel content
|
||||
});
|
||||
|
||||
if (Dumper.inited) {
|
||||
return;
|
||||
}
|
||||
Dumper.inited = true;
|
||||
|
||||
document.documentElement.addEventListener('click', (e) => {
|
||||
let el;
|
||||
// enables <span data-tracy-href=""> & ctrl key
|
||||
if (e.ctrlKey && (el = e.target.closest('[data-tracy-href]'))) {
|
||||
location.href = el.getAttribute('data-tracy-href');
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
document.documentElement.addEventListener('tracy-beforetoggle', (e) => {
|
||||
let el;
|
||||
// initializes lazy <span data-tracy-dump> inside <pre data-tracy-snapshot>
|
||||
if ((el = e.target.closest('[data-tracy-snapshot]'))) {
|
||||
let snapshot = JSON.parse(el.getAttribute('data-tracy-snapshot'));
|
||||
el.removeAttribute('data-tracy-snapshot');
|
||||
el.querySelectorAll('[data-tracy-dump]').forEach((toggler) => {
|
||||
if (!toggler.nextSibling) {
|
||||
toggler.after(document.createTextNode('\n')); // enforce \n after toggler
|
||||
}
|
||||
toggler.nextSibling.after(buildStruct(JSON.parse(toggler.getAttribute('data-tracy-dump')), snapshot, toggler, true, []));
|
||||
toggler.removeAttribute('data-tracy-dump');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.documentElement.addEventListener('tracy-toggle', (e) => {
|
||||
if (!e.target.matches('.tracy-dump *')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let cont = e.detail.relatedTarget;
|
||||
let origE = e.detail.originalEvent;
|
||||
|
||||
if (origE && origE.usedIds) { // triggered by expandChild()
|
||||
toggleChildren(cont, origE.usedIds);
|
||||
return;
|
||||
|
||||
} else if (origE && origE.altKey && cont.querySelector('.tracy-toggle')) { // triggered by alt key
|
||||
if (e.detail.collapsed) { // reopen
|
||||
e.target.classList.toggle('tracy-collapsed', false);
|
||||
cont.classList.toggle('tracy-collapsed', false);
|
||||
e.detail.collapsed = false;
|
||||
}
|
||||
|
||||
let expand = e.target.tracyAltExpand = !e.target.tracyAltExpand;
|
||||
toggleChildren(cont, expand ? {} : false);
|
||||
}
|
||||
|
||||
cont.classList.toggle('tracy-dump-flash', !e.detail.collapsed);
|
||||
});
|
||||
|
||||
document.documentElement.addEventListener('animationend', (e) => {
|
||||
if (e.animationName === 'tracy-dump-flash') {
|
||||
e.target.classList.toggle('tracy-dump-flash', false);
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseover', (e) => {
|
||||
if (!e.target.matches('.tracy-dump *')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let el;
|
||||
|
||||
if (e.target.matches('.tracy-dump-hash') && (el = e.target.closest('tracy-div'))) {
|
||||
el.querySelectorAll('.tracy-dump-hash').forEach((el) => {
|
||||
if (el.textContent === e.target.textContent) {
|
||||
el.classList.add('tracy-dump-highlight');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if ((el = e.target.closest('.tracy-toggle')) && !el.title) {
|
||||
el.title = HINT_ALT;
|
||||
}
|
||||
});
|
||||
|
||||
document.addEventListener('mouseout', (e) => {
|
||||
if (e.target.matches('.tracy-dump-hash')) {
|
||||
document.querySelectorAll('.tracy-dump-hash.tracy-dump-highlight').forEach((el) => {
|
||||
el.classList.remove('tracy-dump-highlight');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
Tracy.Toggle.init();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function build(data, repository, collapsed, parentIds, keyType) {
|
||||
let id, type = data === null ? 'null' : typeof data,
|
||||
collapseCount = collapsed === null ? COLLAPSE_COUNT : COLLAPSE_COUNT_TOP;
|
||||
|
||||
if (type === 'null' || type === 'number' || type === 'boolean') {
|
||||
return createEl(null, null, [
|
||||
createEl(
|
||||
'span',
|
||||
{'class': 'tracy-dump-' + type.replace('ean', '')},
|
||||
[data + '']
|
||||
)
|
||||
]);
|
||||
|
||||
} else if (type === 'string') {
|
||||
data = {
|
||||
string: data.replace(/&/g, '&').replace(/</g, '<'),
|
||||
length: [...data].length
|
||||
};
|
||||
|
||||
} else if (Array.isArray(data)) {
|
||||
data = {array: null, items: data};
|
||||
|
||||
} else if (data.ref) {
|
||||
id = data.ref;
|
||||
data = repository[id];
|
||||
if (!data) {
|
||||
throw new UnknownEntityException;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (data.string !== undefined || data.bin !== undefined) {
|
||||
let s = data.string === undefined ? data.bin : data.string;
|
||||
if (keyType === TYPE_ARRAY) {
|
||||
return createEl(null, null, [
|
||||
createEl(
|
||||
'span',
|
||||
{'class': 'tracy-dump-string'},
|
||||
{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
|
||||
),
|
||||
]);
|
||||
|
||||
} else if (keyType !== undefined) {
|
||||
if (type !== 'string') {
|
||||
s = '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>';
|
||||
}
|
||||
|
||||
const classes = [
|
||||
'tracy-dump-public',
|
||||
'tracy-dump-protected',
|
||||
'tracy-dump-private',
|
||||
'tracy-dump-dynamic',
|
||||
'tracy-dump-virtual',
|
||||
];
|
||||
return createEl(null, null, [
|
||||
createEl(
|
||||
'span',
|
||||
{
|
||||
'class': classes[typeof keyType === 'string' ? PROP_PRIVATE : keyType],
|
||||
'title': typeof keyType === 'string' ? 'declared in ' + keyType : null,
|
||||
},
|
||||
{html: s}
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
let count = (s.match(/\n/g) || []).length;
|
||||
if (count) {
|
||||
let collapsed = count >= COLLAPSE_COUNT;
|
||||
return createEl(null, null, [
|
||||
createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, ['string']),
|
||||
'\n',
|
||||
createEl(
|
||||
'div',
|
||||
{
|
||||
'class': 'tracy-dump-string' + (collapsed ? ' tracy-collapsed' : ''),
|
||||
'title': data.length + (data.bin ? ' bytes' : ' characters'),
|
||||
},
|
||||
{html: '<span class="tracy-dump-lq">\'</span>' + s + '<span>\'</span>'}
|
||||
),
|
||||
]);
|
||||
}
|
||||
|
||||
return createEl(null, null, [
|
||||
createEl(
|
||||
'span',
|
||||
{
|
||||
'class': 'tracy-dump-string',
|
||||
'title': data.length + (data.bin ? ' bytes' : ' characters'),
|
||||
},
|
||||
{html: '<span>\'</span>' + s + '<span>\'</span>'}
|
||||
),
|
||||
]);
|
||||
|
||||
} else if (data.number) {
|
||||
return createEl(null, null, [
|
||||
createEl('span', {'class': 'tracy-dump-number'}, [data.number])
|
||||
]);
|
||||
|
||||
} else if (data.text !== undefined) {
|
||||
return createEl(null, null, [
|
||||
createEl('span', {class: 'tracy-dump-virtual'}, [data.text])
|
||||
]);
|
||||
|
||||
} else { // object || resource || array
|
||||
let pos, nameEl;
|
||||
nameEl = data.object && (pos = data.object.lastIndexOf('\\')) > 0
|
||||
? [data.object.substr(0, pos + 1), createEl('b', null, [data.object.substr(pos + 1)])]
|
||||
: [data.object || data.resource];
|
||||
|
||||
let span = data.array !== undefined
|
||||
? [
|
||||
createEl('span', {'class': 'tracy-dump-array'}, ['array']),
|
||||
' (' + (data.length || data.items.length) + ')'
|
||||
]
|
||||
: [
|
||||
createEl('span', {
|
||||
'class': data.object ? 'tracy-dump-object' : 'tracy-dump-resource',
|
||||
title: data.editor ? 'Declared in file ' + data.editor.file + ' on line ' + data.editor.line + (data.editor.url ? '\n' + HINT_CTRL : '') + '\n' + HINT_ALT : null,
|
||||
'data-tracy-href': data.editor ? data.editor.url : null
|
||||
}, nameEl),
|
||||
...(id ? [' ', createEl('span', {'class': 'tracy-dump-hash'}, [data.resource ? '@' + id.substr(1) : '#' + id])] : [])
|
||||
];
|
||||
|
||||
parentIds = parentIds ? parentIds.slice() : [];
|
||||
let recursive = id && parentIds.indexOf(id) > -1;
|
||||
parentIds.push(id);
|
||||
|
||||
if (recursive || !data.items || !data.items.length) {
|
||||
span.push(recursive ? ' RECURSION' : (!data.items || data.items.length ? ' …' : ''));
|
||||
return createEl(null, null, span);
|
||||
}
|
||||
|
||||
collapsed = collapsed === true || data.collapsed || (data.items && data.items.length >= collapseCount);
|
||||
let toggle = createEl('span', {'class': collapsed ? 'tracy-toggle tracy-collapsed' : 'tracy-toggle'}, span);
|
||||
|
||||
return createEl(null, null, [
|
||||
toggle,
|
||||
'\n',
|
||||
buildStruct(data, repository, toggle, collapsed, parentIds),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function buildStruct(data, repository, toggle, collapsed, parentIds) {
|
||||
if (Array.isArray(data)) {
|
||||
data = {items: data};
|
||||
|
||||
} else if (data.ref) {
|
||||
parentIds = parentIds.slice();
|
||||
parentIds.push(data.ref);
|
||||
data = repository[data.ref];
|
||||
}
|
||||
|
||||
let cut = data.items && data.length > data.items.length;
|
||||
let type = data.object ? TYPE_OBJECT : data.resource ? TYPE_RESOURCE : TYPE_ARRAY;
|
||||
let div = createEl('div', {'class': collapsed ? 'tracy-collapsed' : null});
|
||||
|
||||
if (collapsed) {
|
||||
let handler;
|
||||
toggle.addEventListener('tracy-toggle', handler = function() {
|
||||
toggle.removeEventListener('tracy-toggle', handler);
|
||||
createItems(div, data.items, type, repository, parentIds, null);
|
||||
if (cut) {
|
||||
createEl(div, null, ['…\n']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
createItems(div, data.items, type, repository, parentIds, true);
|
||||
if (cut) {
|
||||
createEl(div, null, ['…\n']);
|
||||
}
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
function createEl(el, attrs, content) {
|
||||
if (!(el instanceof Node)) {
|
||||
el = el ? document.createElement(el) : document.createDocumentFragment();
|
||||
}
|
||||
for (let id in attrs || {}) {
|
||||
if (attrs[id] !== null) {
|
||||
el.setAttribute(id, attrs[id]);
|
||||
}
|
||||
}
|
||||
|
||||
if (content && content.html !== undefined) {
|
||||
el.innerHTML = content.html;
|
||||
return el;
|
||||
}
|
||||
|
||||
content = content || [];
|
||||
el.append(...content.filter((child) => (child !== null)));
|
||||
return el;
|
||||
}
|
||||
|
||||
|
||||
function createItems(el, items, type, repository, parentIds, collapsed) {
|
||||
let key, val, vis, ref, i, tmp;
|
||||
|
||||
for (i = 0; i < items.length; i++) {
|
||||
if (type === TYPE_ARRAY) {
|
||||
[key, val, ref] = items[i];
|
||||
} else {
|
||||
[key, val, vis = PROP_VIRTUAL, ref] = items[i];
|
||||
}
|
||||
|
||||
createEl(el, null, [
|
||||
build(key, null, null, null, type === TYPE_ARRAY ? TYPE_ARRAY : vis),
|
||||
type === TYPE_ARRAY ? ' => ' : ': ',
|
||||
...(ref ? [createEl('span', {'class': 'tracy-dump-hash'}, ['&' + ref]), ' '] : []),
|
||||
tmp = build(val, repository, collapsed, parentIds),
|
||||
tmp.lastElementChild.tagName === 'DIV' ? '' : '\n',
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function toggleChildren(cont, usedIds) {
|
||||
let hashEl, id;
|
||||
|
||||
cont.querySelectorAll(':scope > .tracy-toggle').forEach((el) => {
|
||||
hashEl = (el.querySelector('.tracy-dump-hash') || el.previousElementSibling);
|
||||
id = hashEl && hashEl.matches('.tracy-dump-hash') ? hashEl.textContent : null;
|
||||
|
||||
if (!usedIds || (id && usedIds[id])) {
|
||||
Tracy.Toggle.toggle(el, false);
|
||||
} else {
|
||||
usedIds[id] = true;
|
||||
Tracy.Toggle.toggle(el, true, {usedIds: usedIds});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function UnknownEntityException() {}
|
||||
|
||||
|
||||
let Tracy = window.Tracy = window.Tracy || {};
|
||||
Tracy.Dumper = Tracy.Dumper || Dumper;
|
||||
|
||||
function init() {
|
||||
Tracy.Dumper.init();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
Reference in New Issue
Block a user