package com.adobe.serialization.json
{
public class JSONTokenizer
{
/**
* Flag indicating if the tokenizer should only recognize
* standard JSON tokens. Setting to <code>false</code> allows
* tokens such as NaN and allows numbers to be formatted as
* hex, etc.
*/
private var strict:Boolean;
/** The object that will get parsed from the JSON string */
private var obj:Object;
/** The JSON string to be parsed */
private var jsonString:String;
/** The current parsing location in the JSON string */
private var loc:int;
/** The current character in the JSON string during parsing */
private var ch:String;
/**
* The regular expression used to make sure the string does not
* contain invalid control characters.
*/
private const controlCharsRegExp:RegExp = /[\x00-\x1F]/;
/**
* Constructs a new JSONDecoder to parse a JSON string
* into a native object.
*
* @param s The JSON string to be converted
* into a native object
*/
public function JSONTokenizer( s:String, strict:Boolean )
{
jsonString = s;
this.strict = strict;
loc = 0;
nextChar();
}
/**
* Gets the next token in the input sting and advances
* the character to the next character after the token
*/
public function getNextToken():JSONToken
{
var token:JSONToken = null;
skipIgnored();
switch ( ch )
{
case '{':
token = JSONToken.create( JSONTokenType.LEFT_BRACE, ch );
nextChar();
break
case '}':
token = JSONToken.create( JSONTokenType.RIGHT_BRACE, ch );
nextChar();
break
case '[':
token = JSONToken.create( JSONTokenType.LEFT_BRACKET, ch );
nextChar();
break
case ']':
token = JSONToken.create( JSONTokenType.RIGHT_BRACKET, ch );
nextChar();
break
case ',':
token = JSONToken.create( JSONTokenType.COMMA, ch );
nextChar();
break
case ':':
token = JSONToken.create( JSONTokenType.COLON, ch );
nextChar();
break;
case 't': var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
if ( possibleTrue == "true" )
{
token = JSONToken.create( JSONTokenType.TRUE, true );
nextChar();
}
else
{
parseError( "Expecting 'true' but found " + possibleTrue );
}
break;
case 'f': var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
if ( possibleFalse == "false" )
{
token = JSONToken.create( JSONTokenType.FALSE, false );
nextChar();
}
else
{
parseError( "Expecting 'false' but found " + possibleFalse );
}
break;
case 'n': var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
if ( possibleNull == "null" )
{
token = JSONToken.create( JSONTokenType.NULL, null );
nextChar();
}
else
{
parseError( "Expecting 'null' but found " + possibleNull );
}
break;
case 'N': var possibleNaN:String = "N" + nextChar() + nextChar();
if ( possibleNaN == "NaN" )
{
token = JSONToken.create( JSONTokenType.NAN, NaN );
nextChar();
}
else
{
parseError( "Expecting 'NaN' but found " + possibleNaN );
}
break;
case '"': token = readString();
break;
default:
if ( isDigit( ch ) || ch == '-' )
{
token = readNumber();
}
else if ( ch == '' )
{
token = null;
}
else
{
parseError( "Unexpected " + ch + " encountered" );
}
}
return token;
}
/**
* Attempts to read a string from the input string. Places
* the character location at the first character after the
* string. It is assumed that ch is " before this method is called.
*
* @return the JSONToken with the string value if a string could
* be read. Throws an error otherwise.
*/
private final function readString():JSONToken
{
var quoteIndex:int = loc;
do
{
quoteIndex = jsonString.indexOf( "\"", quoteIndex );
if ( quoteIndex >= 0 )
{
var backspaceCount:int = 0;
var backspaceIndex:int = quoteIndex - 1;
while ( jsonString.charAt( backspaceIndex ) == "\\" )
{
backspaceCount++;
backspaceIndex--;
}
if ( ( backspaceCount & 1 ) == 0 )
{
break;
}
quoteIndex++;
}
else {
parseError( "Unterminated string literal" );
}
} while ( true );
var token:JSONToken = JSONToken.create(
JSONTokenType.STRING,
unescapeString( jsonString.substr( loc, quoteIndex - loc ) ) );
loc = quoteIndex + 1;
nextChar();
return token;
}
/**
* 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
{
if ( strict && controlCharsRegExp.test( input ) )
{
parseError( "String contains unescaped control character (0x00-0x1F)" );
}
var result:String = "";
var backslashIndex:int = 0;
var nextSubstringStartPosition:int = 0;
var len:int = input.length;
do
{
backslashIndex = input.indexOf( '\\', nextSubstringStartPosition );
if ( backslashIndex >= 0 )
{
result += input.substr( nextSubstringStartPosition, backslashIndex - nextSubstringStartPosition );
nextSubstringStartPosition = backslashIndex + 2;
var escapedChar:String = input.charAt( backslashIndex + 1 );
switch ( escapedChar )
{
case '"':
result += escapedChar;
break; case '\\':
result += escapedChar;
break; case 'n':
result += '\n';
break; case 'r':
result += '\r';
break; case 't':
result += '\t';
break;
case 'u':
var hexValue:String = "";
var unicodeEndPosition:int = nextSubstringStartPosition + 4;
if ( unicodeEndPosition > len )
{
parseError( "Unexpected end of input. Expecting 4 hex digits after \\u." );
}
for ( var i:int = nextSubstringStartPosition; i < unicodeEndPosition; i++ )
{
var possibleHexChar:String = input.charAt( i );
if ( !isHexDigit( possibleHexChar ) )
{
parseError( "Excepted a hex digit, but found: " + possibleHexChar );
}
hexValue += possibleHexChar;
}
result += String.fromCharCode( parseInt( hexValue, 16 ) );
nextSubstringStartPosition = unicodeEndPosition;
break;
case 'f':
result += '\f';
break; case '/':
result += '/';
break; case 'b':
result += '\b';
break; default:
result += '\\' + escapedChar; }
}
else
{
result += input.substr( nextSubstringStartPosition );
break;
}
} while ( nextSubstringStartPosition < len );
return result;
}
/**
* Attempts to read a number from the input string. Places
* the character location at the first character after the
* number.
*
* @return The JSONToken with the number value if a number could
* be read. Throws an error otherwise.
*/
private final function readNumber():JSONToken
{
var input:String = "";
if ( ch == '-' )
{
input += '-';
nextChar();
}
if ( !isDigit( ch ) )
{
parseError( "Expecting a digit" );
}
if ( ch == '0' )
{
input += ch;
nextChar();
if ( isDigit( ch ) )
{
parseError( "A digit cannot immediately follow 0" );
}
else if ( !strict && ch == 'x' )
{
input += ch;
nextChar();
if ( isHexDigit( ch ) )
{
input += ch;
nextChar();
}
else
{
parseError( "Number in hex format require at least one hex digit after \"0x\"" );
}
while ( isHexDigit( ch ) )
{
input += ch;
nextChar();
}
}
}
else
{
while ( isDigit( ch ) )
{
input += ch;
nextChar();
}
}
if ( ch == '.' )
{
input += '.';
nextChar();
if ( !isDigit( ch ) )
{
parseError( "Expecting a digit" );
}
while ( isDigit( ch ) )
{
input += ch;
nextChar();
}
}
if ( ch == 'e' || ch == 'E' )
{
input += "e"
nextChar();
if ( ch == '+' || ch == '-' )
{
input += ch;
nextChar();
}
if ( !isDigit( ch ) )
{
parseError( "Scientific notation number needs exponent value" );
}
while ( isDigit( ch ) )
{
input += ch;
nextChar();
}
}
var num:Number = Number( input );
if ( isFinite( num ) && !isNaN( num ) )
{
return JSONToken.create( JSONTokenType.NUMBER, num );
}
else
{
parseError( "Number " + num + " is not valid!" );
}
return null;
}
/**
* Reads the next character in the input
* string and advances the character location.
*
* @return The next character in the input string, or
* null if we've read past the end.
*/
private final function nextChar():String
{
return ch = jsonString.charAt( loc++ );
}
/**
* Advances the character location past any
* sort of white space and comments
*/
private final function skipIgnored():void
{
var originalLoc:int;
do
{
originalLoc = loc;
skipWhite();
skipComments();
} while ( originalLoc != loc );
}
/**
* Skips comments in the input string, either
* single-line or multi-line. Advances the character
* to the first position after the end of the comment.
*/
private function skipComments():void
{
if ( ch == '/' )
{
nextChar();
switch ( ch )
{
case '/':
do
{
nextChar();
} while ( ch != '\n' && ch != '' )
nextChar();
break;
case '*':
nextChar();
while ( true )
{
if ( ch == '*' )
{
nextChar();
if ( ch == '/' )
{
nextChar();
break;
}
}
else
{
nextChar();
}
if ( ch == '' )
{
parseError( "Multi-line comment not closed" );
}
}
break;
default:
parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );
}
}
}
/**
* Skip any whitespace in the input string and advances
* the character to the first character after any possible
* whitespace.
*/
private final function skipWhite():void
{
while ( isWhiteSpace( ch ) )
{
nextChar();
}
}
/**
* Determines if a character is whitespace or not.
*
* @return True if the character passed in is a whitespace
* character
*/
private final function isWhiteSpace( ch:String ):Boolean
{
if ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' )
{
return true;
}
else if ( !strict && ch.charCodeAt( 0 ) == 160 )
{
return true;
}
return false;
}
/**
* Determines if a character is a digit [0-9].
*
* @return True if the character passed in is a digit
*/
private final function isDigit( ch:String ):Boolean
{
return ( ch >= '0' && ch <= '9' );
}
/**
* Determines if a character is a hex digit [0-9A-Fa-f].
*
* @return True if the character passed in is a hex digit
*/
private final function isHexDigit( ch:String ):Boolean
{
return ( isDigit( ch ) || ( ch >= 'A' && ch <= 'F' ) || ( ch >= 'a' && ch <= 'f' ) );
}
/**
* Raises a parsing error with a specified message, tacking
* on the error location and the original string.
*
* @param message The message indicating why the error occurred
*/
public final function parseError( message:String ):void
{
throw new JSONParseError( message, loc, jsonString );
}
}
}