jackspeak
Advanced tools
Comparing version 2.2.3 to 2.3.0
@@ -117,3 +117,3 @@ /// <reference types="node" /> | ||
level: number; | ||
pre?: false; | ||
pre?: boolean; | ||
} | ||
@@ -223,3 +223,5 @@ /** | ||
*/ | ||
heading(text: string, level?: 1 | 2 | 3 | 4 | 5 | 6): Jack<C>; | ||
heading(text: string, level?: 1 | 2 | 3 | 4 | 5 | 6, { pre }?: { | ||
pre?: boolean; | ||
}): Jack<C>; | ||
/** | ||
@@ -266,2 +268,6 @@ * Add a long-form description to the usage output at this position. | ||
/** | ||
* Return the usage banner markdown for the given configuration | ||
*/ | ||
usageMarkdown(): string; | ||
/** | ||
* Return the configuration options as a plain object | ||
@@ -268,0 +274,0 @@ */ |
@@ -248,2 +248,3 @@ "use strict"; | ||
#usage; | ||
#usageMarkdown; | ||
constructor(options = {}) { | ||
@@ -430,7 +431,7 @@ this.#options = options; | ||
*/ | ||
heading(text, level) { | ||
heading(text, level, { pre = false } = {}) { | ||
if (level === undefined) { | ||
level = this.#fields.some(r => isHeading(r)) ? 2 : 1; | ||
} | ||
this.#fields.push({ type: 'heading', text, level }); | ||
this.#fields.push({ type: 'heading', text, level, pre }); | ||
return this; | ||
@@ -599,2 +600,112 @@ } | ||
} | ||
const { rows, maxWidth } = this.#usageRows(start); | ||
// every heading/description after the first gets indented by 2 | ||
// extra spaces. | ||
for (const row of rows) { | ||
if (row.left) { | ||
// If the row is too long, don't wrap it | ||
// Bump the right-hand side down a line to make room | ||
const configIndent = indent(Math.max(headingLevel, 2)); | ||
if (row.left.length > maxWidth - 3) { | ||
ui.div({ text: row.left, padding: [0, 0, 0, configIndent] }); | ||
ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] }); | ||
} | ||
else { | ||
ui.div({ | ||
text: row.left, | ||
padding: [0, 1, 0, configIndent], | ||
width: maxWidth, | ||
}, { padding: [0, 0, 0, 0], text: row.text }); | ||
} | ||
if (row.skipLine) { | ||
ui.div({ padding: [0, 0, 0, 0], text: '' }); | ||
} | ||
} | ||
else { | ||
if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
// only h1 and h2 have bottom padding | ||
// h3-h6 do not | ||
const b = level <= 2 ? 1 : 0; | ||
ui.div({ ...row, padding: [0, 0, b, indent(level)] }); | ||
} | ||
else { | ||
ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] }); | ||
} | ||
} | ||
} | ||
return (this.#usage = ui.toString()); | ||
} | ||
/** | ||
* Return the usage banner markdown for the given configuration | ||
*/ | ||
usageMarkdown() { | ||
if (this.#usageMarkdown) | ||
return this.#usageMarkdown; | ||
const out = []; | ||
let headingLevel = 1; | ||
const first = this.#fields[0]; | ||
let start = first?.type === 'heading' ? 1 : 0; | ||
if (first?.type === 'heading') { | ||
out.push(`# ${normalizeOneLine(first.text)}`); | ||
} | ||
out.push('Usage:'); | ||
if (this.#options.usage) { | ||
out.push(normalizeMarkdown(this.#options.usage, true)); | ||
} | ||
else { | ||
const cmd = (0, node_path_1.basename)(process.argv[1]); | ||
const shortFlags = []; | ||
const shorts = []; | ||
const flags = []; | ||
const opts = []; | ||
for (const [field, config] of Object.entries(this.#configSet)) { | ||
if (config.short) { | ||
if (config.type === 'boolean') | ||
shortFlags.push(config.short); | ||
else | ||
shorts.push([config.short, config.hint || field]); | ||
} | ||
else { | ||
if (config.type === 'boolean') | ||
flags.push(field); | ||
else | ||
opts.push([field, config.hint || field]); | ||
} | ||
} | ||
const sf = shortFlags.length ? ' -' + shortFlags.join('') : ''; | ||
const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join(''); | ||
const lf = flags.map(k => ` --${k}`).join(''); | ||
const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join(''); | ||
const usage = `${cmd}${sf}${so}${lf}${lo}`.trim(); | ||
out.push(normalizeMarkdown(usage, true)); | ||
} | ||
const maybeDesc = this.#fields[start]; | ||
if (isDescription(maybeDesc)) { | ||
out.push(normalizeMarkdown(maybeDesc.text, maybeDesc.pre)); | ||
start++; | ||
} | ||
const { rows } = this.#usageRows(start); | ||
// heading level in markdown is number of # ahead of text | ||
for (const row of rows) { | ||
if (row.left) { | ||
out.push('#'.repeat(headingLevel + 1) + | ||
' ' + | ||
normalizeOneLine(row.left, true)); | ||
if (row.text) | ||
out.push(normalizeMarkdown(row.text)); | ||
} | ||
else if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
out.push(`${'#'.repeat(headingLevel)} ${normalizeOneLine(row.text, row.pre)}`); | ||
} | ||
else { | ||
out.push(normalizeMarkdown(row.text, !!row.pre)); | ||
} | ||
} | ||
return (this.#usageMarkdown = out.join('\n\n') + '\n'); | ||
} | ||
#usageRows(start) { | ||
// turn each config type into a row, and figure out the width of the | ||
@@ -647,39 +758,3 @@ // left hand indentation for the option descriptions. | ||
} | ||
// every heading/description after the first gets indented by 2 | ||
// extra spaces. | ||
for (const row of rows) { | ||
if (row.left) { | ||
// If the row is too long, don't wrap it | ||
// Bump the right-hand side down a line to make room | ||
const configIndent = indent(Math.max(headingLevel, 2)); | ||
if (row.left.length > maxWidth - 3) { | ||
ui.div({ text: row.left, padding: [0, 0, 0, configIndent] }); | ||
ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] }); | ||
} | ||
else { | ||
ui.div({ | ||
text: row.left, | ||
padding: [0, 1, 0, configIndent], | ||
width: maxWidth, | ||
}, { padding: [0, 0, 0, 0], text: row.text }); | ||
} | ||
if (row.skipLine) { | ||
ui.div({ padding: [0, 0, 0, 0], text: '' }); | ||
} | ||
} | ||
else { | ||
if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
// only h1 and h2 have bottom padding | ||
// h3-h6 do not | ||
const b = level <= 2 ? 1 : 0; | ||
ui.div({ ...row, padding: [0, 0, b, indent(level)] }); | ||
} | ||
else { | ||
ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] }); | ||
} | ||
} | ||
} | ||
return (this.#usage = ui.toString()); | ||
return { rows, maxWidth }; | ||
} | ||
@@ -727,2 +802,15 @@ /** | ||
.trim(); | ||
// normalize for markdown printing, remove leading spaces on lines | ||
const normalizeMarkdown = (s, pre = false) => { | ||
const n = normalize(s, pre).replace(/\\/g, '\\\\'); | ||
return pre | ||
? `\`\`\`\n${n.replace(/\u200b/g, '')}\n\`\`\`` | ||
: n.replace(/\n +/g, '\n').trim(); | ||
}; | ||
const normalizeOneLine = (s, pre = false) => { | ||
const n = normalize(s, pre) | ||
.replace(/[\s\u200b]+/g, ' ') | ||
.trim(); | ||
return pre ? `\`${n}\`` : n; | ||
}; | ||
/** | ||
@@ -729,0 +817,0 @@ * Main entry point. Create and return a {@link Jack} object. |
@@ -117,3 +117,3 @@ /// <reference types="node" /> | ||
level: number; | ||
pre?: false; | ||
pre?: boolean; | ||
} | ||
@@ -223,3 +223,5 @@ /** | ||
*/ | ||
heading(text: string, level?: 1 | 2 | 3 | 4 | 5 | 6): Jack<C>; | ||
heading(text: string, level?: 1 | 2 | 3 | 4 | 5 | 6, { pre }?: { | ||
pre?: boolean; | ||
}): Jack<C>; | ||
/** | ||
@@ -266,2 +268,6 @@ * Add a long-form description to the usage output at this position. | ||
/** | ||
* Return the usage banner markdown for the given configuration | ||
*/ | ||
usageMarkdown(): string; | ||
/** | ||
* Return the configuration options as a plain object | ||
@@ -268,0 +274,0 @@ */ |
@@ -240,2 +240,3 @@ import { inspect } from 'node:util'; | ||
#usage; | ||
#usageMarkdown; | ||
constructor(options = {}) { | ||
@@ -422,7 +423,7 @@ this.#options = options; | ||
*/ | ||
heading(text, level) { | ||
heading(text, level, { pre = false } = {}) { | ||
if (level === undefined) { | ||
level = this.#fields.some(r => isHeading(r)) ? 2 : 1; | ||
} | ||
this.#fields.push({ type: 'heading', text, level }); | ||
this.#fields.push({ type: 'heading', text, level, pre }); | ||
return this; | ||
@@ -591,2 +592,112 @@ } | ||
} | ||
const { rows, maxWidth } = this.#usageRows(start); | ||
// every heading/description after the first gets indented by 2 | ||
// extra spaces. | ||
for (const row of rows) { | ||
if (row.left) { | ||
// If the row is too long, don't wrap it | ||
// Bump the right-hand side down a line to make room | ||
const configIndent = indent(Math.max(headingLevel, 2)); | ||
if (row.left.length > maxWidth - 3) { | ||
ui.div({ text: row.left, padding: [0, 0, 0, configIndent] }); | ||
ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] }); | ||
} | ||
else { | ||
ui.div({ | ||
text: row.left, | ||
padding: [0, 1, 0, configIndent], | ||
width: maxWidth, | ||
}, { padding: [0, 0, 0, 0], text: row.text }); | ||
} | ||
if (row.skipLine) { | ||
ui.div({ padding: [0, 0, 0, 0], text: '' }); | ||
} | ||
} | ||
else { | ||
if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
// only h1 and h2 have bottom padding | ||
// h3-h6 do not | ||
const b = level <= 2 ? 1 : 0; | ||
ui.div({ ...row, padding: [0, 0, b, indent(level)] }); | ||
} | ||
else { | ||
ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] }); | ||
} | ||
} | ||
} | ||
return (this.#usage = ui.toString()); | ||
} | ||
/** | ||
* Return the usage banner markdown for the given configuration | ||
*/ | ||
usageMarkdown() { | ||
if (this.#usageMarkdown) | ||
return this.#usageMarkdown; | ||
const out = []; | ||
let headingLevel = 1; | ||
const first = this.#fields[0]; | ||
let start = first?.type === 'heading' ? 1 : 0; | ||
if (first?.type === 'heading') { | ||
out.push(`# ${normalizeOneLine(first.text)}`); | ||
} | ||
out.push('Usage:'); | ||
if (this.#options.usage) { | ||
out.push(normalizeMarkdown(this.#options.usage, true)); | ||
} | ||
else { | ||
const cmd = basename(process.argv[1]); | ||
const shortFlags = []; | ||
const shorts = []; | ||
const flags = []; | ||
const opts = []; | ||
for (const [field, config] of Object.entries(this.#configSet)) { | ||
if (config.short) { | ||
if (config.type === 'boolean') | ||
shortFlags.push(config.short); | ||
else | ||
shorts.push([config.short, config.hint || field]); | ||
} | ||
else { | ||
if (config.type === 'boolean') | ||
flags.push(field); | ||
else | ||
opts.push([field, config.hint || field]); | ||
} | ||
} | ||
const sf = shortFlags.length ? ' -' + shortFlags.join('') : ''; | ||
const so = shorts.map(([k, v]) => ` --${k}=<${v}>`).join(''); | ||
const lf = flags.map(k => ` --${k}`).join(''); | ||
const lo = opts.map(([k, v]) => ` --${k}=<${v}>`).join(''); | ||
const usage = `${cmd}${sf}${so}${lf}${lo}`.trim(); | ||
out.push(normalizeMarkdown(usage, true)); | ||
} | ||
const maybeDesc = this.#fields[start]; | ||
if (isDescription(maybeDesc)) { | ||
out.push(normalizeMarkdown(maybeDesc.text, maybeDesc.pre)); | ||
start++; | ||
} | ||
const { rows } = this.#usageRows(start); | ||
// heading level in markdown is number of # ahead of text | ||
for (const row of rows) { | ||
if (row.left) { | ||
out.push('#'.repeat(headingLevel + 1) + | ||
' ' + | ||
normalizeOneLine(row.left, true)); | ||
if (row.text) | ||
out.push(normalizeMarkdown(row.text)); | ||
} | ||
else if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
out.push(`${'#'.repeat(headingLevel)} ${normalizeOneLine(row.text, row.pre)}`); | ||
} | ||
else { | ||
out.push(normalizeMarkdown(row.text, !!row.pre)); | ||
} | ||
} | ||
return (this.#usageMarkdown = out.join('\n\n') + '\n'); | ||
} | ||
#usageRows(start) { | ||
// turn each config type into a row, and figure out the width of the | ||
@@ -639,39 +750,3 @@ // left hand indentation for the option descriptions. | ||
} | ||
// every heading/description after the first gets indented by 2 | ||
// extra spaces. | ||
for (const row of rows) { | ||
if (row.left) { | ||
// If the row is too long, don't wrap it | ||
// Bump the right-hand side down a line to make room | ||
const configIndent = indent(Math.max(headingLevel, 2)); | ||
if (row.left.length > maxWidth - 3) { | ||
ui.div({ text: row.left, padding: [0, 0, 0, configIndent] }); | ||
ui.div({ text: row.text, padding: [0, 0, 0, maxWidth] }); | ||
} | ||
else { | ||
ui.div({ | ||
text: row.left, | ||
padding: [0, 1, 0, configIndent], | ||
width: maxWidth, | ||
}, { padding: [0, 0, 0, 0], text: row.text }); | ||
} | ||
if (row.skipLine) { | ||
ui.div({ padding: [0, 0, 0, 0], text: '' }); | ||
} | ||
} | ||
else { | ||
if (isHeading(row)) { | ||
const { level } = row; | ||
headingLevel = level; | ||
// only h1 and h2 have bottom padding | ||
// h3-h6 do not | ||
const b = level <= 2 ? 1 : 0; | ||
ui.div({ ...row, padding: [0, 0, b, indent(level)] }); | ||
} | ||
else { | ||
ui.div({ ...row, padding: [0, 0, 1, indent(headingLevel + 1)] }); | ||
} | ||
} | ||
} | ||
return (this.#usage = ui.toString()); | ||
return { rows, maxWidth }; | ||
} | ||
@@ -718,2 +793,15 @@ /** | ||
.trim(); | ||
// normalize for markdown printing, remove leading spaces on lines | ||
const normalizeMarkdown = (s, pre = false) => { | ||
const n = normalize(s, pre).replace(/\\/g, '\\\\'); | ||
return pre | ||
? `\`\`\`\n${n.replace(/\u200b/g, '')}\n\`\`\`` | ||
: n.replace(/\n +/g, '\n').trim(); | ||
}; | ||
const normalizeOneLine = (s, pre = false) => { | ||
const n = normalize(s, pre) | ||
.replace(/[\s\u200b]+/g, ' ') | ||
.trim(); | ||
return pre ? `\`${n}\`` : n; | ||
}; | ||
/** | ||
@@ -720,0 +808,0 @@ * Main entry point. Create and return a {@link Jack} object. |
{ | ||
"name": "jackspeak", | ||
"version": "2.2.3", | ||
"version": "2.3.0", | ||
"description": "A very strict and proper argument parser.", | ||
@@ -5,0 +5,0 @@ "main": "./dist/cjs/index.js", |
@@ -233,2 +233,9 @@ # jackspeak | ||
### `Jack.usageMarkdown(): string` | ||
Returns the compiled `usage` string, with all option descriptions | ||
and heading/description text, but as markdown instead of | ||
formatted for a terminal, for generating HTML documentation for | ||
your CLI. | ||
## Some Example Code | ||
@@ -235,0 +242,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
243547
2273
349