diff --git a/index.js b/index.js index f4737f6..f62f871 100644 --- a/index.js +++ b/index.js @@ -7,6 +7,10 @@ const camelCase = require('camelcase'); const ansiAlign = require('ansi-align'); const wrapAnsi = require('wrap-ansi'); +const NL = '\n'; +const PAD = ' '; +const BORDERS_WIDTH = 2; + const terminalColumns = () => { const {env, stdout, stderr} = process; @@ -71,6 +75,79 @@ const getBorderChars = borderStyle => { return chararacters; }; +const makeContentText = (text, padding, columns, align) => { + text = ansiAlign(text, {align}); + let lines = text.split(NL); + const textWidth = widestLine(text); + + const max = columns - padding.left - padding.right; + + if (textWidth > max) { + const newLines = []; + for (const line of lines) { + const createdLines = wrapAnsi(line, max, {hard: true}); + const alignedLines = ansiAlign(createdLines, {align}); + const alignedLinesArray = alignedLines.split('\n'); + const longestLength = Math.max(...alignedLinesArray.map(s => stringWidth(s))); + + for (const alignedLine of alignedLinesArray) { + let paddedLine; + switch (align) { + case 'center': + paddedLine = PAD.repeat((max - longestLength) / 2) + alignedLine; + break; + case 'right': + paddedLine = PAD.repeat(max - longestLength) + alignedLine; + break; + default: + paddedLine = alignedLine; + break; + } + + newLines.push(paddedLine); + } + } + + lines = newLines; + } + + if (align === 'center' && textWidth < max) { + lines = lines.map(line => PAD.repeat((max - textWidth) / 2) + line); + } else if (align === 'right' && textWidth < max) { + lines = lines.map(line => PAD.repeat(max - textWidth) + line); + } + + const paddingLeft = PAD.repeat(padding.left); + const paddingRight = PAD.repeat(padding.right); + + lines = lines.map(line => paddingLeft + line + paddingRight); + + lines = lines.map(line => { + if (columns - stringWidth(line) > 0) { + switch (align) { + case 'center': + return line + PAD.repeat(columns - stringWidth(line)); + case 'right': + return line + PAD.repeat(columns - stringWidth(line)); + default: + return line + PAD.repeat(columns - stringWidth(line)); + } + } + + return line; + }); + + if (padding.top > 0) { + lines = new Array(padding.top).fill(PAD.repeat(columns)).concat(lines); + } + + if (padding.bottom > 0) { + lines = lines.concat(new Array(padding.bottom).fill(PAD.repeat(columns))); + } + + return lines.join(NL); +}; + const isHex = color => color.match(/^#(?:[0-f]{3}){1,2}$/i); const isColorValid = color => typeof color === 'string' && ((chalk[color]) || isHex(color)); const getColorFn = color => isHex(color) ? chalk.hex(color) : chalk[color]; @@ -105,77 +182,35 @@ module.exports = (text, options) => { const colorizeContent = content => options.backgroundColor ? getBGColorFn(options.backgroundColor)(content) : content; - const NL = '\n'; - const PAD = ' '; const columns = terminalColumns(); - text = ansiAlign(text, {align: options.align}); - - let lines = text.split(NL); - - let contentWidth = widestLine(text) + padding.left + padding.right; - - const BORDERS_WIDTH = 2; - if (contentWidth + BORDERS_WIDTH > columns) { - contentWidth = columns - BORDERS_WIDTH; - const max = contentWidth - padding.left - padding.right; - const newLines = []; - for (const line of lines) { - const createdLines = wrapAnsi(line, max, {hard: true}); - const alignedLines = ansiAlign(createdLines, {align: options.align}); - const alignedLinesArray = alignedLines.split('\n'); - const longestLength = Math.max(...alignedLinesArray.map(s => stringWidth(s))); - - for (const alignedLine of alignedLinesArray) { - let paddedLine; - switch (options.align) { - case 'center': - paddedLine = PAD.repeat((max - longestLength) / 2) + alignedLine; - break; - case 'right': - paddedLine = PAD.repeat(max - longestLength) + alignedLine; - break; - default: - paddedLine = alignedLine; - break; - } - - newLines.push(paddedLine); - } - } - - lines = newLines; - } + let contentWidth = widestLine(wrapAnsi(text, columns - BORDERS_WIDTH, {hard: true})) + padding.left + padding.right; - if (contentWidth + BORDERS_WIDTH + margin.left + margin.right > columns) { + if ((margin.left && margin.right) && contentWidth + BORDERS_WIDTH + margin.left + margin.right > columns) { // Let's assume we have margins: left = 3, right = 5, in total = 8 const spaceForMargins = columns - contentWidth - BORDERS_WIDTH; // Let's assume we have space = 4 const multiplier = spaceForMargins / (margin.left + margin.right); // Here: multiplier = 4/8 = 0.5 - margin.left = Math.floor(margin.left * multiplier); - margin.right = Math.floor(margin.right * multiplier); + margin.left = Math.max(0, Math.floor(margin.left * multiplier)); + margin.right = Math.max(0, Math.floor(margin.right * multiplier)); // Left: 3 * 0.5 = 1.5 -> 1 // Right: 6 * 0.5 = 3 } - if (padding.top > 0) { - lines = new Array(padding.top).fill('').concat(lines); - } + // Prevent content from exceeding the console's width + contentWidth = Math.min(contentWidth, columns - BORDERS_WIDTH - margin.left - margin.right); - if (padding.bottom > 0) { - lines = lines.concat(new Array(padding.bottom).fill('')); - } + text = makeContentText(text, padding, contentWidth, options.align); - const paddingLeft = PAD.repeat(padding.left); let marginLeft = PAD.repeat(margin.left); if (options.float === 'center') { - const padWidth = Math.max((columns - contentWidth - BORDERS_WIDTH) / 2, 0); - marginLeft = PAD.repeat(padWidth); + const marginWidth = Math.max((columns - contentWidth - BORDERS_WIDTH) / 2, 0); + marginLeft = PAD.repeat(marginWidth); } else if (options.float === 'right') { - const padWidth = Math.max(columns - contentWidth - margin.right - BORDERS_WIDTH, 0); - marginLeft = PAD.repeat(padWidth); + const marginWidth = Math.max(columns - contentWidth - margin.right - BORDERS_WIDTH, 0); + marginLeft = PAD.repeat(marginWidth); } const horizontal = chars.horizontal.repeat(contentWidth); @@ -185,9 +220,10 @@ module.exports = (text, options) => { const LINE_SEPARATOR = (contentWidth + BORDERS_WIDTH + margin.left >= columns) ? '' : NL; + const lines = text.split(NL); + const middle = lines.map(line => { - const paddingRight = PAD.repeat(contentWidth - stringWidth(line) - padding.left); - return marginLeft + side + colorizeContent(paddingLeft + line + paddingRight) + side; + return marginLeft + side + colorizeContent(line) + side; }).join(LINE_SEPARATOR); return top + LINE_SEPARATOR + middle + LINE_SEPARATOR + bottom; diff --git a/test.js b/test.js index 1967983..d786357 100644 --- a/test.js +++ b/test.js @@ -525,133 +525,162 @@ test('text is right-aligned after wrapping', t => { t.is(lines[2], expected); }); -// TODO: Find out why it fails on GitHub Actions. -if (!process.env.CI) { - test('text is centered after wrapping when using words', t => { - const width = process.stdout.columns || 120; - const sentence = 'x'.repeat(width / 3) + ' '; - const longContent = sentence.repeat(3).trim(); - const box = boxen(longContent, {align: 'center'}); - - t.is(box.length, width * 4); - - const lines = []; - for (let index = 0; index < 4; ++index) { - const line = box.slice(index * width, (index + 1) * width); - t.is(line.length, width, 'Length of line #' + index); - t.is(line, line.trim(), 'No margin of line #' + index); - - lines.push(line); - } - - const checkAlign = index => { - const line = lines[index]; - const lineWithoutBorders = line.slice(1, -1); - const paddingLeft = lineWithoutBorders.length - lineWithoutBorders.trimStart().length; - const paddingRight = lineWithoutBorders.length - lineWithoutBorders.trimEnd().length; +test('box not overflowing terminal', t => { + const width = process.stdout.columns || 120; + const longContent = 'x'.repeat(width * 4); + const box = boxen(longContent); - t.true(paddingLeft > 0, 'Padding left in line #' + index); - t.true(paddingRight > 0, 'Padding right in line #' + index); - t.true(Math.abs(paddingLeft - paddingRight) <= 1, 'Left and right padding are not (almost) equal in line #' + index); - }; + const lines = []; + for (let index = 1; index < 6; ++index) { + const line = box.slice(index * width, (index + 1) * width); + lines.push(line); + } - checkAlign(1); - checkAlign(2); - }); -} + for (const line of lines) { + t.is(line[0], '│', 'First character of line isn\'t box border'); + t.is(line[width - 1], '│', 'Last character of line isn\'t box border'); + } +}); -test('text is left-aligned after wrapping when using words', t => { +test('box not overflowing terminal with padding', t => { const width = process.stdout.columns || 120; - const sentence = 'x'.repeat(width / 3) + ' '; - const longContent = sentence.repeat(3).trim(); - const box = boxen(longContent, {align: 'left'}); + const longContent = 'x'.repeat(width * 4); + const box = boxen(longContent, {padding: 2}); - t.is(box.length, width * 4); + const lines = []; + for (let index = 1; index < 10; ++index) { + const line = box.slice(index * width, (index + 1) * width); + lines.push(line); + } + + for (const line of lines) { + t.is(line[0], '│', 'First character of line isn\'t box border'); + t.is(line[width - 1], '│', 'Last character of line isn\'t box border'); + } +}); + +test('box not overflowing terminal with padding and margin', t => { + const width = process.stdout.columns || 120; + const longContent = 'x'.repeat(width * 4); + const box = boxen(longContent, {padding: 2, margin: {left: 2, right: 2}}); const lines = []; - for (let index = 0; index < 4; ++index) { + for (let index = 1; index < 10; ++index) { const line = box.slice(index * width, (index + 1) * width); - t.is(line.length, width, 'Length of line #' + index); - t.is(line, line.trim(), 'No margin of line #' + index); + lines.push(line); + } + + for (const line of lines) { + t.is(line[0], '│', 'First character of line isn\'t box border'); + t.is(line[width - 1], '│', 'Last character of line isn\'t box border'); + } +}); + +test('box not overflowing terminal with words and margin', t => { + const width = process.stdout.columns || 120; + const word = 'x'.repeat(width / 2) + ' '; + const longContent = word.repeat(5); + const box = boxen(longContent, {margin: {left: 2, right: 2}}); + const lines = []; + for (let index = 1; index < 6; ++index) { + const line = box.slice(index * (word.length + 4), (index + 1) * (word.length + 4)); lines.push(line); } - const checkAlign = index => { + for (const line of lines) { + t.is(line.trim()[0], '│', 'First character of line isn\'t box border'); + t.is(line.trim()[word.length], '│', 'Last character of line isn\'t box border'); + } +}); + +test('text is centered after wrapping when using words', t => { + const width = process.stdout.columns || 120; + const sentence = 'x'.repeat(width / 4) + ' '; + const longContent = sentence.repeat(4).trim(); + const box = boxen(longContent, {align: 'center'}); + + const lines = box.split('\n'); + + const checkAlign = ({index, leftPad, rightPad}) => { const line = lines[index]; const lineWithoutBorders = line.slice(1, -1); const paddingLeft = lineWithoutBorders.length - lineWithoutBorders.trimStart().length; const paddingRight = lineWithoutBorders.length - lineWithoutBorders.trimEnd().length; - t.is(paddingLeft, 0, 'Padding left in line #' + index); - t.true(paddingRight > 0, 'Padding right in line #' + index); + t.is(paddingLeft, leftPad, 'Padding left in line #' + index); + t.is(paddingRight, rightPad, 'Padding right in line #' + index); }; - checkAlign(1); - checkAlign(2); + checkAlign({index: 1, leftPad: 0, rightPad: 0}); + checkAlign({index: 2, leftPad: sentence.length, rightPad: sentence.length}); }); -test('text is right-aligned after wrapping when using words', t => { +test('text is left-aligned after wrapping when using words', t => { const width = process.stdout.columns || 120; - const sentence = 'x'.repeat(width / 3) + ' '; - const longContent = sentence.repeat(3).trim(); - const box = boxen(longContent, {align: 'right'}); + const sentence = 'x'.repeat(width / 4) + ' '; + const longContent = sentence.repeat(4).trim(); + const box = boxen(longContent); - t.is(box.length, width * 4); + const lines = box.split('\n'); - const lines = []; - for (let index = 0; index < 4; ++index) { - const line = box.slice(index * width, (index + 1) * width); - t.is(line.length, width, 'Length of line #' + index); - t.is(line, line.trim(), 'No margin of line #' + index); + const checkAlign = ({index, leftPad, rightPad}) => { + const line = lines[index]; + const lineWithoutBorders = line.slice(1, -1); + const paddingLeft = lineWithoutBorders.length - lineWithoutBorders.trimStart().length; + const paddingRight = lineWithoutBorders.length - lineWithoutBorders.trimEnd().length; - lines.push(line); - } + t.is(paddingLeft, leftPad, 'Padding left in line #' + index); + t.is(paddingRight, rightPad, 'Padding right in line #' + index); + }; - const checkAlign = index => { + checkAlign({index: 1, leftPad: 0, rightPad: 0}); + checkAlign({index: 2, leftPad: 0, rightPad: sentence.length * 2}); +}); + +test('text is right-aligned after wrapping when using words', t => { + const width = process.stdout.columns || 120; + const sentence = 'x'.repeat(width / 4) + ' '; + const longContent = sentence.repeat(4).trim(); + const box = boxen(longContent, {align: 'right'}); + + const lines = box.split('\n'); + + const checkAlign = ({index, leftPad, rightPad}) => { const line = lines[index]; const lineWithoutBorders = line.slice(1, -1); const paddingLeft = lineWithoutBorders.length - lineWithoutBorders.trimStart().length; const paddingRight = lineWithoutBorders.length - lineWithoutBorders.trimEnd().length; - t.true(paddingLeft > 0, 'Padding left in line #' + index); - t.is(paddingRight, 0, 'Padding right in line #' + index); + t.is(paddingLeft, leftPad, 'Padding left in line #' + index); + t.is(paddingRight, rightPad, 'Padding right in line #' + index); }; - checkAlign(1); - checkAlign(2); + checkAlign({index: 1, leftPad: 0, rightPad: 0}); + checkAlign({index: 2, leftPad: sentence.length * 2, rightPad: 0}); }); test('text is right-aligned after wrapping when using words, with padding', t => { const width = process.stdout.columns || 120; - const sentence = 'x'.repeat(width / 3) + ' '; - const longContent = sentence.repeat(3).trim(); + const sentence = 'x'.repeat(width / 4) + ' '; + const longContent = sentence.repeat(4).trim(); const box = boxen(longContent, { align: 'right', padding: {left: 1, right: 1, top: 0, bottom: 0} }); - t.is(box.length, width * 4); - - const lines = []; - for (let index = 0; index < 4; ++index) { - const line = box.slice(index * width, (index + 1) * width); - t.is(line.length, width, 'Length of line #' + index); - t.is(line, line.trim(), 'No margin of line #' + index); - - lines.push(line); - } + const lines = box.split('\n'); - const checkAlign = index => { + const checkAlign = ({index, leftPad, rightPad}) => { const line = lines[index]; const lineWithoutBorders = line.slice(1, -1); const paddingLeft = lineWithoutBorders.length - lineWithoutBorders.trimStart().length; const paddingRight = lineWithoutBorders.length - lineWithoutBorders.trimEnd().length; - t.true(paddingLeft > 0, 'Padding left in line #' + index); - t.is(paddingRight, 1, 'Padding right in line #' + index); + t.is(paddingLeft, leftPad, 'Padding left in line #' + index); + t.is(paddingRight, rightPad, 'Padding right in line #' + index); }; - checkAlign(1); - checkAlign(2); + checkAlign({index: 1, leftPad: 1, rightPad: 1}); + checkAlign({index: 2, leftPad: (sentence.length * 2) + 1, rightPad: 1}); });