0
|
1 /*
|
|
2 cycle.js
|
|
3 2012-08-19
|
|
4
|
|
5 Public Domain.
|
|
6
|
|
7 NO WARRANTY EXPRESSED OR IMPLIED. USE AT YOUR OWN RISK.
|
|
8
|
|
9 This code should be minified before deployment.
|
|
10 See http://javascript.crockford.com/jsmin.html
|
|
11
|
|
12 USE YOUR OWN COPY. IT IS EXTREMELY UNWISE TO LOAD CODE FROM SERVERS YOU DO
|
|
13 NOT CONTROL.
|
|
14 */
|
|
15
|
|
16 /*jslint evil: true, regexp: true */
|
|
17
|
|
18 /*members $ref, apply, call, decycle, hasOwnProperty, length, prototype, push,
|
|
19 retrocycle, stringify, test, toString
|
|
20 */
|
|
21
|
|
22 if (typeof JSON.decycle !== 'function') {
|
|
23 JSON.decycle = function decycle(object) {
|
|
24 'use strict';
|
|
25
|
|
26 // Make a deep copy of an object or array, assuring that there is at most
|
|
27 // one instance of each object or array in the resulting structure. The
|
|
28 // duplicate references (which might be forming cycles) are replaced with
|
|
29 // an object of the form
|
|
30 // {$ref: PATH}
|
|
31 // where the PATH is a JSONPath string that locates the first occurance.
|
|
32 // So,
|
|
33 // var a = [];
|
|
34 // a[0] = a;
|
|
35 // return JSON.stringify(JSON.decycle(a));
|
|
36 // produces the string '[{"$ref":"$"}]'.
|
|
37
|
|
38 // JSONPath is used to locate the unique object. $ indicates the top level of
|
|
39 // the object or array. [NUMBER] or [STRING] indicates a child member or
|
|
40 // property.
|
|
41
|
|
42 var objects = [], // Keep a reference to each unique object or array
|
|
43 paths = []; // Keep the path to each unique object or array
|
|
44
|
|
45 return (function derez(value, path) {
|
|
46
|
|
47 // The derez recurses through the object, producing the deep copy.
|
|
48
|
|
49 var i, // The loop counter
|
|
50 name, // Property name
|
|
51 nu; // The new object or array
|
|
52
|
|
53 switch (typeof value) {
|
|
54 case 'object':
|
|
55
|
|
56 // typeof null === 'object', so get out if this value is not really an object.
|
|
57 // Also get out if it is a weird builtin object.
|
|
58
|
|
59 if (value === null ||
|
|
60 value instanceof Boolean ||
|
|
61 value instanceof Date ||
|
|
62 value instanceof Number ||
|
|
63 value instanceof RegExp ||
|
|
64 value instanceof String) {
|
|
65 return value;
|
|
66 }
|
|
67
|
|
68 // If the value is an object or array, look to see if we have already
|
|
69 // encountered it. If so, return a $ref/path object. This is a hard way,
|
|
70 // linear search that will get slower as the number of unique objects grows.
|
|
71
|
|
72 for (i = 0; i < objects.length; i += 1) {
|
|
73 if (objects[i] === value) {
|
|
74 return {$ref: paths[i]};
|
|
75 }
|
|
76 }
|
|
77
|
|
78 // Otherwise, accumulate the unique value and its path.
|
|
79
|
|
80 objects.push(value);
|
|
81 paths.push(path);
|
|
82
|
|
83 // If it is an array, replicate the array.
|
|
84
|
|
85 if (Object.prototype.toString.apply(value) === '[object Array]') {
|
|
86 nu = [];
|
|
87 for (i = 0; i < value.length; i += 1) {
|
|
88 nu[i] = derez(value[i], path + '[' + i + ']');
|
|
89 }
|
|
90 } else {
|
|
91
|
|
92 // If it is an object, replicate the object.
|
|
93
|
|
94 nu = {};
|
|
95 for (name in value) {
|
|
96 if (Object.prototype.hasOwnProperty.call(value, name)) {
|
|
97 nu[name] = derez(value[name],
|
|
98 path + '[' + JSON.stringify(name) + ']');
|
|
99 }
|
|
100 }
|
|
101 }
|
|
102 return nu;
|
|
103 case 'number':
|
|
104 case 'string':
|
|
105 case 'boolean':
|
|
106 return value;
|
|
107 }
|
|
108 }(object, '$'));
|
|
109 };
|
|
110 }
|
|
111
|
|
112
|
|
113 if (typeof JSON.retrocycle !== 'function') {
|
|
114 JSON.retrocycle = function retrocycle($) {
|
|
115 'use strict';
|
|
116
|
|
117 // Restore an object that was reduced by decycle. Members whose values are
|
|
118 // objects of the form
|
|
119 // {$ref: PATH}
|
|
120 // are replaced with references to the value found by the PATH. This will
|
|
121 // restore cycles. The object will be mutated.
|
|
122
|
|
123 // The eval function is used to locate the values described by a PATH. The
|
|
124 // root object is kept in a $ variable. A regular expression is used to
|
|
125 // assure that the PATH is extremely well formed. The regexp contains nested
|
|
126 // * quantifiers. That has been known to have extremely bad performance
|
|
127 // problems on some browsers for very long strings. A PATH is expected to be
|
|
128 // reasonably short. A PATH is allowed to belong to a very restricted subset of
|
|
129 // Goessner's JSONPath.
|
|
130
|
|
131 // So,
|
|
132 // var s = '[{"$ref":"$"}]';
|
|
133 // return JSON.retrocycle(JSON.parse(s));
|
|
134 // produces an array containing a single element which is the array itself.
|
|
135
|
|
136 var px =
|
|
137 /^\$(?:\[(?:\d+|\"(?:[^\\\"\u0000-\u001f]|\\([\\\"\/bfnrt]|u[0-9a-zA-Z]{4}))*\")\])*$/;
|
|
138
|
|
139 (function rez(value) {
|
|
140
|
|
141 // The rez function walks recursively through the object looking for $ref
|
|
142 // properties. When it finds one that has a value that is a path, then it
|
|
143 // replaces the $ref object with a reference to the value that is found by
|
|
144 // the path.
|
|
145
|
|
146 var i, item, name, path;
|
|
147
|
|
148 if (value && typeof value === 'object') {
|
|
149 if (Object.prototype.toString.apply(value) === '[object Array]') {
|
|
150 for (i = 0; i < value.length; i += 1) {
|
|
151 item = value[i];
|
|
152 if (item && typeof item === 'object') {
|
|
153 path = item.$ref;
|
|
154 if (typeof path === 'string' && px.test(path)) {
|
|
155 value[i] = eval(path);
|
|
156 } else {
|
|
157 rez(item);
|
|
158 }
|
|
159 }
|
|
160 }
|
|
161 } else {
|
|
162 for (name in value) {
|
|
163 if (typeof value[name] === 'object') {
|
|
164 item = value[name];
|
|
165 if (item) {
|
|
166 path = item.$ref;
|
|
167 if (typeof path === 'string' && px.test(path)) {
|
|
168 value[name] = eval(path);
|
|
169 } else {
|
|
170 rez(item);
|
|
171 }
|
|
172 }
|
|
173 }
|
|
174 }
|
|
175 }
|
|
176 }
|
|
177 }($));
|
|
178 return $;
|
|
179 };
|
|
180 }
|