From ff43cfda1abbf8681bf683c74e3983855311b537 Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdxcode@users.noreply.github.com> Date: Tue, 30 Jan 2018 14:59:40 -0800 Subject: [PATCH] chore: started work on markdown output --- src/index.ts | 52 ++++++++++++++++++++++++------- test/command.test.ts | 60 ++++++++++++++++++++++++++---------- test/markdown.test.ts | 71 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 156 insertions(+), 27 deletions(-) create mode 100644 test/markdown.test.ts diff --git a/src/index.ts b/src/index.ts index ca60c46c..f898e260 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,7 +39,7 @@ export interface HelpOptions { markdown?: boolean } -function renderList(input: (string | undefined)[][], opts: {multiline?: boolean} = {}): string { +function renderList(input: (string | undefined)[][], opts: {maxWidth: number, multiline?: boolean}): string { if (input.length === 0) { return '' } @@ -48,15 +48,15 @@ function renderList(input: (string | undefined)[][], opts: {multiline?: boolean} for (let [left, right] of input) { if (!left && !right) continue if (left) { - output += indent(wrap(left.trim(), screen.stdtermwidth - 2, {hard: true, trim: false}), 2) + output += wrap(left.trim(), opts.maxWidth, {hard: true, trim: false}) } if (right) { output += '\n' - output += indent(wrap(right.trim(), screen.stdtermwidth - 4, {hard: true, trim: false}), 6) + output += indent(wrap(right.trim(), opts.maxWidth - 2, {hard: true, trim: false}), 4) } output += '\n\n' } - return output + return output.trim() } const maxLength = widestLine(input.map(i => i[0]).join('\n')) let spacer = '\n' @@ -71,7 +71,7 @@ function renderList(input: (string | undefined)[][], opts: {multiline?: boolean} cur = cur.trim() continue } - right = wrap(right.trim(), screen.stdtermwidth - (maxLength + 4), {hard: true, trim: false}) + right = wrap(right.trim(), opts.maxWidth - (maxLength + 2), {hard: true, trim: false}) // right = wrap(right.trim(), screen.stdtermwidth - (maxLength + 4), {hard: true, trim: false}) const [first, ...lines] = right!.split('\n').map(s => s.trim()) cur += ' '.repeat(maxLength - width(cur) + 2) @@ -80,7 +80,7 @@ function renderList(input: (string | undefined)[][], opts: {multiline?: boolean} continue } // if we start putting too many lines down, render in multiline format - // if (lines.length > 4) return renderList(input, {...opts, multiline: true}) + if (lines.length > 4) return renderList(input, {...opts, multiline: true}) spacer = '\n\n' cur += '\n' cur += indent(lines.join('\n'), maxLength + 2) @@ -95,13 +95,43 @@ function renderList(input: (string | undefined)[][], opts: {multiline?: boolean} export default class Help { constructor(public config: IConfig) {} - command(command: ICachedCommand, _: HelpOptions = {}): string { + command(command: ICachedCommand, opts: HelpOptions = {}): string { const help = new CommandHelp(this.config) const article = help.command(command) - return this.render(article) + return this.render(article, opts) } - protected render(article: Article): string { + protected render(article: Article, opts: HelpOptions): string { + if (opts.markdown) return this.renderMarkdown(article) + return this.renderScreen(article) + } + + protected renderMarkdown(article: Article): string { + const maxWidth = 120 + return _([ + article.title, + '='.repeat(width(article.title)), + ...article.sections + .map(s => { + let body + if (s.body.length === 0) { + body = '' + } else if (_.isArray(s.body[0])) { + body = renderList(s.body as any, {maxWidth: maxWidth - 2}) + } else { + body = _.castArray(s.body as string).join('\n') + body = wrap(body, maxWidth - 2, {trim: false, hard: true}) + } + return _([ + bold(s.heading.toUpperCase()), + indent(body, 2), + ]).compact().join('\n') + }) + ]).compact().join('\n') + } + + protected renderScreen(article: Article): string { + const maxWidth = screen.stdtermwidth return _([ article.title, ...article.sections @@ -110,10 +140,10 @@ export default class Help { if (s.body.length === 0) { body = '' } else if (_.isArray(s.body[0])) { - body = renderList(s.body as any) + body = renderList(s.body as any, {maxWidth: maxWidth - 2}) } else { body = _.castArray(s.body as string).join('\n') - body = wrap(body, screen.stdtermwidth - 2, {trim: false, hard: true}) + body = wrap(body, maxWidth - 2, {trim: false, hard: true}) } return _([ bold(s.heading.toUpperCase()), diff --git a/test/command.test.ts b/test/command.test.ts index c057e683..d8495730 100644 --- a/test/command.test.ts +++ b/test/command.test.ts @@ -41,7 +41,7 @@ describe('command help', () => { ss: flags.boolean({description: 'newliney\n'.repeat(4)}), remote: flags.string({char: 'r'}), }}) - .it(ctx => expect(ctx.commandHelp).to.equal(`USAGE + .it('shows lots of output', ctx => expect(ctx.commandHelp).to.equal(`USAGE $ dxcli apps:create [APP_NAME] [OPTIONS] ARGUMENTS @@ -83,7 +83,7 @@ ALIASES ss: flags.boolean({description: 'newliney\n'.repeat(5)}), remote: flags.string({char: 'r'}), }}) - .it(ctx => expect(ctx.commandHelp).to.equal(`description of apps:create + .it('shows alternate output when many lines', ctx => expect(ctx.commandHelp).to.equal(`description of apps:create USAGE $ dxcli apps:create [APP_NAME] [OPTIONS] @@ -92,24 +92,52 @@ ARGUMENTS APP_NAME app to use OPTIONS - -f, --foo=foo foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo - barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar - foobar + -f, --foo=foo + foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoob + arfoobarfoobarfoobarfoobarfoobarfoobarfoobar -r, --remote=remote - --force force it force it force it force it force it force - it force it force it force it force it force it - force it force it force it force it force it force - it force it force it force it force it force it - force it force it force it force it force it force - it force it + --force + force it force it force it force it force it force it force it force + it force it force it force it force it force it force it force it + force it force it force it force it force it force it force it force + it force it force it force it force it force it force it - --ss newliney - newliney - newliney - newliney - newliney + --ss + newliney + newliney + newliney + newliney + newliney + +ALIASES + $ dxcli app:init + $ dxcli create`)) + + test + .commandHelp(class extends Command { + static id = 'apps:create' + static title = 'description of apps:create' + static aliases = ['app:init', 'create'] + static description = `some + + multiline help + ` + static args = [{name: 'app_name', description: 'app to use'}] + static flags = { + force: flags.boolean({description: 'forces'}), + }}) + .it('outputs with title', ctx => expect(ctx.commandHelp).to.equal(`description of apps:create + +USAGE + $ dxcli apps:create [APP_NAME] [OPTIONS] + +ARGUMENTS + APP_NAME app to use + +OPTIONS + --force forces ALIASES $ dxcli app:init diff --git a/test/markdown.test.ts b/test/markdown.test.ts new file mode 100644 index 00000000..2784d5fb --- /dev/null +++ b/test/markdown.test.ts @@ -0,0 +1,71 @@ +import {Command as Base, flags} from '@dxcli/command' +import {ICommand} from '@dxcli/config' +import {expect, test as base} from '@dxcli/test' +import stripAnsi = require('strip-ansi') + +global.columns = 80 +import Help from '../src' + +class Command extends Base { + async run() { return } +} + +const test = base +.loadConfig() +.add('help', ctx => new Help(ctx.config)) +.register('commandHelp', (command?: ICommand) => ({ + run(ctx: {help: Help, commandHelp: string, expectation: string}) { + const cached = command!.convertToCached() + let help = ctx.help.command(cached, {markdown: true}) + ctx.commandHelp = stripAnsi(help).split('\n').map(s => s.trimRight()).join('\n') + ctx.expectation = 'has commandHelp' + } +})) + +describe('command help', () => { + test + .commandHelp(class extends Command { + static title = 'the title' + static id = 'apps:create' + static aliases = ['app:init', 'create'] + static description = `some + + multiline help + ` + static args = [{name: 'app_name', description: 'app to use'}] + static flags = { + app: flags.string({char: 'a', hidden: true}), + foo: flags.string({char: 'f', description: 'foobar'.repeat(18)}), + force: flags.boolean({description: 'force it '.repeat(15)}), + ss: flags.boolean({description: 'newliney\n'.repeat(4)}), + remote: flags.string({char: 'r'}), + }}) + .skip() + .it(ctx => expect(ctx.commandHelp).to.equal(`the title +========= + +USAGE + $ dxcli apps:create [APP_NAME] [OPTIONS] + +ARGUMENTS + APP_NAME app to use + +OPTIONS + -f, --foo=foo foobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoo + barfoobarfoobarfoobarfoobarfoobarfoobarfoobarfoobar + + -r, --remote=remote + + --force force it force it force it force it force it force + it force it force it force it force it force it + force it force it force it force it + + --ss newliney + newliney + newliney + newliney + +ALIASES + $ dxcli app:init + $ dxcli create`)) +})