pretty print json output

This commit is contained in:
Jeremy Penner 2011-04-01 10:28:59 -07:00
parent 975949f3ff
commit 7a684c14bc
9 changed files with 291 additions and 185 deletions

View file

@ -42,7 +42,7 @@ package
import net.flashpunk.utils.Key; import net.flashpunk.utils.Key;
import net.flashpunk.World; import net.flashpunk.World;
import net.flashpunk.utils.Input; import net.flashpunk.utils.Input;
import com.adobe.serialization.json.JSON; import com.maccherone.json.JSON;
/** /**
* ... * ...
* @author jjp * @author jjp
@ -361,7 +361,7 @@ package
{ {
var stream: FileStream = new FileStream(); var stream: FileStream = new FileStream();
stream.open(new File(UabFromUrf(urff)), FileMode.WRITE); stream.open(new File(UabFromUrf(urff)), FileMode.WRITE);
stream.writeUTFBytes(JSON.encode(GenJSON())); stream.writeUTFBytes(JSON.encode(GenJSON(), true, 80));
stream.close(); stream.close();
ShowMsg("Saved."); ShowMsg("Saved.");
} }

View file

@ -1,4 +1,6 @@
/* /*
Copyright (c) 2009, Lawrence S. Maccherone, Jr.
Copyright (c) 2008, Adobe Systems Incorporated Copyright (c) 2008, Adobe Systems Incorporated
All rights reserved. All rights reserved.
@ -30,7 +32,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json package com.maccherone.json
{ {
/** /**
@ -56,9 +58,9 @@ package com.adobe.serialization.json
* @playerversion Flash 9.0 * @playerversion Flash 9.0
* @tiptext * @tiptext
*/ */
public static function encode( o:Object ):String public static function encode( o:Object, pretty:Boolean=false, maxLength:int=60 ):String
{ {
return new JSONEncoder( o ).getString(); return new JSONEncoder( o, pretty, maxLength ).getString();
} }
/** /**

View file

@ -30,7 +30,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json package com.maccherone.json
{ {
public class JSONDecoder public class JSONDecoder

View file

@ -1,4 +1,6 @@
/* /*
Copyright (c) 2009, Lawrence S. Maccherone, Jr.
Copyright (c) 2008, Adobe Systems Incorporated Copyright (c) 2008, Adobe Systems Incorporated
All rights reserved. All rights reserved.
@ -30,7 +32,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json package com.maccherone.json
{ {
import flash.utils.describeType; import flash.utils.describeType;
@ -40,6 +42,17 @@ package com.adobe.serialization.json
/** The string that is going to represent the object we're encoding */ /** The string that is going to represent the object we're encoding */
private var jsonString:String; private var jsonString:String;
/** The current level */
private var level:int;
/** Above this length, an object or array will be split into multiple lines. Any value 2 or below will always split. */
private var maxLength:int;
/** When true, the encoder will add spaces and split Arrays and Objects over maxLength into multiple lines */
private var pretty:Boolean;
static private const tabWidth:int = 4;
/** /**
* Creates a new JSONEncoder. * Creates a new JSONEncoder.
* *
@ -48,9 +61,15 @@ package com.adobe.serialization.json
* @playerversion Flash 9.0 * @playerversion Flash 9.0
* @tiptext * @tiptext
*/ */
public function JSONEncoder( value:* ) { public function JSONEncoder( value:*, pretty:Boolean=false, maxLength:int=60 ) {
level = 0;
this.pretty = pretty;
if (pretty) {
this.maxLength = maxLength;
} else {
this.maxLength = int.MAX_VALUE;
}
jsonString = convertToString( value ); jsonString = convertToString( value );
} }
/** /**
@ -74,6 +93,8 @@ package com.adobe.serialization.json
*/ */
private function convertToString( value:* ):String { private function convertToString( value:* ):String {
var temp:String;
// determine what value is and convert it based on it's type // determine what value is and convert it based on it's type
if ( value is String ) { if ( value is String ) {
@ -82,7 +103,7 @@ package com.adobe.serialization.json
} else if ( value is Number ) { } else if ( value is Number ) {
// only encode numbers that finate // only encode numbers that are finite
return isFinite( value as Number) ? value.toString() : "null"; return isFinite( value as Number) ? value.toString() : "null";
} else if ( value is Boolean ) { } else if ( value is Boolean ) {
@ -92,13 +113,28 @@ package com.adobe.serialization.json
} else if ( value is Array ) { } else if ( value is Array ) {
// call the helper method to convert an array if (maxLength <= 2) {
return arrayToString( value as Array ); temp = arrayToStringPretty( value as Array );
} else {
// call the helper method to convert an array
temp = arrayToString( value as Array );
if (temp.length > maxLength) {
temp = arrayToStringPretty( value as Array );
}
}
return temp;
} else if ( value is Object && value != null ) { } else if ( value is Object && value != null ) {
if (maxLength <= 2) {
// call the helper method to convert an object temp = objectToStringPretty( value );
return objectToString( value ); } else {
// call the helper method to convert an object
temp = objectToString( value );
if (temp.length > maxLength) {
temp = objectToStringPretty( value );
}
}
return temp;
} }
return "null"; return "null";
} }
@ -201,6 +237,9 @@ package com.adobe.serialization.json
if ( s.length > 0 ) { if ( s.length > 0 ) {
// we've already added an element, so add the comma separator // we've already added an element, so add the comma separator
s += "," s += ","
if (pretty) {
s += " "
}
} }
// convert the value to a string // convert the value to a string
@ -227,6 +266,37 @@ package com.adobe.serialization.json
return "[" + s + "]"; return "[" + s + "]";
} }
/**
* Converts an array to it's JSON string equivalent using multiple lines
*
* @param a The array to convert
* @return The JSON string representation of <code>a</code>
*/
private function arrayToStringPretty( a:Array ):String {
level++;
// create a string to store the array's jsonstring value
var s:String = "";
// loop over the elements in the array and add their converted
// values to the string
for ( var i:int = 0; i < a.length; i++ ) {
// when the length is 0 we're adding the first element so
// no comma is necessary
if ( s.length > 0 ) {
// we've already added an element, so add the comma separator
s += ",\n"
}
// convert the value to a string
s += getPadding(level) + convertToString( a[i] );
}
// close the array and return it's string value
level--;
return "[" + "\n" + s + "\n" + getPadding(level) + "]";
}
/** /**
* Converts an object to it's JSON string equivalent * Converts an object to it's JSON string equivalent
* *
@ -266,39 +336,39 @@ package com.adobe.serialization.json
if ( s.length > 0 ) { if ( s.length > 0 ) {
// we've already added an item, so add the comma separator // we've already added an item, so add the comma separator
s += "," s += ","
if (pretty) {
s += " "
}
} }
s += escapeString( key ) + ":" + convertToString( value ); s += escapeString( key ) + ":"
if (pretty) {
s += " "
}
s += convertToString( value );
} }
} }
else // o is a class instance else // o is a class instance
{ {
// Loop over all of the variables and accessors in the class and // Loop over all of the variables and accessors in the class and
// serialize them along with their values. // serialize them along with their values.
for each ( var v:XML in classInfo..*.( for each ( var v:XML in classInfo..*.( name() == "variable" || name() == "accessor" ) )
name() == "variable"
||
(
name() == "accessor"
// Issue #116 - Make sure accessors are readable
&& attribute( "access" ).charAt( 0 ) == "r" )
) )
{ {
// Issue #110 - If [Transient] metadata exists, then we should skip
if ( v.metadata && v.metadata.( @name == "Transient" ).length() > 0 )
{
continue;
}
// When the length is 0 we're adding the first item so // When the length is 0 we're adding the first item so
// no comma is necessary // no comma is necessary
if ( s.length > 0 ) { if ( s.length > 0 ) {
// We've already added an item, so add the comma separator // We've already added an item, so add the comma separator
s += "," s += ","
if (pretty) {
s += " "
}
} }
s += escapeString( v.@name.toString() ) + ":" s += escapeString( v.@name.toString() ) + ":"
+ convertToString( o[ v.@name ] ); if (pretty) {
s += " "
}
s += convertToString( o[ v.@name ] );
} }
} }
@ -306,6 +376,91 @@ package com.adobe.serialization.json
return "{" + s + "}"; return "{" + s + "}";
} }
/**
* Converts an object to it's JSON string equivalent with multiple lines
*
* @param o The object to convert
* @return The JSON string representation of <code>o</code>
*/
private function objectToStringPretty( o:Object ):String
{
level++;
// create a string to store the object's jsonstring value
var s:String = "";
// determine if o is a class instance or a plain object
var classInfo:XML = describeType( o );
if ( classInfo.@name.toString() == "Object" )
{
// the value of o[key] in the loop below - store this
// as a variable so we don't have to keep looking up o[key]
// when testing for valid values to convert
var value:Object;
// loop over the keys in the object and add their converted
// values to the string
for ( var key:String in o )
{
// assign value to a variable for quick lookup
value = o[key];
// don't add function's to the JSON string
if ( value is Function )
{
// skip this key and try another
continue;
}
// when the length is 0 we're adding the first item so
// no comma is necessary
if ( s.length > 0 ) {
// we've already added an item, so add the comma separator
s += ",\n"
}
s += getPadding(level) + escapeString( key ) + ":"
if (pretty) {
s += " "
}
s += convertToString( value );
}
}
else // o is a class instance
{
// Loop over all of the variables and accessors in the class and
// serialize them along with their values.
for each ( var v:XML in classInfo..*.( name() == "variable" || name() == "accessor" ) )
{
// When the length is 0 we're adding the first item so
// no comma is necessary
if ( s.length > 0 ) {
// We've already added an item, so add the comma separator
s += ",\n"
}
s += getPadding(level) + escapeString( v.@name.toString() ) + ":"
if (pretty) {
s += " "
}
s += convertToString( o[ v.@name ] );
}
}
level--;
return "{" + "\n" + s + "\n" + getPadding(level) + "}";
}
private static function getPadding(level:int):String {
var length:int = level * tabWidth;
var s:String = "";
for (var i:int=1; i<=length; i++) {
s += " ";
}
return s;
}
} }

