cycle.js 5.7 KB

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