diff --git a/lib/help.js b/lib/help.js index 9c7fb1ecd..bc4826daa 100644 --- a/lib/help.js +++ b/lib/help.js @@ -322,7 +322,7 @@ class Help { // Description const commandDescription = helper.commandDescription(cmd); if (commandDescription.length > 0) { - output = output.concat([commandDescription, '']); + output = output.concat([helper.wrap(commandDescription, helpWidth, 0), '']); } // Arguments @@ -381,24 +381,27 @@ class Help { */ wrap(str, width, indent, minColumnWidth = 40) { - // Detect manually wrapped and indented strings by searching for line breaks - // followed by multiple spaces/tabs. - if (str.match(/[\n]\s+/)) return str; + // Full \s characters, minus the linefeeds. + const indents = ' \\f\\t\\v\u00a0\u1680\u2000-\u200a\u202f\u205f\u3000\ufeff'; + // Detect manually wrapped and indented strings by searching for line break followed by spaces. + const manualIndent = new RegExp(`[\\n][${indents}]+`); + if (str.match(manualIndent)) return str; // Do not wrap if not enough room for a wrapped column of text (as could end up with a word per line). const columnWidth = width - indent; if (columnWidth < minColumnWidth) return str; const leadingStr = str.slice(0, indent); - const columnText = str.slice(indent); - + const columnText = str.slice(indent).replace('\r\n', '\n'); const indentString = ' '.repeat(indent); - const regex = new RegExp('.{1,' + (columnWidth - 1) + '}([\\s\u200B]|$)|[^\\s\u200B]+?([\\s\u200B]|$)', 'g'); + const zeroWidthSpace = '\u200B'; + const breaks = `\\s${zeroWidthSpace}`; + // Match line end (so empty lines don't collapse), + // or as much text as will fit in column, or excess text up to first break. + const regex = new RegExp(`\n|.{1,${columnWidth - 1}}([${breaks}]|$)|[^${breaks}]+?([${breaks}]|$)`, 'g'); const lines = columnText.match(regex) || []; return leadingStr + lines.map((line, i) => { - if (line.slice(-1) === '\n') { - line = line.slice(0, line.length - 1); - } - return ((i > 0) ? indentString : '') + line.trimRight(); + if (line === '\n') return ''; // preserve empty lines + return ((i > 0) ? indentString : '') + line.trimEnd(); }).join('\n'); } } diff --git a/tests/help.wrap.test.js b/tests/help.wrap.test.js index 46ee89d8d..0d12d8359 100644 --- a/tests/help.wrap.test.js +++ b/tests/help.wrap.test.js @@ -41,13 +41,34 @@ ${' '.repeat(10)}${'a '.repeat(5)}a`); expect(wrapped).toEqual(text); }); - test('when text has line breaks then respect and indent', () => { + test('when text has line break then respect and indent', () => { const text = 'term description\nanother line'; const helper = new commander.Help(); const wrapped = helper.wrap(text, 78, 5); expect(wrapped).toEqual('term description\n another line'); }); + test('when text has consecutive line breaks then respect and indent', () => { + const text = 'term description\n\nanother line'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 78, 5); + expect(wrapped).toEqual('term description\n\n another line'); + }); + + test('when text has Windows line break then respect and indent', () => { + const text = 'term description\r\nanother line'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 78, 5); + expect(wrapped).toEqual('term description\n another line'); + }); + + test('when text has Windows consecutive line breaks then respect and indent', () => { + const text = 'term description\r\n\r\nanother line'; + const helper = new commander.Help(); + const wrapped = helper.wrap(text, 78, 5); + expect(wrapped).toEqual('term description\n\n another line'); + }); + test('when text already formatted with line breaks and indent then do not touch', () => { const text = 'term a '.repeat(25) + '\n ' + 'a '.repeat(25) + 'a'; const helper = new commander.Help(); @@ -97,7 +118,7 @@ Options: expect(program.helpInformation()).toBe(expectedOutput); }); - test('when long command description then wrap and indent', () => { + test('when long subcommand description then wrap and indent', () => { const program = new commander.Command(); program .configureHelp({ helpWidth: 80 }) @@ -142,7 +163,7 @@ Commands: expect(program.helpInformation()).toBe(expectedOutput); }); - test('when option descripton preformatted then only add small indent', () => { + test('when option description pre-formatted then only add small indent', () => { // #396: leave custom format alone, apart from space-space indent const optionSpec = '-t, --time '; const program = new commander.Command(); @@ -168,4 +189,25 @@ Options: expect(program.helpInformation()).toBe(expectedOutput); }); + + test('when command description long then wrapped', () => { + const program = new commander.Command(); + program + .configureHelp({ helpWidth: 80 }) + .description(`Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu +After line break Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore eu`); + const expectedOutput = `Usage: [options] + +Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor labore +eu Do fugiat eiusmod ipsum laboris excepteur pariatur sint ullamco tempor +labore eu +After line break Do fugiat eiusmod ipsum laboris excepteur pariatur sint +ullamco tempor labore eu Do fugiat eiusmod ipsum laboris excepteur pariatur +sint ullamco tempor labore eu + +Options: + -h, --help display help for command +`; + expect(program.helpInformation()).toBe(expectedOutput); + }); });