util: group array elements together
When using `util.inspect()` with `compact` mode set to a number, all array entries exceeding 6 are going to be grouped together into logical parts. PR-URL: https://github.com/nodejs/node/pull/26269 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net>
This commit is contained in:
parent
4db10ed9ad
commit
8bb30926fd
@ -468,9 +468,9 @@ changes:
|
||||
to be displayed on a new line. It will also add new lines to text that is
|
||||
longer than `breakLength`. If set to a number, the most `n` inner elements
|
||||
are united on a single line as long as all properties fit into
|
||||
`breakLength`. Note that no text will be reduced below 16 characters, no
|
||||
matter the `breakLength` size. For more information, see the example below.
|
||||
**Default:** `true`.
|
||||
`breakLength`. Short array elements are also grouped together. Note that no
|
||||
text will be reduced below 16 characters, no matter the `breakLength` size.
|
||||
For more information, see the example below. **Default:** `true`.
|
||||
* `sorted` {boolean|Function} If set to `true` or a function, all properties
|
||||
of an object, and `Set` and `Map` entries are sorted in the resulting
|
||||
string. If set to `true` the [default sort][] is used. If set to a function,
|
||||
|
@ -794,8 +794,35 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
|
||||
}
|
||||
}
|
||||
|
||||
const combine = typeof ctx.compact === 'number' &&
|
||||
ctx.currentDepth - recurseTimes < ctx.compact;
|
||||
let combine = false;
|
||||
if (typeof ctx.compact === 'number') {
|
||||
// Memorize the original output length. In case the the output is grouped,
|
||||
// prevent lining up the entries on a single line.
|
||||
const entries = output.length;
|
||||
// Group array elements together if the array contains at least six separate
|
||||
// entries.
|
||||
if (extrasType === kArrayExtrasType && output.length > 6) {
|
||||
output = groupArrayElements(ctx, output);
|
||||
}
|
||||
// `ctx.currentDepth` is set to the most inner depth of the currently
|
||||
// inspected object part while `recurseTimes` is the actual current depth
|
||||
// that is inspected.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// const a = { first: [ 1, 2, 3 ], second: { inner: [ 1, 2, 3 ] } }
|
||||
//
|
||||
// The deepest depth of `a` is 2 (a.second.inner) and `a.first` has a max
|
||||
// depth of 1.
|
||||
//
|
||||
// Consolidate all entries of the local most inner depth up to
|
||||
// `ctx.compact`, as long as the properties are smaller than
|
||||
// `ctx.breakLength`.
|
||||
if (ctx.currentDepth - recurseTimes < ctx.compact &&
|
||||
entries === output.length) {
|
||||
combine = true;
|
||||
}
|
||||
}
|
||||
|
||||
const res = reduceToSingleString(ctx, output, base, braces, combine);
|
||||
const budget = ctx.budget[ctx.indentationLvl] || 0;
|
||||
@ -814,6 +841,83 @@ function formatRaw(ctx, value, recurseTimes, typedArray) {
|
||||
return res;
|
||||
}
|
||||
|
||||
function groupArrayElements(ctx, output) {
|
||||
let totalLength = 0;
|
||||
let maxLength = 0;
|
||||
let i = 0;
|
||||
const dataLen = new Array(output.length);
|
||||
// Calculate the total length of all output entries and the individual max
|
||||
// entries length of all output entries. We have to remove colors first,
|
||||
// otherwise the length would not be calculated properly.
|
||||
for (; i < output.length; i++) {
|
||||
const len = ctx.colors ? removeColors(output[i]).length : output[i].length;
|
||||
dataLen[i] = len;
|
||||
totalLength += len;
|
||||
if (maxLength < len)
|
||||
maxLength = len;
|
||||
}
|
||||
// Add two to `maxLength` as we add a single whitespace character plus a comma
|
||||
// in-between two entries.
|
||||
const actualMax = maxLength + 2;
|
||||
// Check if at least three entries fit next to each other and prevent grouping
|
||||
// of arrays that contains entries of very different length (i.e., if a single
|
||||
// entry is longer than 1/5 of all other entries combined). Otherwise the
|
||||
// space in-between small entries would be enormous.
|
||||
if (actualMax * 3 + ctx.indentationLvl < ctx.breakLength &&
|
||||
(totalLength / maxLength > 5 || maxLength <= 6)) {
|
||||
|
||||
const approxCharHeights = 2.5;
|
||||
const bias = 1;
|
||||
// Dynamically check how many columns seem possible.
|
||||
const columns = Math.min(
|
||||
// Ideally a square should be drawn. We expect a character to be about 2.5
|
||||
// times as high as wide. This is the area formula to calculate a square
|
||||
// which contains n rectangles of size `actualMax * approxCharHeights`.
|
||||
// Divide that by `actualMax` to receive the correct number of columns.
|
||||
// The added bias slightly increases the columns for short entries.
|
||||
Math.round(
|
||||
Math.sqrt(
|
||||
approxCharHeights * (actualMax - bias) * output.length
|
||||
) / (actualMax - bias)
|
||||
),
|
||||
// Limit array grouping for small `compact` modes as the user requested
|
||||
// minimal grouping.
|
||||
ctx.compact * 3,
|
||||
// Limit the columns to a maximum of ten.
|
||||
10
|
||||
);
|
||||
// Return with the original output if no grouping should happen.
|
||||
if (columns <= 1) {
|
||||
return output;
|
||||
}
|
||||
// Calculate the maximum length of all entries that are visible in the first
|
||||
// column of the group.
|
||||
const tmp = [];
|
||||
let firstLineMaxLength = dataLen[0];
|
||||
for (i = columns; i < dataLen.length; i += columns) {
|
||||
if (dataLen[i] > firstLineMaxLength)
|
||||
firstLineMaxLength = dataLen[i];
|
||||
}
|
||||
// Each iteration creates a single line of grouped entries.
|
||||
for (i = 0; i < output.length; i += columns) {
|
||||
// Calculate extra color padding in case it's active. This has to be done
|
||||
// line by line as some lines might contain more colors than others.
|
||||
let colorPadding = output[i].length - dataLen[i];
|
||||
// Add padding to the first column of the output.
|
||||
let str = output[i].padStart(firstLineMaxLength + colorPadding, ' ');
|
||||
// The last lines may contain less entries than columns.
|
||||
const max = Math.min(i + columns, output.length);
|
||||
for (var j = i + 1; j < max; j++) {
|
||||
colorPadding = output[j].length - dataLen[j];
|
||||
str += `, ${output[j].padStart(maxLength + colorPadding, ' ')}`;
|
||||
}
|
||||
tmp.push(str);
|
||||
}
|
||||
output = tmp;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
function handleMaxCallStackSize(ctx, err, constructor, tag, indentationLvl) {
|
||||
if (isStackOverflowError(err)) {
|
||||
ctx.seen.pop();
|
||||
@ -1205,50 +1309,58 @@ function formatProperty(ctx, value, recurseTimes, key, type) {
|
||||
return `${name}:${extra}${str}`;
|
||||
}
|
||||
|
||||
function isBelowBreakLength(ctx, output, start) {
|
||||
// Each entry is separated by at least a comma. Thus, we start with a total
|
||||
// length of at least `output.length`. In addition, some cases have a
|
||||
// whitespace in-between each other that is added to the total as well.
|
||||
let totalLength = output.length + start;
|
||||
if (totalLength + output.length > ctx.breakLength)
|
||||
return false;
|
||||
for (var i = 0; i < output.length; i++) {
|
||||
if (ctx.colors) {
|
||||
totalLength += removeColors(output[i]).length;
|
||||
} else {
|
||||
totalLength += output[i].length;
|
||||
}
|
||||
if (totalLength > ctx.breakLength) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function reduceToSingleString(ctx, output, base, braces, combine = false) {
|
||||
const breakLength = ctx.breakLength;
|
||||
let i = 0;
|
||||
if (ctx.compact !== true) {
|
||||
if (combine) {
|
||||
const totalLength = output.reduce((sum, cur) => sum + cur.length, 0);
|
||||
if (totalLength + output.length * 2 < breakLength) {
|
||||
let res = `${base ? `${base} ` : ''}${braces[0]} `;
|
||||
for (; i < output.length - 1; i++) {
|
||||
res += `${output[i]}, `;
|
||||
}
|
||||
res += `${output[i]} ${braces[1]}`;
|
||||
return res;
|
||||
// Line up all entries on a single line in case the entries do not exceed
|
||||
// `breakLength`. Add 10 as constant to start next to all other factors
|
||||
// that may reduce `breakLength`.
|
||||
const start = output.length + ctx.indentationLvl +
|
||||
braces[0].length + base.length + 10;
|
||||
if (isBelowBreakLength(ctx, output, start)) {
|
||||
return `${base ? `${base} ` : ''}${braces[0]} ${join(output, ', ')} ` +
|
||||
braces[1];
|
||||
}
|
||||
}
|
||||
// Line up each entry on an individual line.
|
||||
const indentation = `\n${' '.repeat(ctx.indentationLvl)}`;
|
||||
let res = `${base ? `${base} ` : ''}${braces[0]}${indentation} `;
|
||||
for (; i < output.length - 1; i++) {
|
||||
res += `${output[i]},${indentation} `;
|
||||
}
|
||||
res += `${output[i]}${indentation}${braces[1]}`;
|
||||
return res;
|
||||
return `${base ? `${base} ` : ''}${braces[0]}${indentation} ` +
|
||||
`${join(output, `,${indentation} `)}${indentation}${braces[1]}`;
|
||||
}
|
||||
if (output.length * 2 <= breakLength) {
|
||||
let length = 0;
|
||||
for (; i < output.length && length <= breakLength; i++) {
|
||||
if (ctx.colors) {
|
||||
length += removeColors(output[i]).length + 1;
|
||||
} else {
|
||||
length += output[i].length + 1;
|
||||
}
|
||||
}
|
||||
if (length <= breakLength)
|
||||
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
|
||||
braces[1];
|
||||
// Line up all entries on a single line in case the entries do not exceed
|
||||
// `breakLength`.
|
||||
if (isBelowBreakLength(ctx, output, 0)) {
|
||||
return `${braces[0]}${base ? ` ${base}` : ''} ${join(output, ', ')} ` +
|
||||
braces[1];
|
||||
}
|
||||
const indentation = ' '.repeat(ctx.indentationLvl);
|
||||
// If the opening "brace" is too large, like in the case of "Set {",
|
||||
// we need to force the first item to be on the next line or the
|
||||
// items will not line up correctly.
|
||||
const indentation = ' '.repeat(ctx.indentationLvl);
|
||||
const ln = base === '' && braces[0].length === 1 ?
|
||||
' ' : `${base ? ` ${base}` : ''}\n${indentation} `;
|
||||
const str = join(output, `,\n${indentation} `);
|
||||
return `${braces[0]}${ln}${str} ${braces[1]}`;
|
||||
// Line up each entry on an individual line.
|
||||
return `${braces[0]}${ln}${join(output, `,\n${indentation} `)} ${braces[1]}`;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
@ -1966,3 +1966,196 @@ assert.strictEqual(
|
||||
'{ foo: [Getter/Setter] Set { [ [Object], 2, {} ], ' +
|
||||
"'foobar', { x: 1 } },\n inc: [Getter: NaN] }");
|
||||
}
|
||||
|
||||
// Check compact number mode.
|
||||
{
|
||||
let obj = {
|
||||
a: {
|
||||
b: {
|
||||
x: 5,
|
||||
c: {
|
||||
x: '10000000000000000 00000000000000000 '.repeat(1e1),
|
||||
d: 2,
|
||||
e: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
b: [
|
||||
1,
|
||||
2,
|
||||
[ 1, 2, { a: 1, b: 2, c: 3 } ]
|
||||
],
|
||||
c: ['foo', 4, 444444],
|
||||
d: Array.from({ length: 100 }).map((e, i) => {
|
||||
return i % 2 === 0 ? i * i : i;
|
||||
}),
|
||||
e: Array(6).fill('foobar'),
|
||||
f: Array(9).fill('foobar'),
|
||||
g: Array(21).fill('foobar baz'),
|
||||
h: [100].concat(Array.from({ length: 9 }).map((e, n) => (n))),
|
||||
long: Array(9).fill('This text is too long for grouping!')
|
||||
};
|
||||
|
||||
let out = util.inspect(obj, { compact: 3, depth: 10 });
|
||||
|
||||
let expected = [
|
||||
'{',
|
||||
' a: {',
|
||||
' b: {',
|
||||
' x: 5,',
|
||||
' c: {',
|
||||
" x: '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ' +",
|
||||
" '10000000000000000 00000000000000000 ',",
|
||||
' d: 2,',
|
||||
' e: 3',
|
||||
' }',
|
||||
' }',
|
||||
' },',
|
||||
' b: [ 1, 2, [ 1, 2, { a: 1, b: 2, c: 3 } ] ],',
|
||||
" c: [ 'foo', 4, 444444 ],",
|
||||
' d: [',
|
||||
' 0, 1, 4, 3, 16, 5, 36,',
|
||||
' 7, 64, 9, 100, 11, 144, 13,',
|
||||
' 196, 15, 256, 17, 324, 19, 400,',
|
||||
' 21, 484, 23, 576, 25, 676, 27,',
|
||||
' 784, 29, 900, 31, 1024, 33, 1156,',
|
||||
' 35, 1296, 37, 1444, 39, 1600, 41,',
|
||||
' 1764, 43, 1936, 45, 2116, 47, 2304,',
|
||||
' 49, 2500, 51, 2704, 53, 2916, 55,',
|
||||
' 3136, 57, 3364, 59, 3600, 61, 3844,',
|
||||
' 63, 4096, 65, 4356, 67, 4624, 69,',
|
||||
' 4900, 71, 5184, 73, 5476, 75, 5776,',
|
||||
' 77, 6084, 79, 6400, 81, 6724, 83,',
|
||||
' 7056, 85, 7396, 87, 7744, 89, 8100,',
|
||||
' 91, 8464, 93, 8836, 95, 9216, 97,',
|
||||
' 9604, 99',
|
||||
' ],',
|
||||
' e: [',
|
||||
" 'foobar',",
|
||||
" 'foobar',",
|
||||
" 'foobar',",
|
||||
" 'foobar',",
|
||||
" 'foobar',",
|
||||
" 'foobar'",
|
||||
' ],',
|
||||
' f: [',
|
||||
" 'foobar', 'foobar',",
|
||||
" 'foobar', 'foobar',",
|
||||
" 'foobar', 'foobar',",
|
||||
" 'foobar', 'foobar',",
|
||||
" 'foobar'",
|
||||
' ],',
|
||||
' g: [',
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz', 'foobar baz',",
|
||||
" 'foobar baz'",
|
||||
' ],',
|
||||
' h: [',
|
||||
' 100, 0, 1,',
|
||||
' 2, 3, 4,',
|
||||
' 5, 6, 7,',
|
||||
' 8',
|
||||
' ],',
|
||||
' long: [',
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!',",
|
||||
" 'This text is too long for grouping!'",
|
||||
' ]',
|
||||
'}'
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(out, expected);
|
||||
|
||||
// Verify that array grouping and line consolidation does not happen together.
|
||||
obj = {
|
||||
a: {
|
||||
b: {
|
||||
x: 5,
|
||||
c: {
|
||||
d: 2,
|
||||
e: 3
|
||||
}
|
||||
}
|
||||
},
|
||||
b: Array.from({ length: 9 }).map((e, n) => {
|
||||
return n % 2 === 0 ? 'foobar' : 'baz';
|
||||
})
|
||||
};
|
||||
|
||||
out = util.inspect(obj, { compact: 1, breakLength: Infinity, colors: true });
|
||||
|
||||
expected = [
|
||||
'{',
|
||||
' a: {',
|
||||
' b: { x: \u001b[33m5\u001b[39m, c: \u001b[36m[Object]\u001b[39m }',
|
||||
' },',
|
||||
' b: [',
|
||||
" \u001b[32m'foobar'\u001b[39m, \u001b[32m'baz'\u001b[39m,",
|
||||
" \u001b[32m'foobar'\u001b[39m, \u001b[32m'baz'\u001b[39m,",
|
||||
" \u001b[32m'foobar'\u001b[39m, \u001b[32m'baz'\u001b[39m,",
|
||||
" \u001b[32m'foobar'\u001b[39m, \u001b[32m'baz'\u001b[39m,",
|
||||
" \u001b[32m'foobar'\u001b[39m",
|
||||
' ]',
|
||||
'}',
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(out, expected);
|
||||
|
||||
obj = Array.from({ length: 60 }).map((e, i) => i);
|
||||
out = util.inspect(obj, { compact: 1, breakLength: Infinity, colors: true });
|
||||
|
||||
expected = [
|
||||
'[',
|
||||
' \u001b[33m0\u001b[39m, \u001b[33m1\u001b[39m, \u001b[33m2\u001b[39m,',
|
||||
' \u001b[33m3\u001b[39m, \u001b[33m4\u001b[39m, \u001b[33m5\u001b[39m,',
|
||||
' \u001b[33m6\u001b[39m, \u001b[33m7\u001b[39m, \u001b[33m8\u001b[39m,',
|
||||
' \u001b[33m9\u001b[39m, \u001b[33m10\u001b[39m, \u001b[33m11\u001b[39m,',
|
||||
' \u001b[33m12\u001b[39m, \u001b[33m13\u001b[39m, \u001b[33m14\u001b[39m,',
|
||||
' \u001b[33m15\u001b[39m, \u001b[33m16\u001b[39m, \u001b[33m17\u001b[39m,',
|
||||
' \u001b[33m18\u001b[39m, \u001b[33m19\u001b[39m, \u001b[33m20\u001b[39m,',
|
||||
' \u001b[33m21\u001b[39m, \u001b[33m22\u001b[39m, \u001b[33m23\u001b[39m,',
|
||||
' \u001b[33m24\u001b[39m, \u001b[33m25\u001b[39m, \u001b[33m26\u001b[39m,',
|
||||
' \u001b[33m27\u001b[39m, \u001b[33m28\u001b[39m, \u001b[33m29\u001b[39m,',
|
||||
' \u001b[33m30\u001b[39m, \u001b[33m31\u001b[39m, \u001b[33m32\u001b[39m,',
|
||||
' \u001b[33m33\u001b[39m, \u001b[33m34\u001b[39m, \u001b[33m35\u001b[39m,',
|
||||
' \u001b[33m36\u001b[39m, \u001b[33m37\u001b[39m, \u001b[33m38\u001b[39m,',
|
||||
' \u001b[33m39\u001b[39m, \u001b[33m40\u001b[39m, \u001b[33m41\u001b[39m,',
|
||||
' \u001b[33m42\u001b[39m, \u001b[33m43\u001b[39m, \u001b[33m44\u001b[39m,',
|
||||
' \u001b[33m45\u001b[39m, \u001b[33m46\u001b[39m, \u001b[33m47\u001b[39m,',
|
||||
' \u001b[33m48\u001b[39m, \u001b[33m49\u001b[39m, \u001b[33m50\u001b[39m,',
|
||||
' \u001b[33m51\u001b[39m, \u001b[33m52\u001b[39m, \u001b[33m53\u001b[39m,',
|
||||
' \u001b[33m54\u001b[39m, \u001b[33m55\u001b[39m, \u001b[33m56\u001b[39m,',
|
||||
' \u001b[33m57\u001b[39m, \u001b[33m58\u001b[39m, \u001b[33m59\u001b[39m',
|
||||
']'
|
||||
].join('\n');
|
||||
|
||||
assert.strictEqual(out, expected);
|
||||
|
||||
out = util.inspect([1, 2, 3, 4], { compact: 1, colors: true });
|
||||
expected = '[ \u001b[33m1\u001b[39m, \u001b[33m2\u001b[39m, ' +
|
||||
'\u001b[33m3\u001b[39m, \u001b[33m4\u001b[39m ]';
|
||||
|
||||
assert.strictEqual(out, expected);
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user