View file

@ -30,7 +30,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json { package com.maccherone.json {
/** /**
* *

View file

@ -30,7 +30,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json { package com.maccherone.json {
public class JSONToken { public class JSONToken {

View file

@ -30,7 +30,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json { package com.maccherone.json {
/** /**
* Class containing constant values for the different types * Class containing constant values for the different types

View file

@ -30,7 +30,7 @@
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/ */
package com.adobe.serialization.json { package com.maccherone.json {
public class JSONTokenizer { public class JSONTokenizer {
@ -54,12 +54,6 @@ package com.adobe.serialization.json {
/** The current character in the JSON string during parsing */ /** The current character in the JSON string during parsing */
private var ch:String; private var ch:String;
/**
* The regular expression used to make sure the string does not
* contain invalid control characters.
*/
private var controlCharsRegExp:RegExp = /[\x00-\x1F]/;
/** /**
* Constructs a new JSONDecoder to parse a JSON string * Constructs a new JSONDecoder to parse a JSON string
* into a native object. * into a native object.
@ -228,158 +222,119 @@ package com.adobe.serialization.json {
*/ */
private function readString():JSONToken private function readString():JSONToken
{ {
// Rather than examine the string character-by-character, it's // the string to store the string we'll try to read
// faster to use indexOf to try to and find the closing quote character var string:String = "";
// and then replace escape sequences after the fact.
// Start at the current input stream position // advance past the first "
var quoteIndex:int = loc;
do
{
// Find the next quote in the input stream
quoteIndex = jsonString.indexOf( "\"", quoteIndex );
if ( quoteIndex >= 0 )
{
// We found the next double quote character in the string, but we need
// to make sure it is not part of an escape sequence.
// Keep looping backwards while the previous character is a backslash
var backspaceCount:int = 0;
var backspaceIndex:int = quoteIndex - 1;
while ( jsonString.charAt( backspaceIndex ) == "\\" )
{
backspaceCount++;
backspaceIndex--;
}
// If we have an even number of backslashes, that means this is the ending quote
if ( backspaceCount % 2 == 0 )
{
break;
}
// At this point, the quote was determined to be part of an escape sequence
// so we need to move past the quote index to look for the next one
quoteIndex++;
}
else // There are no more quotes in the string and we haven't found the end yet
{
parseError( "Unterminated string literal" );
}
} while ( true );
// Unescape the string
// the token for the string we'll try to read
var token:JSONToken = new JSONToken();
token.type = JSONTokenType.STRING;
// Attach resulting string to the token to return it
token.value = unescapeString( jsonString.substr( loc, quoteIndex - loc ) );
// Move past the closing quote in the input string. This updates the next
// character in the input stream to be the character one after the closing quote
loc = quoteIndex + 1;
nextChar(); nextChar();
return token; while ( ch != '"' && ch != '' )
}
/**
* Convert all JavaScript escape characters into normal characters
*
* @param input The input string to convert
* @return Original string with escape characters replaced by real characters
*/
public function unescapeString( input:String ):String
{
// Issue #104 - If the string contains any unescaped control characters, this
// is an error in strict mode
if ( strict && controlCharsRegExp.test( input ) )
{ {
parseError( "String contains unescaped control character (0x00-0x1F)" ); // unescape the escape sequences in the string
} if ( ch == '\\' )
var result:String = "";
var backslashIndex:int = 0;
var nextSubstringStartPosition:int = 0;
var len:int = input.length;
do
{
// Find the next backslash in the input
backslashIndex = input.indexOf( '\\', nextSubstringStartPosition );
if ( backslashIndex >= 0 )
{ {
result += input.substr( nextSubstringStartPosition, backslashIndex - nextSubstringStartPosition ); // get the next character so we know what
// to unescape
nextChar();
// Move past the backslash and next character (all escape sequences are switch ( ch )
// two characters, except for \u, which will advance this further)
nextSubstringStartPosition = backslashIndex + 2;
// Check the next character so we know what to escape
var afterBackslashIndex:int = backslashIndex + 1;
var escapedChar:String = input.charAt( afterBackslashIndex );
switch ( escapedChar )
{ {
// Try to list the most common expected cases first to improve performance case '"': // quotation mark
string += '"';
break;
case '"': result += '"'; break; // quotation mark case '/': // solidus
case '\\': result += '\\'; break; // reverse solidus string += "/";
case 'n': result += '\n'; break; // newline break;
case 'r': result += '\r'; break; // carriage return
case 't': result += '\t'; break; // horizontal tab case '\\': // reverse solidus
string += '\\';
break;
case 'b': // bell
string += '\b';
break;
case 'f': // form feed
string += '\f';
break;
case 'n': // newline
string += '\n';
break;
case 'r': // carriage return
string += '\r';
break;
case 't': // horizontal tab
string += '\t'
break;
// Convert a unicode escape sequence to it's character value
case 'u': case 'u':
// convert a unicode escape sequence
// to it's character value - expecting
// 4 hex digits
// Save the characters as a string we'll convert to an int // save the characters as a string we'll convert to an int
var hexValue:String = ""; var hexValue:String = "";
// Make sure there are enough characters in the string leftover // try to find 4 hex characters
if ( nextSubstringStartPosition + 4 > len ) for ( var i:int = 0; i < 4; i++ )
{
parseError( "Unexpected end of input. Expecting 4 hex digits after \\u." );
}
// Try to find 4 hex characters
for ( var i:int = nextSubstringStartPosition; i < nextSubstringStartPosition + 4; i++ )
{ {
// get the next character and determine // get the next character and determine
// if it's a valid hex digit or not // if it's a valid hex digit or not
var possibleHexChar:String = input.charAt( i ); if ( !isHexDigit( nextChar() ) )
if ( !isHexDigit( possibleHexChar ) )
{ {
parseError( "Excepted a hex digit, but found: " + possibleHexChar ); parseError( " Excepted a hex digit, but found: " + ch );
} }
// valid, add it to the value
// Valid hex digit, add it to the value hexValue += ch;
hexValue += possibleHexChar;
} }
// Convert hexValue to an integer, and use that // convert hexValue to an integer, and use that
// integer value to create a character to add // integrer value to create a character to add
// to our string. // to our string.
result += String.fromCharCode( parseInt( hexValue, 16 ) ); string += String.fromCharCode( parseInt( hexValue, 16 ) );
// Move past the 4 hex digits that we just read
nextSubstringStartPosition += 4;
break; break;
case 'f': result += '\f'; break; // form feed default:
case '/': result += '/'; break; // solidus // couldn't unescape the sequence, so just
case 'b': result += '\b'; break; // bell // pass it through
default: result += '\\' + escapedChar; // Couldn't unescape the sequence, so just pass it through string += '\\' + ch;
} }
} }
else else
{ {
// No more backslashes to replace, append the rest of the string // didn't have to unescape, so add the character to the string
result += input.substr( nextSubstringStartPosition ); string += ch;
break;
} }
} while ( nextSubstringStartPosition < len ); // move to the next character
nextChar();
}
return result; // we read past the end of the string without closing it, which
// is a parse error
if ( ch == '' )
{
parseError( "Unterminated string literal" );
}
// move past the closing " in the input string
nextChar();
// the token for the string we've just read
var token:JSONToken = new JSONToken();
token.type = JSONTokenType.STRING;
// attach to the string to the token so we can return it
token.value = string;
return token;
} }
/** /**
@ -653,18 +608,7 @@ package com.adobe.serialization.json {
*/ */
private function isWhiteSpace( ch:String ):Boolean private function isWhiteSpace( ch:String ):Boolean
{ {
// Check for the whitespace defined in the spec return ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' );
if ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' )
{
return true;
}
// If we're not in strict mode, we also accept non-breaking space
else if ( !strict && ch.charCodeAt( 0 ) == 160 )
{
return true;
}
return false;
} }
/** /**
@ -678,13 +622,19 @@ package com.adobe.serialization.json {
} }
/** /**
* Determines if a character is a hex digit [0-9A-Fa-f]. * Determines if a character is a digit [0-9].
* *
* @return True if the character passed in is a hex digit * @return True if the character passed in is a digit
*/ */
private function isHexDigit( ch:String ):Boolean private function isHexDigit( ch:String ):Boolean
{ {
return ( isDigit( ch ) || ( ch >= 'A' && ch <= 'F' ) || ( ch >= 'a' && ch <= 'f' ) ); // get the uppercase value of ch so we only have
// to compare the value between 'A' and 'F'
var uc:String = ch.toUpperCase();
// a hex digit is a digit of A-F, inclusive ( using
// our uppercase constraint )
return ( isDigit( ch ) || ( uc >= 'A' && uc <= 'F' ) );
} }
/** /**

View file

@ -1,6 +1,5 @@
- decide on a license - decide on a license
- push to github - push to github
- human-readable JSON output
- finalize "library" notion - finalize "library" notion
- text objects - text objects
@ -19,4 +18,4 @@ DONE:
- create a readme - create a readme
- quit to menu - quit to menu
- fixed-sized factories - fixed-sized factories
- JSON output? - human-readable JSON output