stringify-info.js 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. const {
  2. normalizeReplacer,
  3. normalizeSpace,
  4. replaceValue,
  5. getTypeNative,
  6. getTypeAsync,
  7. isLeadingSurrogate,
  8. isTrailingSurrogate,
  9. escapableCharCodeSubstitution,
  10. type: {
  11. PRIMITIVE,
  12. OBJECT,
  13. ARRAY,
  14. PROMISE,
  15. STRING_STREAM,
  16. OBJECT_STREAM
  17. }
  18. } = require('./utils');
  19. const charLength2048 = Array.from({ length: 2048 }).map((_, code) => {
  20. if (escapableCharCodeSubstitution.hasOwnProperty(code)) {
  21. return 2; // \X
  22. }
  23. if (code < 0x20) {
  24. return 6; // \uXXXX
  25. }
  26. return code < 128 ? 1 : 2; // UTF8 bytes
  27. });
  28. function stringLength(str) {
  29. let len = 0;
  30. let prevLeadingSurrogate = false;
  31. for (let i = 0; i < str.length; i++) {
  32. const code = str.charCodeAt(i);
  33. if (code < 2048) {
  34. len += charLength2048[code];
  35. } else if (isLeadingSurrogate(code)) {
  36. len += 6; // \uXXXX since no pair with trailing surrogate yet
  37. prevLeadingSurrogate = true;
  38. continue;
  39. } else if (isTrailingSurrogate(code)) {
  40. len = prevLeadingSurrogate
  41. ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
  42. : len + 6; // \uXXXX
  43. } else {
  44. len += 3; // code >= 2048 is 3 bytes length for UTF8
  45. }
  46. prevLeadingSurrogate = false;
  47. }
  48. return len + 2; // +2 for quotes
  49. }
  50. function primitiveLength(value) {
  51. switch (typeof value) {
  52. case 'string':
  53. return stringLength(value);
  54. case 'number':
  55. return Number.isFinite(value) ? String(value).length : 4 /* null */;
  56. case 'boolean':
  57. return value ? 4 /* true */ : 5 /* false */;
  58. case 'undefined':
  59. case 'object':
  60. return 4; /* null */
  61. default:
  62. return 0;
  63. }
  64. }
  65. function spaceLength(space) {
  66. space = normalizeSpace(space);
  67. return typeof space === 'string' ? space.length : 0;
  68. }
  69. module.exports = function jsonStringifyInfo(value, replacer, space, options) {
  70. function walk(holder, key, value) {
  71. if (stop) {
  72. return;
  73. }
  74. value = replaceValue(holder, key, value, replacer);
  75. let type = getType(value);
  76. // check for circular structure
  77. if (type !== PRIMITIVE && stack.has(value)) {
  78. circular.add(value);
  79. length += 4; // treat as null
  80. if (!options.continueOnCircular) {
  81. stop = true;
  82. }
  83. return;
  84. }
  85. switch (type) {
  86. case PRIMITIVE:
  87. if (value !== undefined || Array.isArray(holder)) {
  88. length += primitiveLength(value);
  89. } else if (holder === root) {
  90. length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
  91. }
  92. break;
  93. case OBJECT: {
  94. if (visited.has(value)) {
  95. duplicate.add(value);
  96. length += visited.get(value);
  97. break;
  98. }
  99. const valueLength = length;
  100. let entries = 0;
  101. length += 2; // {}
  102. stack.add(value);
  103. for (const key in value) {
  104. if (hasOwnProperty.call(value, key) && (allowlist === null || allowlist.has(key))) {
  105. const prevLength = length;
  106. walk(value, key, value[key]);
  107. if (prevLength !== length) {
  108. // value is printed
  109. length += stringLength(key) + 1; // "key":
  110. entries++;
  111. }
  112. }
  113. }
  114. if (entries > 1) {
  115. length += entries - 1; // commas
  116. }
  117. stack.delete(value);
  118. if (space > 0 && entries > 0) {
  119. length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space}
  120. length += 1 + stack.size * space; // for }
  121. }
  122. visited.set(value, length - valueLength);
  123. break;
  124. }
  125. case ARRAY: {
  126. if (visited.has(value)) {
  127. duplicate.add(value);
  128. length += visited.get(value);
  129. break;
  130. }
  131. const valueLength = length;
  132. length += 2; // []
  133. stack.add(value);
  134. for (let i = 0; i < value.length; i++) {
  135. walk(value, i, value[i]);
  136. }
  137. if (value.length > 1) {
  138. length += value.length - 1; // commas
  139. }
  140. stack.delete(value);
  141. if (space > 0 && value.length > 0) {
  142. length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space}
  143. length += 1 + stack.size * space; // for ]
  144. }
  145. visited.set(value, length - valueLength);
  146. break;
  147. }
  148. case PROMISE:
  149. case STRING_STREAM:
  150. async.add(value);
  151. break;
  152. case OBJECT_STREAM:
  153. length += 2; // []
  154. async.add(value);
  155. break;
  156. }
  157. }
  158. let allowlist = null;
  159. replacer = normalizeReplacer(replacer);
  160. if (Array.isArray(replacer)) {
  161. allowlist = new Set(replacer);
  162. replacer = null;
  163. }
  164. space = spaceLength(space);
  165. options = options || {};
  166. const visited = new Map();
  167. const stack = new Set();
  168. const duplicate = new Set();
  169. const circular = new Set();
  170. const async = new Set();
  171. const getType = options.async ? getTypeAsync : getTypeNative;
  172. const root = { '': value };
  173. let stop = false;
  174. let length = 0;
  175. walk(root, '', value);
  176. return {
  177. minLength: isNaN(length) ? Infinity : length,
  178. circular: [...circular],
  179. duplicate: [...duplicate],
  180. async: [...async]
  181. };
  182. };