@oclif/core
Version:
base library for oclif CLIs
195 lines (194 loc) • 7.54 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.HelpFormatter = void 0;
const ansis_1 = __importDefault(require("ansis"));
const indent_string_1 = __importDefault(require("indent-string"));
const string_width_1 = __importDefault(require("string-width"));
const widest_line_1 = __importDefault(require("widest-line"));
const wrap_ansi_1 = __importDefault(require("wrap-ansi"));
const screen_1 = require("../screen");
const theme_1 = require("../ux/theme");
const util_1 = require("./util");
class HelpFormatter {
config;
indentSpacing = 2;
opts;
/**
* Takes a string and replaces `<%= prop =>` with the value of prop, where prop is anything on
* `config=Interfaces.Config` or `opts=Interface.HelpOptions`.
*
* ```javascript
* `<%= config.bin =>` // will resolve to the bin defined in `pjson.oclif`.
* ```
*/
render;
constructor(config, opts = {}) {
this.config = config;
this.opts = { maxWidth: screen_1.stdtermwidth, ...opts };
this.render = (0, util_1.template)(this);
}
/**
* Indent by `this.indentSpacing`. The text should be wrap based on terminal width before indented.
*
* In order to call indent multiple times on the same set or text, the caller must wrap based on
* the number of times the text has been indented. For example.
*
* ```javascript
* const body = `main line\n${indent(wrap('indented example line', 4))}`
* const header = 'SECTION'
* console.log(`${header}\n${indent(wrap(body))}`
* ```
* will output
* ```
* SECTION
* main line
* indented example line
* ```
*
* If the terminal width was 24 and the `4` was not provided in the first wrap, it would like like the following.
* ```
* SECTION
* main line
* indented example
* line
* ```
* @param body the text to indent
* @param spacing the final number of spaces this text will be indented
* @returns the formatted indented text
*/
indent(body, spacing = this.indentSpacing) {
return (0, indent_string_1.default)(body, spacing);
}
renderList(input, opts) {
if (input.length === 0) {
return '';
}
const renderMultiline = () => {
let output = '';
for (let [left, right] of input) {
if (!left && !right)
continue;
if (left) {
if (opts.stripAnsi)
left = ansis_1.default.strip(left);
output += this.wrap(left.trim(), opts.indentation);
}
if (right) {
if (opts.stripAnsi)
right = ansis_1.default.strip(right);
output += '\n';
output += this.indent(this.wrap(right.trim(), opts.indentation + 2), 4);
}
output += '\n\n';
}
return output.trim();
};
if (opts.multiline)
return renderMultiline();
const maxLength = (0, widest_line_1.default)(input.map((i) => i[0]).join('\n'));
let output = '';
let spacer = opts.spacer || '\n';
let cur = '';
for (const [left, r] of input) {
let right = r;
if (cur) {
output += spacer;
output += cur;
}
cur = left || '';
if (opts.stripAnsi)
cur = ansis_1.default.strip(cur);
if (!right) {
cur = cur.trim();
continue;
}
if (opts.stripAnsi)
right = ansis_1.default.strip(right);
right = this.wrap(right.trim(), opts.indentation + maxLength + 2);
const [first, ...lines] = right.split('\n').map((s) => s.trim());
cur += ' '.repeat(maxLength - (0, string_width_1.default)(cur) + 2);
cur += first;
if (lines.length === 0) {
continue;
}
// if we start putting too many lines down, render in multiline format
if (lines.length > 4)
return renderMultiline();
// if spacer is not defined, separate all rows with a newline
if (!opts.spacer)
spacer = '\n';
cur += '\n';
cur += this.indent(lines.join('\n'), maxLength + 2);
}
if (cur) {
output += spacer;
output += cur;
}
return output.trim();
}
section(header, body) {
// Always render template strings with the provided render function before wrapping and indenting
let newBody;
if (typeof body === 'string') {
newBody = this.render(body);
}
else if (Array.isArray(body)) {
newBody = body.map((entry) => {
if ('name' in entry) {
const tableEntry = entry;
return [this.render(tableEntry.name), this.render(tableEntry.description)];
}
const [left, right] = entry;
return [this.render(left), right && this.render(right)];
});
}
else if ('header' in body) {
return this.section(body.header, body.body);
}
else {
newBody = body
.map((entry) => [entry.name, entry.description])
.map(([left, right]) => [this.render(left), right && this.render(right)]);
}
const output = [
(0, theme_1.colorize)(this.config?.theme?.sectionHeader, (0, theme_1.colorize)('bold', header)),
(0, theme_1.colorize)(this.config?.theme?.sectionDescription, this.indent(Array.isArray(newBody) ? this.renderList(newBody, { indentation: 2, stripAnsi: this.opts.stripAnsi }) : newBody)),
].join('\n');
return this.opts.stripAnsi ? ansis_1.default.strip(output) : output;
}
/**
* Wrap text according to `opts.maxWidth` which is typically set to the terminal width. All text
* will be rendered before bring wrapped, otherwise it could mess up the lengths.
*
* A terminal will automatically wrap text, so this method is primarily used for indented
* text. For indented text, specify the indentation so it is taken into account during wrapping.
*
* Here is an example of wrapping with indentation.
* ```
* <------ terminal window width ------>
* <---------- no indentation --------->
* This is my text that will be wrapped
* once it passes maxWidth.
*
* <- indent -><------ text space ----->
* This is my text that will
* be wrapped once it passes
* maxWidth.
*
* <-- indent not taken into account ->
* This is my text that will
* be wrapped
* once it passes maxWidth.
* ```
* @param body the text to wrap
* @param spacing the indentation size to subtract from the terminal width
* @returns the formatted wrapped text
*/
wrap(body, spacing = this.indentSpacing) {
return (0, wrap_ansi_1.default)(this.render(body), this.opts.maxWidth - spacing, { hard: true });
}
}
exports.HelpFormatter = HelpFormatter;