Skip to content

Instantly share code, notes, and snippets.

@JanTvrdik
Last active August 29, 2015 13:58
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JanTvrdik/10277952 to your computer and use it in GitHub Desktop.
Save JanTvrdik/10277952 to your computer and use it in GitHub Desktop.
JSOND - JSON compatibility test
<?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) . "&amp;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)));
}
}
<?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