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