stringify-info.cjs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  1. 'use strict';
  2. const utils = require('./utils.cjs');
  3. const hasOwn = typeof Object.hasOwn === 'function'
  4. ? Object.hasOwn
  5. : (object, key) => Object.hasOwnProperty.call(object, key);
  6. // https://tc39.es/ecma262/#table-json-single-character-escapes
  7. const escapableCharCodeSubstitution = { // JSON Single Character Escape Sequences
  8. 0x08: '\\b',
  9. 0x09: '\\t',
  10. 0x0a: '\\n',
  11. 0x0c: '\\f',
  12. 0x0d: '\\r',
  13. 0x22: '\\\"',
  14. 0x5c: '\\\\'
  15. };
  16. const charLength2048 = Uint8Array.from({ length: 2048 }, (_, code) => {
  17. if (hasOwn(escapableCharCodeSubstitution, code)) {
  18. return 2; // \X
  19. }
  20. if (code < 0x20) {
  21. return 6; // \uXXXX
  22. }
  23. return code < 128 ? 1 : 2; // UTF8 bytes
  24. });
  25. function isLeadingSurrogate(code) {
  26. return code >= 0xD800 && code <= 0xDBFF;
  27. }
  28. function isTrailingSurrogate(code) {
  29. return code >= 0xDC00 && code <= 0xDFFF;
  30. }
  31. function stringLength(str) {
  32. // Fast path to compute length when a string contains only characters encoded as single bytes
  33. if (!/[^\x20\x21\x23-\x5B\x5D-\x7F]/.test(str)) {
  34. return str.length + 2;
  35. }
  36. let len = 0;
  37. let prevLeadingSurrogate = false;
  38. for (let i = 0; i < str.length; i++) {
  39. const code = str.charCodeAt(i);
  40. if (code < 2048) {
  41. len += charLength2048[code];
  42. } else if (isLeadingSurrogate(code)) {
  43. len += 6; // \uXXXX since no pair with trailing surrogate yet
  44. prevLeadingSurrogate = true;
  45. continue;
  46. } else if (isTrailingSurrogate(code)) {
  47. len = prevLeadingSurrogate
  48. ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
  49. : len + 6; // \uXXXX
  50. } else {
  51. len += 3; // code >= 2048 is 3 bytes length for UTF8
  52. }
  53. prevLeadingSurrogate = false;
  54. }
  55. return len + 2; // +2 for quotes
  56. }
  57. // avoid producing a string from a number
  58. function intLength(num) {
  59. let len = 0;
  60. if (num < 0) {
  61. len = 1;
  62. num = -num;
  63. }
  64. if (num >= 1e9) {
  65. len += 9;
  66. num = (num - num % 1e9) / 1e9;
  67. }
  68. if (num >= 1e4) {
  69. if (num >= 1e6) {
  70. return len + (num >= 1e8
  71. ? 9
  72. : num >= 1e7 ? 8 : 7
  73. );
  74. }
  75. return len + (num >= 1e5 ? 6 : 5);
  76. }
  77. return len + (num >= 1e2
  78. ? num >= 1e3 ? 4 : 3
  79. : num >= 10 ? 2 : 1
  80. );
  81. }
  82. function primitiveLength(value) {
  83. switch (typeof value) {
  84. case 'string':
  85. return stringLength(value);
  86. case 'number':
  87. return Number.isFinite(value)
  88. ? Number.isInteger(value)
  89. ? intLength(value)
  90. : String(value).length
  91. : 4 /* null */;
  92. case 'boolean':
  93. return value ? 4 /* true */ : 5 /* false */;
  94. case 'undefined':
  95. case 'object':
  96. return 4; /* null */
  97. default:
  98. return 0;
  99. }
  100. }
  101. function stringifyInfo(value, ...args) {
  102. const { replacer, getKeys, ...options } = utils.normalizeStringifyOptions(...args);
  103. const continueOnCircular = Boolean(options.continueOnCircular);
  104. const space = options.space?.length || 0;
  105. const keysLength = new Map();
  106. const visited = new Map();
  107. const circular = new Set();
  108. const stack = [];
  109. const root = { '': value };
  110. let stop = false;
  111. let bytes = 0;
  112. let spaceBytes = 0;
  113. let objects = 0;
  114. walk(root, '', value);
  115. // when value is undefined or replaced for undefined
  116. if (bytes === 0) {
  117. bytes += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
  118. }
  119. return {
  120. bytes: isNaN(bytes) ? Infinity : bytes + spaceBytes,
  121. spaceBytes: space > 0 && isNaN(bytes) ? Infinity : spaceBytes,
  122. circular: [...circular]
  123. };
  124. function walk(holder, key, value) {
  125. if (stop) {
  126. return;
  127. }
  128. value = utils.replaceValue(holder, key, value, replacer);
  129. if (value === null || typeof value !== 'object') {
  130. // primitive
  131. if (value !== undefined || Array.isArray(holder)) {
  132. bytes += primitiveLength(value);
  133. }
  134. } else {
  135. // check for circular references
  136. if (stack.includes(value)) {
  137. circular.add(value);
  138. bytes += 4; // treat as null
  139. if (!continueOnCircular) {
  140. stop = true;
  141. }
  142. return;
  143. }
  144. // Using 'visited' allows avoiding hang-ups in cases of highly interconnected object graphs;
  145. // for example, a list of git commits with references to parents can lead to N^2 complexity for traversal,
  146. // and N when 'visited' is used
  147. if (visited.has(value)) {
  148. bytes += visited.get(value);
  149. return;
  150. }
  151. objects++;
  152. const prevObjects = objects;
  153. const valueBytes = bytes;
  154. let valueLength = 0;
  155. stack.push(value);
  156. if (Array.isArray(value)) {
  157. // array
  158. valueLength = value.length;
  159. for (let i = 0; i < valueLength; i++) {
  160. walk(value, i, value[i]);
  161. }
  162. } else {
  163. // object
  164. let prevLength = bytes;
  165. for (const key of getKeys(value)) {
  166. walk(value, key, value[key]);
  167. if (prevLength !== bytes) {
  168. let keyLen = keysLength.get(key);
  169. if (keyLen === undefined) {
  170. keysLength.set(key, keyLen = stringLength(key) + 1); // "key":
  171. }
  172. // value is printed
  173. bytes += keyLen;
  174. valueLength++;
  175. prevLength = bytes;
  176. }
  177. }
  178. }
  179. bytes += valueLength === 0
  180. ? 2 // {} or []
  181. : 1 + valueLength; // {} or [] + commas
  182. if (space > 0 && valueLength > 0) {
  183. spaceBytes +=
  184. // a space between ":" and a value for each object entry
  185. (Array.isArray(value) ? 0 : valueLength) +
  186. // the formula results from folding the following components:
  187. // - for each key-value or element: ident + newline
  188. // (1 + stack.length * space) * valueLength
  189. // - ident (one space less) before "}" or "]" + newline
  190. // (stack.length - 1) * space + 1
  191. (1 + stack.length * space) * (valueLength + 1) - space;
  192. }
  193. stack.pop();
  194. // add to 'visited' only objects that contain nested objects
  195. if (prevObjects !== objects) {
  196. visited.set(value, bytes - valueBytes);
  197. }
  198. }
  199. }
  200. }
  201. exports.stringifyInfo = stringifyInfo;