JSOND - JSON compatibility test
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* This file is part of the Tracy (http://tracy.nette.org) | |
* Copyright (c) 2004 David Grudl (http://davidgrudl.com) | |
*/ | |
namespace Tracy; | |
use Tracy; | |
/** | |
* Dumps a variable. | |
* | |
* @author David Grudl | |
*/ | |
class Dumper | |
{ | |
const DEPTH = 'depth', // how many nested levels of array/object properties display (defaults to 4) | |
TRUNCATE = 'truncate', // how truncate long strings? (defaults to 150) | |
COLLAPSE = 'collapse', // always collapse? (defaults to false) | |
COLLAPSE_COUNT = 'collapsecount', // how big array/object are collapsed? (defaults to 7) | |
LOCATION = 'location'; // show location string? (defaults to false) | |
/** @var array */ | |
public static $terminalColors = array( | |
'bool' => '1;33', | |
'null' => '1;33', | |
'number' => '1;32', | |
'string' => '1;36', | |
'array' => '1;31', | |
'key' => '1;37', | |
'object' => '1;31', | |
'visibility' => '1;30', | |
'resource' => '1;37', | |
'indent' => '1;30', | |
); | |
/** @var array */ | |
public static $resources = array( | |
'stream' => 'stream_get_meta_data', | |
'stream-context' => 'stream_context_get_options', | |
'curl' => 'curl_getinfo', | |
); | |
/** | |
* Dumps variable to the output. | |
* @return mixed variable | |
*/ | |
public static function dump($var, array $options = NULL) | |
{ | |
if (PHP_SAPI !== 'cli' && !preg_match('#^Content-Type: (?!text/html)#im', implode("\n", headers_list()))) { | |
echo self::toHtml($var, $options); | |
} elseif (self::detectColors()) { | |
echo self::toTerminal($var, $options); | |
} else { | |
echo self::toText($var, $options); | |
} | |
return $var; | |
} | |
/** | |
* Dumps variable to HTML. | |
* @return string | |
*/ | |
public static function toHtml($var, array $options = NULL) | |
{ | |
$options = (array) $options + array( | |
self::DEPTH => 4, | |
self::TRUNCATE => 150, | |
self::COLLAPSE => FALSE, | |
self::COLLAPSE_COUNT => 7, | |
self::LOCATION => FALSE, | |
); | |
list($file, $line, $code) = $options[self::LOCATION] ? self::findLocation() : NULL; | |
return '<pre class="tracy-dump"' | |
. ($file ? ' title="' . htmlspecialchars("$code\nin file $file on line $line", ENT_IGNORE | ENT_QUOTES) . '">' : '>') | |
. self::dumpVar($var, $options) | |
. ($file ? '<small>in <a href="editor://open/?file=' . rawurlencode($file) . "&line=$line\">" . htmlspecialchars($file, ENT_IGNORE) . ":$line</a></small>" : '') | |
. "</pre>\n"; | |
} | |
/** | |
* Dumps variable to plain text. | |
* @return string | |
*/ | |
public static function toText($var, array $options = NULL) | |
{ | |
return htmlspecialchars_decode(strip_tags(self::toHtml($var, $options)), ENT_QUOTES); | |
} | |
/** | |
* Dumps variable to x-terminal. | |
* @return string | |
*/ | |
public static function toTerminal($var, array $options = NULL) | |
{ | |
return htmlspecialchars_decode(strip_tags(preg_replace_callback('#<span class="tracy-dump-(\w+)">|</span>#', function($m) { | |
return "\033[" . (isset($m[1], Dumper::$terminalColors[$m[1]]) ? Dumper::$terminalColors[$m[1]] : '0') . 'm'; | |
}, self::toHtml($var, $options))), ENT_QUOTES); | |
} | |
/** | |
* Internal toHtml() dump implementation. | |
* @param mixed variable to dump | |
* @param array options | |
* @param int current recursion level | |
* @return string | |
*/ | |
private static function dumpVar(& $var, array $options, $level = 0) | |
{ | |
if (method_exists(__CLASS__, $m = 'dump' . gettype($var))) { | |
return self::$m($var, $options, $level); | |
} else { | |
return "<span>unknown type</span>\n"; | |
} | |
} | |
private static function dumpNull() | |
{ | |
return "<span class=\"tracy-dump-null\">NULL</span>\n"; | |
} | |
private static function dumpBoolean(& $var) | |
{ | |
return '<span class="tracy-dump-bool">' . ($var ? 'TRUE' : 'FALSE') . "</span>\n"; | |
} | |
private static function dumpInteger(& $var) | |
{ | |
return "<span class=\"tracy-dump-number\">$var</span>\n"; | |
} | |
private static function dumpDouble(& $var) | |
{ | |
$var = is_finite($var) | |
? ($tmp = json_encode($var)) . (strpos($tmp, '.') === FALSE ? '.0' : '') | |
: var_export($var, TRUE); | |
return "<span class=\"tracy-dump-number\">$var</span>\n"; | |
} | |
private static function dumpString(& $var, $options) | |
{ | |
return '<span class="tracy-dump-string">' | |
. self::encodeString($options[self::TRUNCATE] && strlen($var) > $options[self::TRUNCATE] ? substr($var, 0, $options[self::TRUNCATE]) . ' ... ' : $var) | |
. '</span>' . (strlen($var) > 1 ? ' (' . strlen($var) . ')' : '') . "\n"; | |
} | |
private static function dumpArray(& $var, $options, $level) | |
{ | |
static $marker; | |
if ($marker === NULL) { | |
$marker = uniqid("\x00", TRUE); | |
} | |
$out = '<span class="tracy-dump-array">array</span> ('; | |
if (empty($var)) { | |
return $out . ")\n"; | |
} elseif (isset($var[$marker])) { | |
return $out . (count($var) - 1) . ") [ <i>RECURSION</i> ]\n"; | |
} elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH]) { | |
$collapsed = $level ? count($var) >= $options[self::COLLAPSE_COUNT] : $options[self::COLLAPSE]; | |
$out = '<span class="tracy-toggle' . ($collapsed ? '-collapsed' : '') . '">' . $out . count($var) . ")</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; | |
$var[$marker] = TRUE; | |
foreach ($var as $k => & $v) { | |
if ($k !== $marker) { | |
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' | |
. '<span class="tracy-dump-key">' . (preg_match('#^\w+\z#', $k) ? $k : self::encodeString($k)) . '</span> => ' | |
. self::dumpVar($v, $options, $level + 1); | |
} | |
} | |
unset($var[$marker]); | |
return $out . '</div>'; | |
} else { | |
return $out . count($var) . ") [ ... ]\n"; | |
} | |
} | |
private static function dumpObject(& $var, $options, $level) | |
{ | |
if ($var instanceof \Closure) { | |
$rc = new \ReflectionFunction($var); | |
$fields = array(); | |
foreach ($rc->getParameters() as $param) { | |
$fields[] = '$' . $param->getName(); | |
} | |
$fields = array( | |
'file' => $rc->getFileName(), 'line' => $rc->getStartLine(), | |
'variables' => $rc->getStaticVariables(), 'parameters' => implode(', ', $fields) | |
); | |
} elseif ($var instanceof \SplFileInfo) { | |
$fields = array('path' => $var->getPathname()); | |
} elseif ($var instanceof \SplObjectStorage) { | |
$fields = array(); | |
foreach (clone $var as $obj) { | |
$fields[] = array('object' => $obj, 'data' => $var[$obj]); | |
} | |
} else { | |
$fields = (array) $var; | |
} | |
static $list = array(); | |
$rc = $var instanceof \Closure ? new \ReflectionFunction($var) : new \ReflectionClass($var); | |
$out = '<span class="tracy-dump-object"' | |
. ($rc->getFileName() ? ' data-tracy-href="' . htmlspecialchars(strtr(Debugger::$editor, array('%file' => rawurlencode($rc->getFileName()), '%line' => $rc->getStartLine()))) . '"' : '') | |
. '>' . get_class($var) . '</span> <span class="tracy-dump-hash">#' . substr(md5(spl_object_hash($var)), 0, 4) . '</span>'; | |
if (empty($fields)) { | |
return $out . "\n"; | |
} elseif (in_array($var, $list, TRUE)) { | |
return $out . " { <i>RECURSION</i> }\n"; | |
} elseif (!$options[self::DEPTH] || $level < $options[self::DEPTH] || $var instanceof \Closure) { | |
$collapsed = $level ? count($fields) >= $options[self::COLLAPSE_COUNT] : $options[self::COLLAPSE]; | |
$out = '<span class="tracy-toggle' . ($collapsed ? '-collapsed' : '') . '">' . $out . "</span>\n<div" . ($collapsed ? ' class="tracy-collapsed"' : '') . '>'; | |
$list[] = $var; | |
foreach ($fields as $k => & $v) { | |
$vis = ''; | |
if ($k[0] === "\x00") { | |
$vis = ' <span class="tracy-dump-visibility">' . ($k[1] === '*' ? 'protected' : 'private') . '</span>'; | |
$k = substr($k, strrpos($k, "\x00") + 1); | |
} | |
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' | |
. '<span class="tracy-dump-key">' . (preg_match('#^\w+\z#', $k) ? $k : self::encodeString($k)) . "</span>$vis => " | |
. self::dumpVar($v, $options, $level + 1); | |
} | |
array_pop($list); | |
return $out . '</div>'; | |
} else { | |
return $out . " { ... }\n"; | |
} | |
} | |
private static function dumpResource(& $var, $options, $level) | |
{ | |
$type = get_resource_type($var); | |
$out = '<span class="tracy-dump-resource">' . htmlSpecialChars($type) . ' resource</span>'; | |
if (isset(self::$resources[$type])) { | |
$out = "<span class=\"tracy-toggle-collapsed\">$out</span>\n<div class=\"tracy-collapsed\">"; | |
foreach (call_user_func(self::$resources[$type], $var) as $k => $v) { | |
$out .= '<span class="tracy-dump-indent"> ' . str_repeat('| ', $level) . '</span>' | |
. '<span class="tracy-dump-key">' . htmlSpecialChars($k) . "</span> => " . self::dumpVar($v, $options, $level + 1); | |
} | |
return $out . '</div>'; | |
} | |
return "$out\n"; | |
} | |
private static function encodeString($s) | |
{ | |
static $table; | |
if ($table === NULL) { | |
foreach (array_merge(range("\x00", "\x1F"), range("\x7F", "\xFF")) as $ch) { | |
$table[$ch] = '\x' . str_pad(dechex(ord($ch)), 2, '0', STR_PAD_LEFT); | |
} | |
$table["\\"] = '\\\\'; | |
$table["\r"] = '\r'; | |
$table["\n"] = '\n'; | |
$table["\t"] = '\t'; | |
} | |
if (preg_match('#[^\x09\x0A\x0D\x20-\x7E\xA0-\x{10FFFF}]#u', $s) || preg_last_error()) { | |
$s = strtr($s, $table); | |
} | |
return '"' . htmlSpecialChars($s, ENT_NOQUOTES) . '"'; | |
} | |
/** | |
* Finds the location where dump was called. | |
* @return array [file, line, code] | |
*/ | |
private static function findLocation() | |
{ | |
foreach (debug_backtrace(PHP_VERSION_ID >= 50306 ? DEBUG_BACKTRACE_IGNORE_ARGS : FALSE) as $item) { | |
if (isset($item['class']) && $item['class'] === __CLASS__) { | |
$location = $item; | |
continue; | |
} elseif (isset($item['function'])) { | |
try { | |
$reflection = isset($item['class']) | |
? new \ReflectionMethod($item['class'], $item['function']) | |
: new \ReflectionFunction($item['function']); | |
if (preg_match('#\s@tracySkipLocation\s#', $reflection->getDocComment())) { | |
$location = $item; | |
continue; | |
} | |
} catch (\ReflectionException $e) {} | |
} | |
break; | |
} | |
if (isset($location['file'], $location['line']) && is_file($location['file'])) { | |
$lines = file($location['file']); | |
$line = $lines[$location['line'] - 1]; | |
return array( | |
$location['file'], | |
$location['line'], | |
trim(preg_match('#\w*dump(er::\w+)?\(.*\)#i', $line, $m) ? $m[0] : $line) | |
); | |
} | |
} | |
/** | |
* @return bool | |
*/ | |
private static function detectColors() | |
{ | |
return self::$terminalColors && | |
(getenv('ConEmuANSI') === 'ON' | |
|| getenv('ANSICON') !== FALSE | |
|| (defined('STDOUT') && function_exists('posix_isatty') && posix_isatty(STDOUT))); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
require __DIR__ . '/Dumper.php'; | |
$dataSet = array( | |
// https://tools.ietf.org/html/rfc7159 | |
'RFC JSON' => array( | |
// numbers | |
array("0", 0), | |
array("1", 1), | |
array("0.1", 0.1), | |
array("1.1", 1.1), | |
array("1.100000", 1.1), | |
array("1.111111", 1.111111), | |
array("-0", -0), | |
array("-1", -1), | |
array("-0.1", -0.1), | |
array("-1.1", -1.1), | |
array("-1.100000", -1.1), | |
array("-1.111111", -1.111111), | |
array("1.1e1", 11.0), | |
array("1.1e+1", 11.0), | |
array("1.1e-1", 0.11), | |
array("1.1E1", 11.0), | |
array("1.1E+1", 11.0), | |
array("1.1E-1", 0.11), | |
// literals | |
array("null", NULL), | |
array("true", TRUE), | |
array("false", FALSE), | |
// strings | |
array('""', ''), | |
array('"foo"', "foo"), | |
array('"f\\no"', "f\no"), | |
array('"\\b\\f\\n\\r\\t\\"\\/\\\\"', "\x08\f\n\r\t\"/\\"), | |
array('"\u0040"', "@"), | |
array('"\u011B"', "\xC4\x9B"), | |
array('"\uD801\uDC01"', "\xf0\x90\x90\x81"), // U+10401 encoded as surrogate pair | |
), | |
// JSON parser implementation in PHP (json_decode); extends RFC JSON | |
'PHP JSON' => array( | |
// extended numbers syntax (only on top level) | |
array('0777', 777), | |
array('00000777', 777), | |
array('0xff', 0xff), | |
array('.1', 0.1), | |
array('-.1', -0.1), | |
// extended numbers syntax (everywhere) | |
array('[1.]', array(1.0)), | |
array('[1.e1]', array(10.0)), | |
array('[-1.]', array(-1.0)), | |
array('[-1.e-1]', array(-0.1)), | |
// empty input | |
array('', NULL), | |
), | |
// inputs with invalid syntax, but still valid UTF-8 | |
'invalid syntax' => array( | |
array('"\\a invalid escape"'), | |
array('"\\v invalid escape"'), | |
array('"\\u202 invalid escape"'), | |
array('"\\012 invalid escape"'), | |
array('"\\\' invalid escape"'), | |
array('"Unterminated string'), | |
array('"Unterminated string\\"'), | |
array('"Unterminated string\\\\\\"'), | |
array('"42" ""'), | |
array('"" ""'), | |
array('[] ""'), | |
array('[true] ""'), | |
array('{} ""'), | |
array('{"x":true} ""'), | |
array('"Garbage""After string"'), | |
array('function () { return 0; }'), | |
array("[1, 2"), | |
array('{"x": 3'), | |
array('1e--1]'), | |
array(' '), | |
), | |
// inputs which are not valid UTF-8 | |
'invalid encoding' => array( | |
array("'\xc3\x28'"), // Invalid 2 Octet Sequence | |
array("'\xa0\xa1'"), // Invalid Sequence Identifier | |
array("'\xe2\x28\xa1'"), // Invalid 3 Octet Sequence (in 2nd Octet) | |
array("'\xe2\x82\x28'"), // Invalid 3 Octet Sequence (in 3rd Octet) | |
array("'\xf0\x28\x8c\xbc'"), // Invalid 4 Octet Sequence (in 2nd Octet) | |
array("'\xf0\x90\x28\xbc'"), // Invalid 4 Octet Sequence (in 3rd Octet) | |
array("'\xf0\x28\x8c\x28'"), // Invalid 4 Octet Sequence (in 4th Octet) | |
array("'\xf8\xa1\xa1\xa1\xa1'"), // Valid 5 Octet Sequence (but not Unicode!) | |
array("'\xfc\xa1\xa1\xa1\xa1\xa1'"), // Valid 6 Octet Sequence (but not Unicode!) | |
array("'\xed\xa0\x80'"), // invalid code point (U+D800) | |
array("'\xf0\x82\x82\xac'"), // overlong encoding of U+20AC (euro sign) | |
), | |
); | |
foreach ($dataSet as $label => $set) { | |
echo "<h2>" . htmlspecialchars($label) . "</h2>\n"; | |
echo "<table style='font-family: monospace;'>\n"; | |
echo "<tr>\n"; | |
echo "<th width=200>Input</th>\n"; | |
echo "<th width=200>JSON</th>\n"; | |
echo "<th width=200>JSOND</th>\n"; | |
echo "<th width=200>Ref</th>\n"; | |
echo "</tr>\n"; | |
foreach ($set as $tmp) { | |
$in = $tmp[0]; | |
$ref = isset($tmp[1]) ? $tmp[1] : NULL; | |
$jsonResult = json_decode($in); | |
$jsonColor = ($jsonResult === $ref && json_last_error() === 0) ? 'green' : 'red'; | |
$jsondResult = jsond_decode($in); | |
$jsondColor = ($jsondResult === $ref && jsond_last_error() === 0) ? 'green' : 'red'; | |
echo "<tr>\n"; | |
echo "<td>" . Tracy\Dumper::toHtml($in) . "</td>\n"; | |
echo "<td style=\"color: $jsonColor\">" . Tracy\Dumper::toHtml($jsonResult) . "</td>\n"; | |
echo "<td style=\"color: $jsondColor\">" . Tracy\Dumper::toHtml($jsondResult) . "</td>\n"; | |
echo "<td>" . Tracy\Dumper::toHtml($ref) . "</td>\n"; | |
echo "</tr>\n"; | |
} | |
echo "</table>\n"; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment