mas01mj@732
|
1 /*
|
mas01mj@732
|
2 Copyright (c) 2008, Adobe Systems Incorporated
|
mas01mj@732
|
3 All rights reserved.
|
mas01mj@732
|
4
|
mas01mj@732
|
5 Redistribution and use in source and binary forms, with or without
|
mas01mj@732
|
6 modification, are permitted provided that the following conditions are
|
mas01mj@732
|
7 met:
|
mas01mj@732
|
8
|
mas01mj@732
|
9 * Redistributions of source code must retain the above copyright notice,
|
mas01mj@732
|
10 this list of conditions and the following disclaimer.
|
mas01mj@732
|
11
|
mas01mj@732
|
12 * Redistributions in binary form must reproduce the above copyright
|
mas01mj@732
|
13 notice, this list of conditions and the following disclaimer in the
|
mas01mj@732
|
14 documentation and/or other materials provided with the distribution.
|
mas01mj@732
|
15
|
mas01mj@732
|
16 * Neither the name of Adobe Systems Incorporated nor the names of its
|
mas01mj@732
|
17 contributors may be used to endorse or promote products derived from
|
mas01mj@732
|
18 this software without specific prior written permission.
|
mas01mj@732
|
19
|
mas01mj@732
|
20 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
|
mas01mj@732
|
21 IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
mas01mj@732
|
22 THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
mas01mj@732
|
23 PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
|
mas01mj@732
|
24 CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
mas01mj@732
|
25 EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
|
mas01mj@732
|
26 PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
|
mas01mj@732
|
27 PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
|
mas01mj@732
|
28 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
|
mas01mj@732
|
29 NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
mas01mj@732
|
30 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
mas01mj@732
|
31 */
|
mas01mj@732
|
32
|
mas01mj@732
|
33 package com.adobe.serialization.json {
|
mas01mj@732
|
34
|
mas01mj@732
|
35 public class JSONTokenizer {
|
mas01mj@732
|
36
|
mas01mj@732
|
37 /**
|
mas01mj@732
|
38 * Flag indicating if the tokenizer should only recognize
|
mas01mj@732
|
39 * standard JSON tokens. Setting to <code>false</code> allows
|
mas01mj@732
|
40 * tokens such as NaN and allows numbers to be formatted as
|
mas01mj@732
|
41 * hex, etc.
|
mas01mj@732
|
42 */
|
mas01mj@732
|
43 private var strict:Boolean;
|
mas01mj@732
|
44
|
mas01mj@732
|
45 /** The object that will get parsed from the JSON string */
|
mas01mj@732
|
46 private var obj:Object;
|
mas01mj@732
|
47
|
mas01mj@732
|
48 /** The JSON string to be parsed */
|
mas01mj@732
|
49 private var jsonString:String;
|
mas01mj@732
|
50
|
mas01mj@732
|
51 /** The current parsing location in the JSON string */
|
mas01mj@732
|
52 private var loc:int;
|
mas01mj@732
|
53
|
mas01mj@732
|
54 /** The current character in the JSON string during parsing */
|
mas01mj@732
|
55 private var ch:String;
|
mas01mj@732
|
56
|
mas01mj@732
|
57 /**
|
mas01mj@732
|
58 * The regular expression used to make sure the string does not
|
mas01mj@732
|
59 * contain invalid control characters.
|
mas01mj@732
|
60 */
|
mas01mj@732
|
61 private var controlCharsRegExp:RegExp = /[\x00-\x1F]/;
|
mas01mj@732
|
62
|
mas01mj@732
|
63 /**
|
mas01mj@732
|
64 * Constructs a new JSONDecoder to parse a JSON string
|
mas01mj@732
|
65 * into a native object.
|
mas01mj@732
|
66 *
|
mas01mj@732
|
67 * @param s The JSON string to be converted
|
mas01mj@732
|
68 * into a native object
|
mas01mj@732
|
69 */
|
mas01mj@732
|
70 public function JSONTokenizer( s:String, strict:Boolean )
|
mas01mj@732
|
71 {
|
mas01mj@732
|
72 jsonString = s;
|
mas01mj@732
|
73 this.strict = strict;
|
mas01mj@732
|
74 loc = 0;
|
mas01mj@732
|
75
|
mas01mj@732
|
76 // prime the pump by getting the first character
|
mas01mj@732
|
77 nextChar();
|
mas01mj@732
|
78 }
|
mas01mj@732
|
79
|
mas01mj@732
|
80 /**
|
mas01mj@732
|
81 * Gets the next token in the input sting and advances
|
mas01mj@732
|
82 * the character to the next character after the token
|
mas01mj@732
|
83 */
|
mas01mj@732
|
84 public function getNextToken():JSONToken
|
mas01mj@732
|
85 {
|
mas01mj@732
|
86 var token:JSONToken = new JSONToken();
|
mas01mj@732
|
87
|
mas01mj@732
|
88 // skip any whitespace / comments since the last
|
mas01mj@732
|
89 // token was read
|
mas01mj@732
|
90 skipIgnored();
|
mas01mj@732
|
91
|
mas01mj@732
|
92 // examine the new character and see what we have...
|
mas01mj@732
|
93 switch ( ch )
|
mas01mj@732
|
94 {
|
mas01mj@732
|
95 case '{':
|
mas01mj@732
|
96 token.type = JSONTokenType.LEFT_BRACE;
|
mas01mj@732
|
97 token.value = '{';
|
mas01mj@732
|
98 nextChar();
|
mas01mj@732
|
99 break
|
mas01mj@732
|
100
|
mas01mj@732
|
101 case '}':
|
mas01mj@732
|
102 token.type = JSONTokenType.RIGHT_BRACE;
|
mas01mj@732
|
103 token.value = '}';
|
mas01mj@732
|
104 nextChar();
|
mas01mj@732
|
105 break
|
mas01mj@732
|
106
|
mas01mj@732
|
107 case '[':
|
mas01mj@732
|
108 token.type = JSONTokenType.LEFT_BRACKET;
|
mas01mj@732
|
109 token.value = '[';
|
mas01mj@732
|
110 nextChar();
|
mas01mj@732
|
111 break
|
mas01mj@732
|
112
|
mas01mj@732
|
113 case ']':
|
mas01mj@732
|
114 token.type = JSONTokenType.RIGHT_BRACKET;
|
mas01mj@732
|
115 token.value = ']';
|
mas01mj@732
|
116 nextChar();
|
mas01mj@732
|
117 break
|
mas01mj@732
|
118
|
mas01mj@732
|
119 case ',':
|
mas01mj@732
|
120 token.type = JSONTokenType.COMMA;
|
mas01mj@732
|
121 token.value = ',';
|
mas01mj@732
|
122 nextChar();
|
mas01mj@732
|
123 break
|
mas01mj@732
|
124
|
mas01mj@732
|
125 case ':':
|
mas01mj@732
|
126 token.type = JSONTokenType.COLON;
|
mas01mj@732
|
127 token.value = ':';
|
mas01mj@732
|
128 nextChar();
|
mas01mj@732
|
129 break;
|
mas01mj@732
|
130
|
mas01mj@732
|
131 case 't': // attempt to read true
|
mas01mj@732
|
132 var possibleTrue:String = "t" + nextChar() + nextChar() + nextChar();
|
mas01mj@732
|
133
|
mas01mj@732
|
134 if ( possibleTrue == "true" )
|
mas01mj@732
|
135 {
|
mas01mj@732
|
136 token.type = JSONTokenType.TRUE;
|
mas01mj@732
|
137 token.value = true;
|
mas01mj@732
|
138 nextChar();
|
mas01mj@732
|
139 }
|
mas01mj@732
|
140 else
|
mas01mj@732
|
141 {
|
mas01mj@732
|
142 parseError( "Expecting 'true' but found " + possibleTrue );
|
mas01mj@732
|
143 }
|
mas01mj@732
|
144
|
mas01mj@732
|
145 break;
|
mas01mj@732
|
146
|
mas01mj@732
|
147 case 'f': // attempt to read false
|
mas01mj@732
|
148 var possibleFalse:String = "f" + nextChar() + nextChar() + nextChar() + nextChar();
|
mas01mj@732
|
149
|
mas01mj@732
|
150 if ( possibleFalse == "false" )
|
mas01mj@732
|
151 {
|
mas01mj@732
|
152 token.type = JSONTokenType.FALSE;
|
mas01mj@732
|
153 token.value = false;
|
mas01mj@732
|
154 nextChar();
|
mas01mj@732
|
155 }
|
mas01mj@732
|
156 else
|
mas01mj@732
|
157 {
|
mas01mj@732
|
158 parseError( "Expecting 'false' but found " + possibleFalse );
|
mas01mj@732
|
159 }
|
mas01mj@732
|
160
|
mas01mj@732
|
161 break;
|
mas01mj@732
|
162
|
mas01mj@732
|
163 case 'n': // attempt to read null
|
mas01mj@732
|
164 var possibleNull:String = "n" + nextChar() + nextChar() + nextChar();
|
mas01mj@732
|
165
|
mas01mj@732
|
166 if ( possibleNull == "null" )
|
mas01mj@732
|
167 {
|
mas01mj@732
|
168 token.type = JSONTokenType.NULL;
|
mas01mj@732
|
169 token.value = null;
|
mas01mj@732
|
170 nextChar();
|
mas01mj@732
|
171 }
|
mas01mj@732
|
172 else
|
mas01mj@732
|
173 {
|
mas01mj@732
|
174 parseError( "Expecting 'null' but found " + possibleNull );
|
mas01mj@732
|
175 }
|
mas01mj@732
|
176
|
mas01mj@732
|
177 break;
|
mas01mj@732
|
178
|
mas01mj@732
|
179 case 'N': // attempt to read NaN
|
mas01mj@732
|
180 var possibleNaN:String = "N" + nextChar() + nextChar();
|
mas01mj@732
|
181
|
mas01mj@732
|
182 if ( possibleNaN == "NaN" )
|
mas01mj@732
|
183 {
|
mas01mj@732
|
184 token.type = JSONTokenType.NAN;
|
mas01mj@732
|
185 token.value = NaN;
|
mas01mj@732
|
186 nextChar();
|
mas01mj@732
|
187 }
|
mas01mj@732
|
188 else
|
mas01mj@732
|
189 {
|
mas01mj@732
|
190 parseError( "Expecting 'NaN' but found " + possibleNaN );
|
mas01mj@732
|
191 }
|
mas01mj@732
|
192
|
mas01mj@732
|
193 break;
|
mas01mj@732
|
194
|
mas01mj@732
|
195 case '"': // the start of a string
|
mas01mj@732
|
196 token = readString();
|
mas01mj@732
|
197 break;
|
mas01mj@732
|
198
|
mas01mj@732
|
199 default:
|
mas01mj@732
|
200 // see if we can read a number
|
mas01mj@732
|
201 if ( isDigit( ch ) || ch == '-' )
|
mas01mj@732
|
202 {
|
mas01mj@732
|
203 token = readNumber();
|
mas01mj@732
|
204 }
|
mas01mj@732
|
205 else if ( ch == '' )
|
mas01mj@732
|
206 {
|
mas01mj@732
|
207 // check for reading past the end of the string
|
mas01mj@732
|
208 return null;
|
mas01mj@732
|
209 }
|
mas01mj@732
|
210 else
|
mas01mj@732
|
211 {
|
mas01mj@732
|
212 // not sure what was in the input string - it's not
|
mas01mj@732
|
213 // anything we expected
|
mas01mj@732
|
214 parseError( "Unexpected " + ch + " encountered" );
|
mas01mj@732
|
215 }
|
mas01mj@732
|
216 }
|
mas01mj@732
|
217
|
mas01mj@732
|
218 return token;
|
mas01mj@732
|
219 }
|
mas01mj@732
|
220
|
mas01mj@732
|
221 /**
|
mas01mj@732
|
222 * Attempts to read a string from the input string. Places
|
mas01mj@732
|
223 * the character location at the first character after the
|
mas01mj@732
|
224 * string. It is assumed that ch is " before this method is called.
|
mas01mj@732
|
225 *
|
mas01mj@732
|
226 * @return the JSONToken with the string value if a string could
|
mas01mj@732
|
227 * be read. Throws an error otherwise.
|
mas01mj@732
|
228 */
|
mas01mj@732
|
229 private function readString():JSONToken
|
mas01mj@732
|
230 {
|
mas01mj@732
|
231 // Rather than examine the string character-by-character, it's
|
mas01mj@732
|
232 // faster to use indexOf to try to and find the closing quote character
|
mas01mj@732
|
233 // and then replace escape sequences after the fact.
|
mas01mj@732
|
234
|
mas01mj@732
|
235 // Start at the current input stream position
|
mas01mj@732
|
236 var quoteIndex:int = loc;
|
mas01mj@732
|
237 do
|
mas01mj@732
|
238 {
|
mas01mj@732
|
239 // Find the next quote in the input stream
|
mas01mj@732
|
240 quoteIndex = jsonString.indexOf( "\"", quoteIndex );
|
mas01mj@732
|
241
|
mas01mj@732
|
242 if ( quoteIndex >= 0 )
|
mas01mj@732
|
243 {
|
mas01mj@732
|
244 // We found the next double quote character in the string, but we need
|
mas01mj@732
|
245 // to make sure it is not part of an escape sequence.
|
mas01mj@732
|
246
|
mas01mj@732
|
247 // Keep looping backwards while the previous character is a backslash
|
mas01mj@732
|
248 var backspaceCount:int = 0;
|
mas01mj@732
|
249 var backspaceIndex:int = quoteIndex - 1;
|
mas01mj@732
|
250 while ( jsonString.charAt( backspaceIndex ) == "\\" )
|
mas01mj@732
|
251 {
|
mas01mj@732
|
252 backspaceCount++;
|
mas01mj@732
|
253 backspaceIndex--;
|
mas01mj@732
|
254 }
|
mas01mj@732
|
255
|
mas01mj@732
|
256 // If we have an even number of backslashes, that means this is the ending quote
|
mas01mj@732
|
257 if ( backspaceCount % 2 == 0 )
|
mas01mj@732
|
258 {
|
mas01mj@732
|
259 break;
|
mas01mj@732
|
260 }
|
mas01mj@732
|
261
|
mas01mj@732
|
262 // At this point, the quote was determined to be part of an escape sequence
|
mas01mj@732
|
263 // so we need to move past the quote index to look for the next one
|
mas01mj@732
|
264 quoteIndex++;
|
mas01mj@732
|
265 }
|
mas01mj@732
|
266 else // There are no more quotes in the string and we haven't found the end yet
|
mas01mj@732
|
267 {
|
mas01mj@732
|
268 parseError( "Unterminated string literal" );
|
mas01mj@732
|
269 }
|
mas01mj@732
|
270 } while ( true );
|
mas01mj@732
|
271
|
mas01mj@732
|
272 // Unescape the string
|
mas01mj@732
|
273 // the token for the string we'll try to read
|
mas01mj@732
|
274 var token:JSONToken = new JSONToken();
|
mas01mj@732
|
275 token.type = JSONTokenType.STRING;
|
mas01mj@732
|
276 // Attach resulting string to the token to return it
|
mas01mj@732
|
277 token.value = unescapeString( jsonString.substr( loc, quoteIndex - loc ) );
|
mas01mj@732
|
278
|
mas01mj@732
|
279 // Move past the closing quote in the input string. This updates the next
|
mas01mj@732
|
280 // character in the input stream to be the character one after the closing quote
|
mas01mj@732
|
281 loc = quoteIndex + 1;
|
mas01mj@732
|
282 nextChar();
|
mas01mj@732
|
283
|
mas01mj@732
|
284 return token;
|
mas01mj@732
|
285 }
|
mas01mj@732
|
286
|
mas01mj@732
|
287 /**
|
mas01mj@732
|
288 * Convert all JavaScript escape characters into normal characters
|
mas01mj@732
|
289 *
|
mas01mj@732
|
290 * @param input The input string to convert
|
mas01mj@732
|
291 * @return Original string with escape characters replaced by real characters
|
mas01mj@732
|
292 */
|
mas01mj@732
|
293 public function unescapeString( input:String ):String
|
mas01mj@732
|
294 {
|
mas01mj@732
|
295 // Issue #104 - If the string contains any unescaped control characters, this
|
mas01mj@732
|
296 // is an error in strict mode
|
mas01mj@732
|
297 if ( strict && controlCharsRegExp.test( input ) )
|
mas01mj@732
|
298 {
|
mas01mj@732
|
299 parseError( "String contains unescaped control character (0x00-0x1F)" );
|
mas01mj@732
|
300 }
|
mas01mj@732
|
301
|
mas01mj@732
|
302 var result:String = "";
|
mas01mj@732
|
303 var backslashIndex:int = 0;
|
mas01mj@732
|
304 var nextSubstringStartPosition:int = 0;
|
mas01mj@732
|
305 var len:int = input.length;
|
mas01mj@732
|
306 do
|
mas01mj@732
|
307 {
|
mas01mj@732
|
308 // Find the next backslash in the input
|
mas01mj@732
|
309 backslashIndex = input.indexOf( '\\', nextSubstringStartPosition );
|
mas01mj@732
|
310
|
mas01mj@732
|
311 if ( backslashIndex >= 0 )
|
mas01mj@732
|
312 {
|
mas01mj@732
|
313 result += input.substr( nextSubstringStartPosition, backslashIndex - nextSubstringStartPosition );
|
mas01mj@732
|
314
|
mas01mj@732
|
315 // Move past the backslash and next character (all escape sequences are
|
mas01mj@732
|
316 // two characters, except for \u, which will advance this further)
|
mas01mj@732
|
317 nextSubstringStartPosition = backslashIndex + 2;
|
mas01mj@732
|
318
|
mas01mj@732
|
319 // Check the next character so we know what to escape
|
mas01mj@732
|
320 var afterBackslashIndex:int = backslashIndex + 1;
|
mas01mj@732
|
321 var escapedChar:String = input.charAt( afterBackslashIndex );
|
mas01mj@732
|
322 switch ( escapedChar )
|
mas01mj@732
|
323 {
|
mas01mj@732
|
324 // Try to list the most common expected cases first to improve performance
|
mas01mj@732
|
325
|
mas01mj@732
|
326 case '"': result += '"'; break; // quotation mark
|
mas01mj@732
|
327 case '\\': result += '\\'; break; // reverse solidus
|
mas01mj@732
|
328 case 'n': result += '\n'; break; // newline
|
mas01mj@732
|
329 case 'r': result += '\r'; break; // carriage return
|
mas01mj@732
|
330 case 't': result += '\t'; break; // horizontal tab
|
mas01mj@732
|
331
|
mas01mj@732
|
332 // Convert a unicode escape sequence to it's character value
|
mas01mj@732
|
333 case 'u':
|
mas01mj@732
|
334
|
mas01mj@732
|
335 // Save the characters as a string we'll convert to an int
|
mas01mj@732
|
336 var hexValue:String = "";
|
mas01mj@732
|
337
|
mas01mj@732
|
338 // Make sure there are enough characters in the string leftover
|
mas01mj@732
|
339 if ( nextSubstringStartPosition + 4 > len )
|
mas01mj@732
|
340 {
|
mas01mj@732
|
341 parseError( "Unexpected end of input. Expecting 4 hex digits after \\u." );
|
mas01mj@732
|
342 }
|
mas01mj@732
|
343
|
mas01mj@732
|
344 // Try to find 4 hex characters
|
mas01mj@732
|
345 for ( var i:int = nextSubstringStartPosition; i < nextSubstringStartPosition + 4; i++ )
|
mas01mj@732
|
346 {
|
mas01mj@732
|
347 // get the next character and determine
|
mas01mj@732
|
348 // if it's a valid hex digit or not
|
mas01mj@732
|
349 var possibleHexChar:String = input.charAt( i );
|
mas01mj@732
|
350 if ( !isHexDigit( possibleHexChar ) )
|
mas01mj@732
|
351 {
|
mas01mj@732
|
352 parseError( "Excepted a hex digit, but found: " + possibleHexChar );
|
mas01mj@732
|
353 }
|
mas01mj@732
|
354
|
mas01mj@732
|
355 // Valid hex digit, add it to the value
|
mas01mj@732
|
356 hexValue += possibleHexChar;
|
mas01mj@732
|
357 }
|
mas01mj@732
|
358
|
mas01mj@732
|
359 // Convert hexValue to an integer, and use that
|
mas01mj@732
|
360 // integer value to create a character to add
|
mas01mj@732
|
361 // to our string.
|
mas01mj@732
|
362 result += String.fromCharCode( parseInt( hexValue, 16 ) );
|
mas01mj@732
|
363 // Move past the 4 hex digits that we just read
|
mas01mj@732
|
364 nextSubstringStartPosition += 4;
|
mas01mj@732
|
365 break;
|
mas01mj@732
|
366
|
mas01mj@732
|
367 case 'f': result += '\f'; break; // form feed
|
mas01mj@732
|
368 case '/': result += '/'; break; // solidus
|
mas01mj@732
|
369 case 'b': result += '\b'; break; // bell
|
mas01mj@732
|
370 default: result += '\\' + escapedChar; // Couldn't unescape the sequence, so just pass it through
|
mas01mj@732
|
371 }
|
mas01mj@732
|
372 }
|
mas01mj@732
|
373 else
|
mas01mj@732
|
374 {
|
mas01mj@732
|
375 // No more backslashes to replace, append the rest of the string
|
mas01mj@732
|
376 result += input.substr( nextSubstringStartPosition );
|
mas01mj@732
|
377 break;
|
mas01mj@732
|
378 }
|
mas01mj@732
|
379
|
mas01mj@732
|
380 } while ( nextSubstringStartPosition < len );
|
mas01mj@732
|
381
|
mas01mj@732
|
382 return result;
|
mas01mj@732
|
383 }
|
mas01mj@732
|
384
|
mas01mj@732
|
385 /**
|
mas01mj@732
|
386 * Attempts to read a number from the input string. Places
|
mas01mj@732
|
387 * the character location at the first character after the
|
mas01mj@732
|
388 * number.
|
mas01mj@732
|
389 *
|
mas01mj@732
|
390 * @return The JSONToken with the number value if a number could
|
mas01mj@732
|
391 * be read. Throws an error otherwise.
|
mas01mj@732
|
392 */
|
mas01mj@732
|
393 private function readNumber():JSONToken
|
mas01mj@732
|
394 {
|
mas01mj@732
|
395 // the string to accumulate the number characters
|
mas01mj@732
|
396 // into that we'll convert to a number at the end
|
mas01mj@732
|
397 var input:String = "";
|
mas01mj@732
|
398
|
mas01mj@732
|
399 // check for a negative number
|
mas01mj@732
|
400 if ( ch == '-' )
|
mas01mj@732
|
401 {
|
mas01mj@732
|
402 input += '-';
|
mas01mj@732
|
403 nextChar();
|
mas01mj@732
|
404 }
|
mas01mj@732
|
405
|
mas01mj@732
|
406 // the number must start with a digit
|
mas01mj@732
|
407 if ( !isDigit( ch ) )
|
mas01mj@732
|
408 {
|
mas01mj@732
|
409 parseError( "Expecting a digit" );
|
mas01mj@732
|
410 }
|
mas01mj@732
|
411
|
mas01mj@732
|
412 // 0 can only be the first digit if it
|
mas01mj@732
|
413 // is followed by a decimal point
|
mas01mj@732
|
414 if ( ch == '0' )
|
mas01mj@732
|
415 {
|
mas01mj@732
|
416 input += ch;
|
mas01mj@732
|
417 nextChar();
|
mas01mj@732
|
418
|
mas01mj@732
|
419 // make sure no other digits come after 0
|
mas01mj@732
|
420 if ( isDigit( ch ) )
|
mas01mj@732
|
421 {
|
mas01mj@732
|
422 parseError( "A digit cannot immediately follow 0" );
|
mas01mj@732
|
423 }
|
mas01mj@732
|
424 // unless we have 0x which starts a hex number, but this
|
mas01mj@732
|
425 // doesn't match JSON spec so check for not strict mode.
|
mas01mj@732
|
426 else if ( !strict && ch == 'x' )
|
mas01mj@732
|
427 {
|
mas01mj@732
|
428 // include the x in the input
|
mas01mj@732
|
429 input += ch;
|
mas01mj@732
|
430 nextChar();
|
mas01mj@732
|
431
|
mas01mj@732
|
432 // need at least one hex digit after 0x to
|
mas01mj@732
|
433 // be valid
|
mas01mj@732
|
434 if ( isHexDigit( ch ) )
|
mas01mj@732
|
435 {
|
mas01mj@732
|
436 input += ch;
|
mas01mj@732
|
437 nextChar();
|
mas01mj@732
|
438 }
|
mas01mj@732
|
439 else
|
mas01mj@732
|
440 {
|
mas01mj@732
|
441 parseError( "Number in hex format require at least one hex digit after \"0x\"" );
|
mas01mj@732
|
442 }
|
mas01mj@732
|
443
|
mas01mj@732
|
444 // consume all of the hex values
|
mas01mj@732
|
445 while ( isHexDigit( ch ) )
|
mas01mj@732
|
446 {
|
mas01mj@732
|
447 input += ch;
|
mas01mj@732
|
448 nextChar();
|
mas01mj@732
|
449 }
|
mas01mj@732
|
450 }
|
mas01mj@732
|
451 }
|
mas01mj@732
|
452 else
|
mas01mj@732
|
453 {
|
mas01mj@732
|
454 // read numbers while we can
|
mas01mj@732
|
455 while ( isDigit( ch ) )
|
mas01mj@732
|
456 {
|
mas01mj@732
|
457 input += ch;
|
mas01mj@732
|
458 nextChar();
|
mas01mj@732
|
459 }
|
mas01mj@732
|
460 }
|
mas01mj@732
|
461
|
mas01mj@732
|
462 // check for a decimal value
|
mas01mj@732
|
463 if ( ch == '.' )
|
mas01mj@732
|
464 {
|
mas01mj@732
|
465 input += '.';
|
mas01mj@732
|
466 nextChar();
|
mas01mj@732
|
467
|
mas01mj@732
|
468 // after the decimal there has to be a digit
|
mas01mj@732
|
469 if ( !isDigit( ch ) )
|
mas01mj@732
|
470 {
|
mas01mj@732
|
471 parseError( "Expecting a digit" );
|
mas01mj@732
|
472 }
|
mas01mj@732
|
473
|
mas01mj@732
|
474 // read more numbers to get the decimal value
|
mas01mj@732
|
475 while ( isDigit( ch ) )
|
mas01mj@732
|
476 {
|
mas01mj@732
|
477 input += ch;
|
mas01mj@732
|
478 nextChar();
|
mas01mj@732
|
479 }
|
mas01mj@732
|
480 }
|
mas01mj@732
|
481
|
mas01mj@732
|
482 // check for scientific notation
|
mas01mj@732
|
483 if ( ch == 'e' || ch == 'E' )
|
mas01mj@732
|
484 {
|
mas01mj@732
|
485 input += "e"
|
mas01mj@732
|
486 nextChar();
|
mas01mj@732
|
487 // check for sign
|
mas01mj@732
|
488 if ( ch == '+' || ch == '-' )
|
mas01mj@732
|
489 {
|
mas01mj@732
|
490 input += ch;
|
mas01mj@732
|
491 nextChar();
|
mas01mj@732
|
492 }
|
mas01mj@732
|
493
|
mas01mj@732
|
494 // require at least one number for the exponent
|
mas01mj@732
|
495 // in this case
|
mas01mj@732
|
496 if ( !isDigit( ch ) )
|
mas01mj@732
|
497 {
|
mas01mj@732
|
498 parseError( "Scientific notation number needs exponent value" );
|
mas01mj@732
|
499 }
|
mas01mj@732
|
500
|
mas01mj@732
|
501 // read in the exponent
|
mas01mj@732
|
502 while ( isDigit( ch ) )
|
mas01mj@732
|
503 {
|
mas01mj@732
|
504 input += ch;
|
mas01mj@732
|
505 nextChar();
|
mas01mj@732
|
506 }
|
mas01mj@732
|
507 }
|
mas01mj@732
|
508
|
mas01mj@732
|
509 // convert the string to a number value
|
mas01mj@732
|
510 var num:Number = Number( input );
|
mas01mj@732
|
511
|
mas01mj@732
|
512 if ( isFinite( num ) && !isNaN( num ) )
|
mas01mj@732
|
513 {
|
mas01mj@732
|
514 // the token for the number that we've read
|
mas01mj@732
|
515 var token:JSONToken = new JSONToken();
|
mas01mj@732
|
516 token.type = JSONTokenType.NUMBER;
|
mas01mj@732
|
517 token.value = num;
|
mas01mj@732
|
518 return token;
|
mas01mj@732
|
519 }
|
mas01mj@732
|
520 else
|
mas01mj@732
|
521 {
|
mas01mj@732
|
522 parseError( "Number " + num + " is not valid!" );
|
mas01mj@732
|
523 }
|
mas01mj@732
|
524
|
mas01mj@732
|
525 return null;
|
mas01mj@732
|
526 }
|
mas01mj@732
|
527
|
mas01mj@732
|
528 /**
|
mas01mj@732
|
529 * Reads the next character in the input
|
mas01mj@732
|
530 * string and advances the character location.
|
mas01mj@732
|
531 *
|
mas01mj@732
|
532 * @return The next character in the input string, or
|
mas01mj@732
|
533 * null if we've read past the end.
|
mas01mj@732
|
534 */
|
mas01mj@732
|
535 private function nextChar():String
|
mas01mj@732
|
536 {
|
mas01mj@732
|
537 return ch = jsonString.charAt( loc++ );
|
mas01mj@732
|
538 }
|
mas01mj@732
|
539
|
mas01mj@732
|
540 /**
|
mas01mj@732
|
541 * Advances the character location past any
|
mas01mj@732
|
542 * sort of white space and comments
|
mas01mj@732
|
543 */
|
mas01mj@732
|
544 private function skipIgnored():void
|
mas01mj@732
|
545 {
|
mas01mj@732
|
546 var originalLoc:int;
|
mas01mj@732
|
547
|
mas01mj@732
|
548 // keep trying to skip whitespace and comments as long
|
mas01mj@732
|
549 // as we keep advancing past the original location
|
mas01mj@732
|
550 do
|
mas01mj@732
|
551 {
|
mas01mj@732
|
552 originalLoc = loc;
|
mas01mj@732
|
553 skipWhite();
|
mas01mj@732
|
554 skipComments();
|
mas01mj@732
|
555 }
|
mas01mj@732
|
556 while ( originalLoc != loc );
|
mas01mj@732
|
557 }
|
mas01mj@732
|
558
|
mas01mj@732
|
559 /**
|
mas01mj@732
|
560 * Skips comments in the input string, either
|
mas01mj@732
|
561 * single-line or multi-line. Advances the character
|
mas01mj@732
|
562 * to the first position after the end of the comment.
|
mas01mj@732
|
563 */
|
mas01mj@732
|
564 private function skipComments():void
|
mas01mj@732
|
565 {
|
mas01mj@732
|
566 if ( ch == '/' )
|
mas01mj@732
|
567 {
|
mas01mj@732
|
568 // Advance past the first / to find out what type of comment
|
mas01mj@732
|
569 nextChar();
|
mas01mj@732
|
570 switch ( ch )
|
mas01mj@732
|
571 {
|
mas01mj@732
|
572 case '/': // single-line comment, read through end of line
|
mas01mj@732
|
573
|
mas01mj@732
|
574 // Loop over the characters until we find
|
mas01mj@732
|
575 // a newline or until there's no more characters left
|
mas01mj@732
|
576 do
|
mas01mj@732
|
577 {
|
mas01mj@732
|
578 nextChar();
|
mas01mj@732
|
579 }
|
mas01mj@732
|
580 while ( ch != '\n' && ch != '' )
|
mas01mj@732
|
581
|
mas01mj@732
|
582 // move past the \n
|
mas01mj@732
|
583 nextChar();
|
mas01mj@732
|
584
|
mas01mj@732
|
585 break;
|
mas01mj@732
|
586
|
mas01mj@732
|
587 case '*': // multi-line comment, read until closing */
|
mas01mj@732
|
588
|
mas01mj@732
|
589 // move past the opening *
|
mas01mj@732
|
590 nextChar();
|
mas01mj@732
|
591
|
mas01mj@732
|
592 // try to find a trailing */
|
mas01mj@732
|
593 while ( true )
|
mas01mj@732
|
594 {
|
mas01mj@732
|
595 if ( ch == '*' )
|
mas01mj@732
|
596 {
|
mas01mj@732
|
597 // check to see if we have a closing /
|
mas01mj@732
|
598 nextChar();
|
mas01mj@732
|
599 if ( ch == '/')
|
mas01mj@732
|
600 {
|
mas01mj@732
|
601 // move past the end of the closing */
|
mas01mj@732
|
602 nextChar();
|
mas01mj@732
|
603 break;
|
mas01mj@732
|
604 }
|
mas01mj@732
|
605 }
|
mas01mj@732
|
606 else
|
mas01mj@732
|
607 {
|
mas01mj@732
|
608 // move along, looking if the next character is a *
|
mas01mj@732
|
609 nextChar();
|
mas01mj@732
|
610 }
|
mas01mj@732
|
611
|
mas01mj@732
|
612 // when we're here we've read past the end of
|
mas01mj@732
|
613 // the string without finding a closing */, so error
|
mas01mj@732
|
614 if ( ch == '' )
|
mas01mj@732
|
615 {
|
mas01mj@732
|
616 parseError( "Multi-line comment not closed" );
|
mas01mj@732
|
617 }
|
mas01mj@732
|
618 }
|
mas01mj@732
|
619
|
mas01mj@732
|
620 break;
|
mas01mj@732
|
621
|
mas01mj@732
|
622 // Can't match a comment after a /, so it's a parsing error
|
mas01mj@732
|
623 default:
|
mas01mj@732
|
624 parseError( "Unexpected " + ch + " encountered (expecting '/' or '*' )" );
|
mas01mj@732
|
625 }
|
mas01mj@732
|
626 }
|
mas01mj@732
|
627
|
mas01mj@732
|
628 }
|
mas01mj@732
|
629
|
mas01mj@732
|
630
|
mas01mj@732
|
631 /**
|
mas01mj@732
|
632 * Skip any whitespace in the input string and advances
|
mas01mj@732
|
633 * the character to the first character after any possible
|
mas01mj@732
|
634 * whitespace.
|
mas01mj@732
|
635 */
|
mas01mj@732
|
636 private function skipWhite():void
|
mas01mj@732
|
637 {
|
mas01mj@732
|
638 // As long as there are spaces in the input
|
mas01mj@732
|
639 // stream, advance the current location pointer
|
mas01mj@732
|
640 // past them
|
mas01mj@732
|
641 while ( isWhiteSpace( ch ) )
|
mas01mj@732
|
642 {
|
mas01mj@732
|
643 nextChar();
|
mas01mj@732
|
644 }
|
mas01mj@732
|
645
|
mas01mj@732
|
646 }
|
mas01mj@732
|
647
|
mas01mj@732
|
648 /**
|
mas01mj@732
|
649 * Determines if a character is whitespace or not.
|
mas01mj@732
|
650 *
|
mas01mj@732
|
651 * @return True if the character passed in is a whitespace
|
mas01mj@732
|
652 * character
|
mas01mj@732
|
653 */
|
mas01mj@732
|
654 private function isWhiteSpace( ch:String ):Boolean
|
mas01mj@732
|
655 {
|
mas01mj@732
|
656 // Check for the whitespace defined in the spec
|
mas01mj@732
|
657 if ( ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' )
|
mas01mj@732
|
658 {
|
mas01mj@732
|
659 return true;
|
mas01mj@732
|
660 }
|
mas01mj@732
|
661 // If we're not in strict mode, we also accept non-breaking space
|
mas01mj@732
|
662 else if ( !strict && ch.charCodeAt( 0 ) == 160 )
|
mas01mj@732
|
663 {
|
mas01mj@732
|
664 return true;
|
mas01mj@732
|
665 }
|
mas01mj@732
|
666
|
mas01mj@732
|
667 return false;
|
mas01mj@732
|
668 }
|
mas01mj@732
|
669
|
mas01mj@732
|
670 /**
|
mas01mj@732
|
671 * Determines if a character is a digit [0-9].
|
mas01mj@732
|
672 *
|
mas01mj@732
|
673 * @return True if the character passed in is a digit
|
mas01mj@732
|
674 */
|
mas01mj@732
|
675 private function isDigit( ch:String ):Boolean
|
mas01mj@732
|
676 {
|
mas01mj@732
|
677 return ( ch >= '0' && ch <= '9' );
|
mas01mj@732
|
678 }
|
mas01mj@732
|
679
|
mas01mj@732
|
680 /**
|
mas01mj@732
|
681 * Determines if a character is a hex digit [0-9A-Fa-f].
|
mas01mj@732
|
682 *
|
mas01mj@732
|
683 * @return True if the character passed in is a hex digit
|
mas01mj@732
|
684 */
|
mas01mj@732
|
685 private function isHexDigit( ch:String ):Boolean
|
mas01mj@732
|
686 {
|
mas01mj@732
|
687 return ( isDigit( ch ) || ( ch >= 'A' && ch <= 'F' ) || ( ch >= 'a' && ch <= 'f' ) );
|
mas01mj@732
|
688 }
|
mas01mj@732
|
689
|
mas01mj@732
|
690 /**
|
mas01mj@732
|
691 * Raises a parsing error with a specified message, tacking
|
mas01mj@732
|
692 * on the error location and the original string.
|
mas01mj@732
|
693 *
|
mas01mj@732
|
694 * @param message The message indicating why the error occurred
|
mas01mj@732
|
695 */
|
mas01mj@732
|
696 public function parseError( message:String ):void
|
mas01mj@732
|
697 {
|
mas01mj@732
|
698 throw new JSONParseError( message, loc, jsonString );
|
mas01mj@732
|
699 }
|
mas01mj@732
|
700 }
|
mas01mj@732
|
701
|
mas01mj@732
|
702 }
|