cycle.js 6.0 KB

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