|
| 1 | +/* |
| 2 | + Generates CLI command reference in markdown format using command data obtained from yargs-help-parser |
| 3 | +*/ |
| 4 | + |
| 5 | +const logger = require('../cli/logger')('docs'); |
| 6 | + |
| 7 | + |
| 8 | +// Exclude these commands from the markdown |
| 9 | +const skipCommands = [ |
| 10 | + 'stripes', // root not needed |
| 11 | + 'stripes new', // deprecated |
| 12 | + 'stripes completion', // internal to yargs, does not render help |
| 13 | +]; |
| 14 | + |
| 15 | +// Exclude these options that apply to all commands |
| 16 | +const skipOptions = [ |
| 17 | + '--help', |
| 18 | + '--version', |
| 19 | + '--interactive', |
| 20 | +]; |
| 21 | + |
| 22 | +// Returns command in the document |
| 23 | +function commandTitle(command) { |
| 24 | + const wip = '(work in progress)'; |
| 25 | + return `\`${command.name}\` command${command.description.includes(wip) ? ` ${wip}` : ''}`; |
| 26 | +} |
| 27 | + |
| 28 | +// Returns link to a command in the document |
| 29 | +function markdownCommandLink(command, displayName) { |
| 30 | + return `[${displayName}](#${commandTitle(command).replace(/[`()]/g, '').replace(/\s/g, '-')})`; |
| 31 | +} |
| 32 | + |
| 33 | +// Returns a command heading prefixed with appropriate number of '#' |
| 34 | +function markdownCommandSummary(command, depth) { |
| 35 | + let level = depth; |
| 36 | + let headingLevel = '#'; |
| 37 | + while (level--) { |
| 38 | + headingLevel += '#'; |
| 39 | + } |
| 40 | + let summary = `\n${headingLevel} ${commandTitle(command)}\n\n`; |
| 41 | + summary += `${command.description}\n\n`; |
| 42 | + summary += 'Usage:\n```\n'; |
| 43 | + summary += `$ ${command.usage}\n`; |
| 44 | + summary += '```\n'; |
| 45 | + return summary; |
| 46 | +} |
| 47 | + |
| 48 | +// Returns bulleted list of sub-commands with links |
| 49 | +function markdownSubCommands(subCommands) { |
| 50 | + if (!subCommands.length) { |
| 51 | + return ''; |
| 52 | + } |
| 53 | + let section = 'Sub-commands:\n'; |
| 54 | + subCommands.forEach(sub => { |
| 55 | + const displayName = `\`${sub.fullName}\``; |
| 56 | + section += `* ${markdownCommandLink(sub, displayName)}\n`; |
| 57 | + }); |
| 58 | + return section; |
| 59 | +} |
| 60 | + |
| 61 | +// Returns formatted table of options (also used for positionals) |
| 62 | +function markdownOptionTable(displayName, optionsData) { |
| 63 | + const options = optionsData.filter(opt => !skipOptions.includes(opt.name)); |
| 64 | + if (!options.length) { |
| 65 | + return ''; |
| 66 | + } |
| 67 | + let optionsTable = `${displayName} | Description | Type | Notes\n`; |
| 68 | + optionsTable += '---|---|---|---\n'; |
| 69 | + options.forEach((option) => { |
| 70 | + const description = option.stdin ? option.description.replace('(stdin)', '') : option.description; |
| 71 | + const notes = [ |
| 72 | + option.required ? '(*)' : '', |
| 73 | + option.default ? `default: ${option.default}` : '', |
| 74 | + option.choices ? `choices: ${option.choices}` : '', |
| 75 | + option.stdin ? 'supports stdin' : '', |
| 76 | + ]; |
| 77 | + optionsTable += `\`${option.name}\` | ${description} | ${option.type} | ${notes.filter(note => note.length > 0).join(' ')}\n`; |
| 78 | + }); |
| 79 | + return optionsTable; |
| 80 | +} |
| 81 | + |
| 82 | +// Returns code blocks for command's "Examples:" section |
| 83 | +function markdownExamples(examples) { |
| 84 | + if (!examples.length) { |
| 85 | + return ''; |
| 86 | + } |
| 87 | + let exampleSection = 'Examples:\n\n'; |
| 88 | + examples.forEach((example) => { |
| 89 | + exampleSection += `${example.description}:\n`; |
| 90 | + exampleSection += '```\n'; |
| 91 | + exampleSection += `$ ${example.usage}\n`; |
| 92 | + exampleSection += '```\n'; |
| 93 | + }); |
| 94 | + return exampleSection; |
| 95 | +} |
| 96 | + |
| 97 | +// Defines overall structure and returns markdown for a single command |
| 98 | +function markdownCommand(command, depth) { |
| 99 | + logger.log(`generate markdown for ${command.name}`); |
| 100 | + const sections = [ |
| 101 | + markdownCommandSummary(command, depth), |
| 102 | + markdownSubCommands(command.subCommands), |
| 103 | + markdownOptionTable('Positional', command.positionals), |
| 104 | + markdownOptionTable('Option', command.options), |
| 105 | + markdownExamples(command.examples), |
| 106 | + ]; |
| 107 | + return sections.filter(section => section.length > 0).join('\n'); |
| 108 | +} |
| 109 | + |
| 110 | +// Recursively returns markdown for all commands in the tree |
| 111 | +function generateMarkdown(command, depth = 1) { |
| 112 | + const skip = skipCommands.includes(command.fullName); |
| 113 | + let markdown = skip ? '' : markdownCommand(command, depth); |
| 114 | + command.subCommands.forEach(subCommand => { |
| 115 | + markdown += generateMarkdown(subCommand, skip ? depth : depth + 1); |
| 116 | + }); |
| 117 | + return markdown; |
| 118 | +} |
| 119 | + |
| 120 | +// Recursively returns markdown TOC for all commands in the tree |
| 121 | +function generateToc(command, depth = 0) { |
| 122 | + const skip = skipCommands.includes(command.fullName); |
| 123 | + let level = depth; |
| 124 | + let tocIndent = ''; |
| 125 | + while (level--) { |
| 126 | + tocIndent += ' '; |
| 127 | + } |
| 128 | + const displayName = commandTitle(command); |
| 129 | + let toc = skip ? '' : `${tocIndent}* ${markdownCommandLink(command, displayName)}\n`; |
| 130 | + command.subCommands.forEach(subCommand => { |
| 131 | + toc += generateToc(subCommand, skip ? depth : depth + 1); |
| 132 | + }); |
| 133 | + return toc; |
| 134 | +} |
| 135 | + |
| 136 | +module.exports = { |
| 137 | + generateToc: (command) => generateToc(command).trim(), |
| 138 | + generateMarkdown, |
| 139 | +}; |
0 commit comments