1 //------------------------------------------------------------------------------
  2 // File: jamJSON.jsxinc
  3 // Version: 4.5
  4 // Release Date: 2016-09-29
  5 // Copyright: © 2011-2016 Michel MARIANI <http://www.tonton-pixel.com/blog/>
  6 // Licence: GPL <http://www.gnu.org/licenses/gpl.html>
  7 //------------------------------------------------------------------------------
  8 // This program is free software: you can redistribute it and/or modify
  9 // it under the terms of the GNU General Public License as published by
 10 // the Free Software Foundation, either version 3 of the License, or
 11 // (at your option) any later version.
 12 // 
 13 // This program is distributed in the hope that it will be useful,
 14 // but WITHOUT ANY WARRANTY; without even the implied warranty of
 15 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 16 // GNU General Public License for more details.
 17 // 
 18 // You should have received a copy of the GNU General Public License
 19 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
 20 //------------------------------------------------------------------------------
 21 // Version History:
 22 //  4.5:
 23 //  - Incremented version number to keep in sync with other modules.
 24 //  4.4:
 25 //  - Normalized error messages.
 26 //  4.0:
 27 //  - Removed reference to 'this' for main global object.
 28 //  3.6:
 29 //  - Incremented version number to keep in sync with other modules.
 30 //  3.5:
 31 //  - Incremented version number to keep in sync with other modules.
 32 //  3.4:
 33 //  - Incremented version number to keep in sync with other modules.
 34 //  3.3:
 35 //  - Incremented version number to keep in sync with other modules.
 36 //  3.2:
 37 //  - Incremented version number to keep in sync with other modules.
 38 //  3.1:
 39 //  - Incremented version number to keep in sync with other modules.
 40 //  3.0:
 41 //  - Incremented version number to keep in sync with other modules.
 42 //  2.0:
 43 //  - Renamed jamJSON.js to jamJSON.jsxinc.
 44 //  1.0:
 45 //  - Initial release.
 46 //------------------------------------------------------------------------------
 47 
 48 /**
 49  * @fileOverview
 50  * @name jamJSON.jsxinc
 51  * @author Michel MARIANI
 52  */
 53 
 54 if (typeof jamJSON !== 'object')
 55 {
 56     /**
 57      * @description Global object (used to simulate a namespace in JavaScript) containing customized JSON methods
 58      * for translating a JavaScript data structure to and from a JSON text string; can be used in scripts written with
 59      * the <a href="http://www.tonton-pixel.com/blog/json-photoshop-scripting/json-action-manager/">JSON Action Manager</a> engine.
 60      * <ul>
 61      * <li>
 62      * Adapted from: <a href="https://github.com/douglascrockford/JSON-js/blob/master/json_parse_state.js">json_parse_state.js</a>
 63      * and
 64      * <a href="https://github.com/douglascrockford/JSON-js/blob/master/json2.js">json2.js</a>
 65      * by Douglas CrockFord.
 66      * </li>
 67      * <li>
 68      * Spotted and fixed a few problems in the Photoshop implementation of the JavaScript interpreter:
 69      * <ul>
 70      * <li>
 71      * In regular expressions, hexadecimal escape sequences (both \x and \u) must be in capital letters (A-F).
 72      * </li>
 73      * <li>
 74      *  The precedence of nested ternary operators ( ? : ) is handled differently, extra parentheses must be used.
 75      * </li>
 76      * <li>
 77      * The test (state instanceof SyntaxError) returns true even if state is a ReferenceError; in fact, it seems that all error
 78      * types: Error, EvalError,  RangeError, ReferenceError, SyntaxError, TypeError, URIError, are just  synonyms for each other;
 79      * therefore, one must check beforehand if a function is missing in the action table before calling it, so that no
 80      * ReferenceError gets ever thrown...
 81      * </li>
 82      * </ul>
 83      * </li>
 84      * </ul>
 85      * @author Michel MARIANI
 86      * @version 4.5
 87      * @namespace
 88      */
 89     var jamJSON = { };
 90     //
 91     (function ()
 92     {
 93         // The state of the parser, one of
 94         // 'go'             The starting state
 95         // 'ok'             The final, accepting state
 96         // 'firstokey'      Ready for the first key of the object or the closing of an empty object
 97         // 'okey'           Ready for the next key of the object
 98         // 'colon'          Ready for the colon
 99         // 'ovalue'         Ready for the value half of a key/value pair
100         // 'ocomma'         Ready for a comma or closing }
101         // 'firstavalue'    Ready for the first value of an array or an empty array
102         // 'avalue'         Ready for the next value of an array
103         // 'acomma'         Ready for a comma or closing ]
104         var state;
105         var stack;      // The stack, for controlling nesting.
106         var container;  // The current container object or array
107         var key;        // The current key
108         var value;      // The current value
109         // Escapement translation table
110         var escapes =
111         {
112             '\\': '\\',
113             '"': '"',
114             '/': '/',
115             't': '\t',
116             'n': '\n',
117             'r': '\r',
118             'f': '\f',
119             'b': '\b'
120         };
121         // The action table describes the behavior of the machine. It contains an object for each token.
122         // Each object contains a method that is called when a token is matched in a state.
123         // An object will lack a method for illegal states.
124         var action =
125         {
126             '{':
127             {
128                 go: function ()
129                 {
130                     stack.push ({ state: 'ok' });
131                     container = { };
132                     state = 'firstokey';
133                 },
134                 ovalue: function ()
135                 {
136                     stack.push ({ container: container, state: 'ocomma', key: key });
137                     container = { };
138                     state = 'firstokey';
139                 },
140                 firstavalue: function ()
141                 {
142                     stack.push ({ container: container, state: 'acomma' });
143                     container = { };
144                     state = 'firstokey';
145                 },
146                 avalue: function ()
147                 {
148                     stack.push ({ container: container, state: 'acomma' });
149                     container = { };
150                     state = 'firstokey';
151                 }
152             },
153             '}':
154             {
155                 firstokey: function ()
156                 {
157                     var pop = stack.pop ();
158                     value = container;
159                     container = pop.container;
160                     key = pop.key;
161                     state = pop.state;
162                 },
163                 ocomma: function ()
164                 {
165                     var pop = stack.pop ();
166                     container[key] = value;
167                     value = container;
168                     container = pop.container;
169                     key = pop.key;
170                     state = pop.state;
171                 }
172             },
173             '[':
174             {
175                 go: function ()
176                 {
177                     stack.push ({ state: 'ok' });
178                     container = [ ];
179                     state = 'firstavalue';
180                 },
181                 ovalue: function ()
182                 {
183                     stack.push ({ container: container, state: 'ocomma', key: key });
184                     container = [ ];
185                     state = 'firstavalue';
186                 },
187                 firstavalue: function ()
188                 {
189                     stack.push ({ container: container, state: 'acomma' });
190                     container = [ ];
191                     state = 'firstavalue';
192                 },
193                 avalue: function ()
194                 {
195                     stack.push ({ container: container, state: 'acomma' });
196                     container = [ ];
197                     state = 'firstavalue';
198                 }
199             },
200             ']':
201             {
202                 firstavalue: function ()
203                 {
204                     var pop = stack.pop ();
205                     value = container;
206                     container = pop.container;
207                     key = pop.key;
208                     state = pop.state;
209                 },
210                 acomma: function ()
211                 {
212                     var pop = stack.pop ();
213                     container.push (value);
214                     value = container;
215                     container = pop.container;
216                     key = pop.key;
217                     state = pop.state;
218                 }
219             },
220             ':':
221             {
222                 colon: function ()
223                 {
224                     if (container.hasOwnProperty (key))
225                     {
226                         throw new SyntaxError ("[jamJSON.parse] Duplicate key: “" + key + "”");
227                     }
228                     state = 'ovalue';
229                 }
230             },
231             ',':
232             {
233                 ocomma: function ()
234                 {
235                     container[key] = value;
236                     state = 'okey';
237                 },
238                 acomma: function ()
239                 {
240                     container.push (value);
241                     state = 'avalue';
242                 }
243             },
244             'true':
245             {
246                 go: function ()
247                 {
248                     value = true;
249                     state = 'ok';
250                 },
251                 ovalue: function ()
252                 {
253                     value = true;
254                     state = 'ocomma';
255                 },
256                 firstavalue: function ()
257                 {
258                     value = true;
259                     state = 'acomma';
260                 },
261                 avalue: function ()
262                 {
263                     value = true;
264                     state = 'acomma';
265                 }
266             },
267             'false':
268             {
269                 go: function ()
270                 {
271                     value = false;
272                     state = 'ok';
273                 },
274                 ovalue: function ()
275                 {
276                     value = false;
277                     state = 'ocomma';
278                 },
279                 firstavalue: function ()
280                 {
281                     value = false;
282                     state = 'acomma';
283                 },
284                 avalue: function ()
285                 {
286                     value = false;
287                     state = 'acomma';
288                 }
289             },
290             'null':
291             {
292                 go: function ()
293                 {
294                     value = null;
295                     state = 'ok';
296                 },
297                 ovalue: function ()
298                 {
299                     value = null;
300                     state = 'ocomma';
301                 },
302                 firstavalue: function ()
303                 {
304                     value = null;
305                     state = 'acomma';
306                 },
307                 avalue: function ()
308                 {
309                     value = null;
310                     state = 'acomma';
311                 }
312             }
313         };
314         // The actions for number tokens
315         var number =
316         {
317             go: function ()
318             {
319                 state = 'ok';
320             },
321             ovalue: function ()
322             {
323                 state = 'ocomma';
324             },
325             firstavalue: function ()
326             {
327                 state = 'acomma';
328             },
329             avalue: function ()
330             {
331                 state = 'acomma';
332             }
333         };
334         // The actions for string tokens
335         var string =
336         {
337             go: function ()
338             {
339                 state = 'ok';
340             },
341             firstokey: function ()
342             {
343                 key = value;
344                 state = 'colon';
345             },
346             okey: function ()
347             {
348                 key = value;
349                 state = 'colon';
350             },
351             ovalue: function ()
352             {
353                 state = 'ocomma';
354             },
355             firstavalue: function ()
356             {
357                 state = 'acomma';
358             },
359             avalue: function ()
360             {
361                 state = 'acomma';
362             }
363         };
364         //
365         var commentFunc = function () { };  // No state change
366         //
367         function debackslashify (text)
368         {
369             // Remove and replace any backslash escapement.
370             return text.replace (/\\(?:u(.{4})|([^u]))/g, function (a, b, c) { return (b) ? String.fromCharCode (parseInt (b, 16)) : escapes[c]; });
371         }
372         //
373         /**
374          * @description Convert a JSON text string into a JavaScript data structure.<br />
375          * <ul>
376          * <li>
377          * Adapted from <a href="https://github.com/douglascrockford/JSON-js/blob/master/json_parse_state.js">json_parse_state.js</a> by Douglas Crockford:
378          * <ul>
379          * <li>
380          * Removed the reviver parameter.
381          * </li>
382          * <li>
383          * Added an extra validate parameter; if false (or undefined), a simple eval is performed.
384          * </li>
385          * <li>
386          * Added an extra allowComments parameter; if false (or undefined), validation does not allow comments (strict JSON).
387          * </li>
388          * <li>
389          * Corrected the regular expression for numbers to conform to the JSON grammar;
390          * cf. <a href="http://www.json.org/">Introducing JSON</a> and <a href="http://www.ietf.org/rfc/rfc4627.txt">RFC 4627</a>.
391          * </li>
392          * </ul>
393          * </li>
394          * </ul>
395          * @param {String} text JSON text string
396          * @param {Boolean} [validate] validate JSON syntax while parsing
397          * @param {Boolean} [allowComments] validate comments too
398          * @returns {Object|Array|String|Number|Boolean|Null} JavaScript data structure (usually an object or array)
399          * @see jamJSON.stringify
400          * @example
401          * var jsonText = '{ "Last Name": "Einstein", "First Name": "Albert" }';
402          * try
403          * {
404          *     var jsObj = jamJSON.<strong>parse</strong> (jsonText, true);
405          *     alert (jsObj["First Name"] + " " + jsObj["Last Name"]);    // -> Albert Einstein
406          * }
407          * catch (e)
408          * {
409          *     alert ("E≠mc2!");
410          * }
411          */
412         jamJSON.parse = function (text, validate, allowComments)
413         {
414             if (validate)
415             {
416                 // Use a state machine rather than the dangerous eval function to parse a JSON text.
417                 // A regular expression is used to extract tokens from the JSON text.
418                 var tx = /^[\x20\t\n\r]*(?:([,:\[\]{}]|true|false|null)|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?)|"((?:[^\r\n\t\\\"]|\\(?:["\\\/trnfb]|u[0-9a-fA-F]{4}))*)")/;
419                 var txc = /^[\x20\t\n\r]*(?:(\/(?:\/.*|\*(?:.|[\r\n])*?\*\/))|([,:\[\]{}]|true|false|null)|(-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+\-]?[0-9]+)?)|"((?:[^\r\n\t\\\"]|\\(?:["\\\/trnfb]|u[0-9a-fA-F]{4}))*)")/;
420                 // The extraction process is cautious.
421                 var r;          // The result of the exec method.
422                 var i;          // The index shift in result array
423                 var actionFunc; // The current action function
424                 // Set the starting state.
425                 state = 'go';
426                 // The stack records the container, key, and state for each object or array
427                 // that contains another object or array while processing nested structures.
428                 stack = [ ];
429                 // If any error occurs, we will catch it and ultimately throw a syntax error.
430                 try
431                 {
432                     // For each token...
433                     while (true)
434                     {
435                         i = (allowComments) ? 1 : 0;
436                         r = (allowComments) ? txc.exec (text) : tx.exec (text);
437                         if (!r)
438                         {
439                             break;
440                         }
441                         // r is the result array from matching the tokenizing regular expression.
442                         //  r[0] contains everything that matched, including any initial whitespace.
443                         //  r[1] contains any punctuation that was matched, or true, false, or null.
444                         //  r[2] contains a matched number, still in string form.
445                         //  r[3] contains a matched string, without quotes but with escapement.
446                         if (allowComments && r[1])
447                         {
448                             // Comment: just do nothing...
449                             actionFunc = commentFunc;
450                         }
451                         else if (r[i + 1])
452                         {
453                             // Token: execute the action for this state and token.
454                             actionFunc = action[r[i + 1]][state];
455                         }
456                         else if (r[i + 2])
457                         {
458                             // Number token: convert the number string into a number value and execute
459                             // the action for this state and number.
460                             value = +r[i + 2];
461                             actionFunc = number[state];
462                         }
463                         else    // Do not test r[i + 3] explicitely since a string can be empty
464                         {
465                             // String token: replace the escapement sequences and execute the action for
466                             // this state and string.
467                             value = debackslashify (r[i + 3]);
468                             actionFunc = string[state];
469                         }
470                         //
471                         if (actionFunc)
472                         {
473                             actionFunc ();
474                             // Remove the token from the string. The loop will continue as long as there
475                             // are tokens. This is a slow process, but it allows the use of ^ matching,
476                             // which assures that no illegal tokens slip through.
477                             text = text.slice (r[0].length);
478                         }
479                         else
480                         {
481                             break;
482                         }
483                     }
484                 }
485                 catch (e)
486                 {
487                     // If we find a state/token combination that is illegal, then the action will
488                     // cause an error. We handle the error by simply changing the state.
489                     state = e;
490                 }
491                 // The parsing is finished. If we are not in the final 'ok' state, or if the
492                 // remaining source text contains anything except whitespace, then we did not have
493                 // a well-formed JSON text.
494                 if (state !== 'ok' || /[^\x20\t\n\r]/.test (text))
495                 {
496                     throw state instanceof SyntaxError ? state : new SyntaxError ("[jamJSON.parse] Invalid JSON");
497                 }
498                 return value;
499             }
500             else
501             {
502                 // Let's live dangerously (but so fast)! ;-)
503                 return eval ('(' + text + ')');
504             }
505         };
506         //
507         var escapable = /[\\\"\x00-\x1F\x7F-\x9F\u00AD\u0600-\u0604\u070F\u17B4\u17B5\u200C-\u200F\u2028-\u202F\u2060-\u206F\uFEFF\uFFF0-\uFFFF]/g;
508         var meta =  // table of character substitutions
509         {
510             '\b': '\\b',
511             '\t': '\\t',
512             '\n': '\\n',
513             '\f': '\\f',
514             '\r': '\\r',
515             '"' : '\\"',
516             '\\': '\\\\'
517         };
518         var gap;
519         var indent;
520         var prefixIndent;
521         //
522         function quote (string)
523         {
524             // If the string contains no control characters, no quote characters, and no
525             // backslash characters, then we can safely slap some quotes around it.
526             // Otherwise we must also replace the offending characters with safe escape
527             // sequences.
528             escapable.lastIndex = 0;
529             return escapable.test (string) ?
530                 '"' + string.replace (escapable, function (a) {
531                     var c = meta[a];
532                     return (typeof c === 'string') ? c : '\\u' + ('0000' + a.charCodeAt (0).toString (16).toUpperCase ()).slice (-4);
533                 }) + '"' : '"' + string + '"';
534         }
535         //
536         function str (value)    // Produce a string from value.
537         {
538             var i;  // The loop counter.
539             var k;  // The member key.
540             var v;  // The member value.
541             var mind = gap;
542             var partial;
543             // What happens next depends on the value's type.
544             switch (typeof value)
545             {
546                 case 'string':
547                     return quote (value);
548                 case 'number':
549                     // JSON numbers must be finite. Encode non-finite numbers as null.
550                     return isFinite (value) ? String (value) : 'null';
551                 case 'boolean':
552                 case 'null':
553                     // If the value is a boolean or null, convert it to a string. Note:
554                     // typeof null does not produce 'null'. The case is included here in
555                     // the remote chance that this gets fixed someday.
556                     return String (value);
557                 case 'object':  // If the type is 'object', we might be dealing with an object or an array or null.
558                     // Due to a specification blunder in ECMAScript, typeof null is 'object',
559                     // so watch out for that case.
560                     if (!value)
561                     {
562                         return 'null';
563                     }
564                     // Make an array to hold the partial results of stringifying this object value.
565                     gap += indent;
566                     partial = [ ];
567                     // Is the value an array?
568                     if (value.constructor === Array)
569                     {
570                         // The value is an array. Stringify every element.
571                         for (i = 0; i < value.length; i++)
572                         {
573                             partial[i] = str (value[i]);
574                         }
575                         // Join all of the elements together, separated with commas, and wrap them in brackets.
576                         v = (partial.length === 0) ?
577                             (gap ? '[\n' + prefixIndent + mind + ']' : '[ ]') :
578                             (gap ? '[\n' + prefixIndent + gap + partial.join (',\n' + prefixIndent + gap) + '\n' + prefixIndent + mind + ']' : '[ ' + partial.join (', ') + ' ]');
579                         gap = mind;
580                         return v;
581                     }
582                     else
583                     {
584                         // Iterate through all of the keys in the object.
585                         for (k in value)
586                         {
587                             if (value.hasOwnProperty (k))
588                             {
589                                 v = str (value[k]);
590                                 if (v)  // Useless ?
591                                 {
592                                     partial.push (quote (k) + (gap && ((v.charAt (0) === '{') || (v.charAt (0) === '[')) ? ':\n' + prefixIndent + gap : ': ') + v);
593                                 }
594                             }
595                         }
596                         // Join all of the member texts together, separated with commas, and wrap them in braces.
597                         v = (partial.length === 0) ?
598                             (gap ? '{\n' + prefixIndent + mind + '}' : '{ }') :
599                             (gap ? '{\n' + prefixIndent + gap + partial.join (',\n' + prefixIndent + gap) + '\n' + prefixIndent + mind + '}' : '{ ' + partial.join (', ') + ' }');
600                         gap = mind;
601                         return v;
602                     }
603                 default:
604                     throw new SyntaxError ("[jamJSON.stringify] Invalid JSON");
605             }
606         }
607         //
608         /**
609          * @description Convert a JavaScript data structure into a JSON text string.<br />
610          * <ul>
611          * <li>
612          * Adapted from <a href="https://github.com/douglascrockford/JSON-js/blob/master/json2.js">json2.js</a> by Douglas Crockford:
613          * <ul>
614          * <li>
615          * Removed the replacer parameter.
616          * </li>
617          * <li>
618          * No handling of toJSON methods whatsoever.
619          * </li>
620          * <li>
621          * Added an extra prefix parameter to allow the insertion of the resulting text into already-indented code.
622          * </li>
623          * <li>
624          * Improved indenting so that pairs of brackets { } and [ ] are always aligned on the same vertical position.
625          * </li>
626          * <li>
627          * Single spaces are systematically inserted for better readability when indenting is off.
628          * </li>
629          * <li>
630          * A syntax error is thrown for any invalid JSON element (undefined, function, etc.).
631          * </li>
632          * </ul>
633          * </li>
634          * </ul>
635          * @param {Object|Array|String|Number|Boolean|Null} value JavaScript data structure (usually an object or array)
636          * @param {String|Number} [space] Indent space string (e.g. "\t") or number of spaces
637          * @param {String|Number} [prefix] Prefix space string (e.g. "\t") or number of spaces
638          * @returns {String} JSON text string
639          * @see jamJSON.parse
640          * @example
641          * var dummy = null;
642          * var jsArr =
643          * [
644          *     3.14E0,
645          *     'Hello ' + 'JSON!',
646          *     { on: (0 === 0) },
647          *     [ 1 + 1, dummy ]
648          * ];
649          * alert (jamJSON.<strong>stringify</strong> (jsArr));  // -> [ 3.14, "Hello JSON!", { "on": true }, [ 2, null ] ]
650          */
651         jamJSON.stringify = function (value, space, prefix)
652         {
653             // The stringify method takes a value, two optional parameters: space and prefix, and returns a JSON text.
654             // Use of the space parameter can produce text that is more easily readable.
655             // Use of the prefix parameter allows the insertion of the resulting text into some existing code already indented.
656             var i;
657             gap = '';
658             indent = '';
659             prefixIndent = '';
660             //
661             // If the space parameter is a number, make an indent string containing that many spaces.
662             if (typeof space === 'number')
663             {
664                 for (i = 0; i < space; i++)
665                 {
666                     indent += ' ';
667                 }
668             }
669             else if (typeof space === 'string') // If the space parameter is a string, it will be used as the indent string.
670             {
671                 indent = space;
672             }
673             // If the prefix parameter is a number, make a prefix indent string containing that many spaces.
674             if (typeof prefix === 'number')
675             {
676                 for (i = 0; i < prefix; i++)
677                 {
678                     prefixIndent += ' ';
679                 }
680             }
681             else if (typeof prefix === 'string')    // If the prefix parameter is a string, it will be used as the prefix indent string.
682             {
683                 prefixIndent = prefix;
684             }
685             // Return the result of stringifying the value.
686             return prefixIndent + str (value);
687         };
688     } ());
689 }
690 
691 //------------------------------------------------------------------------------
692 
693