Commit version 24.12.13800

This commit is contained in:
2025-01-06 17:35:06 -05:00
parent b7f6a79c2c
commit 55d9218816
6133 changed files with 4239740 additions and 1374287 deletions

View 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;
}
}

View 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;
}
}

View 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);
}
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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, '&amp;').replace(/</g, '&lt;'),
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();
}