UNPKG

@hashgraph/solo

Version:

An opinionated CLI tool to deploy and manage private Hedera Networks.

169 lines (140 loc) 5.82 kB
// SPDX-License-Identifier: Apache-2.0 import {inject, injectable} from 'tsyringe-neo'; import {InjectTokens} from './dependency-injection/inject-tokens.js'; import {type SoloLogger} from './logging/solo-logger.js'; import {patchInject} from './dependency-injection/container-helper.js'; type Table = string[][]; @injectable() export class HelpRenderer { public constructor(@inject(InjectTokens.SoloLogger) private readonly logger: SoloLogger) { this.logger = patchInject(logger, InjectTokens.SoloLogger, this.constructor.name); } private splitAtClosestWhitespace(input: string, maxLength: number = 120): [string, string] { if (input.length <= maxLength) { return [input, '']; } const splitIndex: number = input.lastIndexOf(' ', maxLength); if (splitIndex === -1) { return [input, '']; } return [input.slice(0, Math.max(0, splitIndex)), input.slice(Math.max(0, splitIndex + 1))]; } private createFlagsTable(lines: string[]): Table { const table: Table = []; for (const line of lines) { let columns: string[] = line.split(/(--[1-9a-zA-Z|-]+)/); // if the description contains --flag there will be more than the expected amount of columns // joins all columns after columns[2] if (columns.length > 3) { const firstPart: string[] = columns.slice(0, 2); const secondPart: string = columns.slice(2).join(' '); columns = [...firstPart, secondPart]; } else if (columns.length === 1 && table.length > 0) { const descriptions: string[] = columns[0].split(/(\[.+])/); table.at(-1)[2] += ` ${descriptions[0].trim()}`; if (descriptions[1]) { table.at(-1)[3] += ` ${descriptions[1].trim()}`.trim(); } } try { const descriptions: string[] = columns[2].split(/(\[.+])/); columns[2] = descriptions[0]; columns[3] = descriptions[1] || ''; columns = columns.map((column: string): string => column.trim()); table.push(columns); } catch (error) { this.logger.debug(`Error processing line for help rendering: ${line}`, error as Error); } } return table; } private sortFlagsTable(table: Table): Table { table.sort((row1: string[], row2: string[]) => { return row1[1].localeCompare(row2[1]); }); const requiredTable: Table = table.filter((row: string[]): boolean => row[3].includes('required')); const optionalTable: Table = table.filter((row: string[]): boolean => !row[3].includes('required')); return [ ...requiredTable, ['', '', '', ''], // add a blank line between required and optional flags ...optionalTable, ]; } private calculateMaxColumnLengths(table: Table): number[] { const columnMaxLengths: number[] = [0, 0, 0, 0]; for (const row of table) { for (const [index, element] of row.entries()) { columnMaxLengths[index] = Math.max(columnMaxLengths[index], element.length); } } return columnMaxLengths; } private wrapFlagsTable(table: Table, columnMaxLengths: number[], wrap: number): Table { const wrappedTable: Table = []; for (const row of table) { if (row[2].length > wrap) { const description: string = row[2]; let splitDescription: [string, string] = this.splitAtClosestWhitespace(description, wrap); wrappedTable.push([row[0], row[1], splitDescription[0], row[3]]); while (splitDescription[1] && splitDescription[1].length > 0) { splitDescription = this.splitAtClosestWhitespace(splitDescription[1], wrap); wrappedTable.push(['', '', splitDescription[0], '']); } } else { wrappedTable.push(row); } } return wrappedTable; } private getDescriptionWrap(terminalWidth: number, columnMaxLengths: number[]): number { let wrap: number = terminalWidth - columnMaxLengths[0] - columnMaxLengths[1] - columnMaxLengths[3] - 6; if (wrap < 30) { wrap = 30; } // set min and max values if (wrap > 70) { wrap = 70; } if (columnMaxLengths[2] < wrap) { wrap = columnMaxLengths[2]; } else { columnMaxLengths[2] = wrap; } return wrap; } private addColumnPadding(table: Table, columnMaxLengths: number[]): string[] { const outputLines: string[] = []; for (const row of table) { const line: string[] = []; for (const [index, element] of row.entries()) { line.push(element.padEnd(columnMaxLengths[index])); } outputLines.push(line.join(' ')); } return outputLines; } public render(rootCmd: any, rawHelp: string): void { const splittingString: string = 'Options:\n'; const splitOutput: string[] = rawHelp.split(splittingString); if (splitOutput.length < 2) { this.logger.showUser(rawHelp); return; } let finalOutput: string = splitOutput[0] + splittingString; let lines: string[] = splitOutput[1].split('\n'); lines = lines.map((line: string): string => line.replace(/^\s+/, '')); // Formatting for flag options const table: Table = this.createFlagsTable(lines); // apply sorting const sortedTable: Table = this.sortFlagsTable(table); const columnMaxLengths: number[] = this.calculateMaxColumnLengths(sortedTable); // wrap the description column at the wrapping point const terminalWidth: number = rootCmd.terminalWidth(); const wrap: number = this.getDescriptionWrap(terminalWidth, columnMaxLengths); const wrappedTable = this.wrapFlagsTable(sortedTable, columnMaxLengths, wrap); const outputLines = this.addColumnPadding(wrappedTable, columnMaxLengths); finalOutput += '\n'; finalOutput += outputLines.join('\n'); finalOutput += '\n'; this.logger.showUser(finalOutput); } }