const { normalizeReplacer, normalizeSpace, replaceValue, getTypeNative, getTypeAsync, isLeadingSurrogate, isTrailingSurrogate, escapableCharCodeSubstitution, type: { PRIMITIVE, OBJECT, ARRAY, PROMISE, STRING_STREAM, OBJECT_STREAM } } = require('./utils'); const charLength2048 = Array.from({ length: 2048 }).map((_, code) => { if (escapableCharCodeSubstitution.hasOwnProperty(code)) { return 2; // \X } if (code < 0x20) { return 6; // \uXXXX } return code < 128 ? 1 : 2; // UTF8 bytes }); function stringLength(str) { let len = 0; let prevLeadingSurrogate = false; for (let i = 0; i < str.length; i++) { const code = str.charCodeAt(i); if (code < 2048) { len += charLength2048[code]; } else if (isLeadingSurrogate(code)) { len += 6; // \uXXXX since no pair with trailing surrogate yet prevLeadingSurrogate = true; continue; } else if (isTrailingSurrogate(code)) { len = prevLeadingSurrogate ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes : len + 6; // \uXXXX } else { len += 3; // code >= 2048 is 3 bytes length for UTF8 } prevLeadingSurrogate = false; } return len + 2; // +2 for quotes } function primitiveLength(value) { switch (typeof value) { case 'string': return stringLength(value); case 'number': return Number.isFinite(value) ? String(value).length : 4 /* null */; case 'boolean': return value ? 4 /* true */ : 5 /* false */; case 'undefined': case 'object': return 4; /* null */ default: return 0; } } function spaceLength(space) { space = normalizeSpace(space); return typeof space === 'string' ? space.length : 0; } module.exports = function jsonStringifyInfo(value, replacer, space, options) { function walk(holder, key, value) { if (stop) { return; } value = replaceValue(holder, key, value, replacer); let type = getType(value); // check for circular structure if (type !== PRIMITIVE && stack.has(value)) { circular.add(value); length += 4; // treat as null if (!options.continueOnCircular) { stop = true; } return; } switch (type) { case PRIMITIVE: if (value !== undefined || Array.isArray(holder)) { length += primitiveLength(value); } else if (holder === root) { length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null? } break; case OBJECT: { if (visited.has(value)) { duplicate.add(value); length += visited.get(value); break; } const valueLength = length; let entries = 0; length += 2; // {} stack.add(value); for (const property in value) { if (hasOwnProperty.call(value, property)) { const prevLength = length; walk(value, property, value[property]); if (prevLength !== length) { // value is printed length += stringLength(property) + 1; // "property": entries++; } } } if (entries > 1) { length += entries - 1; // commas } stack.delete(value); if (space > 0 && entries > 0) { length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space} length += 1 + stack.size * space; // for } } visited.set(value, length - valueLength); break; } case ARRAY: { if (visited.has(value)) { duplicate.add(value); length += visited.get(value); break; } const valueLength = length; length += 2; // [] stack.add(value); for (let i = 0; i < value.length; i++) { walk(value, i, value[i]); } if (value.length > 1) { length += value.length - 1; // commas } stack.delete(value); if (space > 0 && value.length > 0) { length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space} length += 1 + stack.size * space; // for ] } visited.set(value, length - valueLength); break; } case PROMISE: case STRING_STREAM: async.add(value); break; case OBJECT_STREAM: length += 2; // [] async.add(value); break; } } replacer = normalizeReplacer(replacer); space = spaceLength(space); options = options || {}; const visited = new Map(); const stack = new Set(); const duplicate = new Set(); const circular = new Set(); const async = new Set(); const getType = options.async ? getTypeAsync : getTypeNative; const root = { '': value }; let stop = false; let length = 0; walk(root, '', value); return { minLength: isNaN(length) ? Infinity : length, circular: [...circular], duplicate: [...duplicate], async: [...async] }; };