mirror of
https://github.com/curioustorvald/tsvm.git
synced 2026-06-08 14:24:05 +09:00
181 lines
6.0 KiB
JavaScript
181 lines
6.0 KiB
JavaScript
/*
|
|
* synopsis.js -- system-wide help / tldr.
|
|
*
|
|
* Prints a command's human-targeted one-line summary and an auto-generated
|
|
* synopsis (usage line, arguments, options and constraints) derived from its
|
|
* TSF .synopsis document via the `synopsis` library (synopsis.mjs).
|
|
*
|
|
* Usage: synopsis PROGRAM
|
|
* synopsis (describes itself)
|
|
*/
|
|
|
|
let syn
|
|
try {
|
|
syn = require("synopsis")
|
|
} catch (e) {
|
|
printerrln("synopsis: the 'synopsis' library is not installed")
|
|
return 1
|
|
}
|
|
|
|
const termW = (con.getmaxyx()[1]) || 80
|
|
|
|
// Word-wrap plain text to `width`, returning an array of lines.
|
|
function wrap(text, width) {
|
|
if (!text) return []
|
|
if (width < 8) width = 8
|
|
let words = ('' + text).split(/\s+/).filter(function (w) { return w.length })
|
|
let lines = [], line = ''
|
|
words.forEach(function (w) {
|
|
if (line.length === 0) line = w
|
|
else if (line.length + 1 + w.length <= width) line += ' ' + w
|
|
else { lines.push(line); line = w }
|
|
})
|
|
if (line.length) lines.push(line)
|
|
return lines
|
|
}
|
|
|
|
// Print a "left summary" row: the summary is wrapped into the right column and
|
|
// continuation lines are aligned under it. An over-wide `left` spills onto its
|
|
// own line.
|
|
function row(left, summary, leftW, indent) {
|
|
let pad = ' '.repeat(indent)
|
|
let gap = 2
|
|
let sumW = Math.max(8, termW - indent - leftW - gap)
|
|
let wrapped = wrap(summary, sumW)
|
|
if (left.length > leftW) {
|
|
println(pad + left)
|
|
wrapped.forEach(function (l) { println(pad + ' '.repeat(leftW + gap) + l) })
|
|
} else {
|
|
let first = wrapped.length ? wrapped[0] : ''
|
|
println(pad + left + ' '.repeat(leftW - left.length + gap) + first)
|
|
for (let i = 1; i < wrapped.length; i++)
|
|
println(pad + ' '.repeat(leftW + gap) + wrapped[i])
|
|
}
|
|
}
|
|
|
|
// ---- resolve the target ----------------------------------------------------
|
|
let token = (exec_args[1] !== undefined && exec_args[1] !== '') ? exec_args[1] : "synopsis"
|
|
|
|
let model = syn.getModel(token)
|
|
if (!model) {
|
|
printerrln(`synopsis: no synopsis found for '${token}'`)
|
|
return 1
|
|
}
|
|
|
|
// Display name for a referenced symbol id (used by the constraints section).
|
|
function symDisplay(id) {
|
|
let s = model.symbols[id]
|
|
if (!s) return id
|
|
if (s.kind === 'option') return s.long || s.short || id
|
|
if (s.kind === 'positional') return s.name || id
|
|
if (s.kind === 'subcommand') return s.name || id
|
|
return id
|
|
}
|
|
|
|
// Append a "{a, b, c}" hint of permitted values to a summary, if any.
|
|
function withValues(summary, values) {
|
|
if (!values || !values.length) return summary || ''
|
|
let vs = values.map(function (v) {
|
|
return (v && typeof v === 'object' && ('value' in v)) ? v.value : v
|
|
}).join(', ')
|
|
return (summary ? summary + ' ' : '') + '{' + vs + '}'
|
|
}
|
|
|
|
// Left-column text for an option, e.g. "-o, --output=FILE".
|
|
function optionLeft(e) {
|
|
let forms = []
|
|
if (e.short) forms.push(e.short)
|
|
if (e.long) forms.push(e.long)
|
|
let s = forms.join(', ')
|
|
if (e.hasValue) {
|
|
let vn = (e.value && (e.value.name || e.value.type)) || 'VALUE'
|
|
if (e.long) s += e.valueRequired ? '=' + vn : '[=' + vn + ']'
|
|
else s += e.valueRequired ? ' ' + vn : ' [' + vn + ']'
|
|
}
|
|
return s
|
|
}
|
|
function optionSummary(e) {
|
|
let s = e.summary || ''
|
|
if (e.negatable) s += (s ? ' ' : '') + '(negatable)'
|
|
if (e.value && e.value.values && e.value.values.length) s = withValues(s, e.value.values)
|
|
return s
|
|
}
|
|
|
|
function constraintText(c) {
|
|
let names = (c.symbols || []).map(symDisplay)
|
|
if (c.type === 'conflicts') return 'Mutually exclusive: ' + names.join(', ')
|
|
if (c.type === 'requires') return symDisplay(c.subject) + ' requires ' + (c.targets || []).map(symDisplay).join(', ')
|
|
if (c.type === 'implies') return symDisplay(c.subject) + ' implies ' + (c.targets || []).map(symDisplay).join(', ')
|
|
if (c.type === 'cardinality') {
|
|
let mn = c.minimum, mx = c.maximum, q
|
|
if (mn === 1 && mx === 1) q = 'Exactly one of'
|
|
else if (mn === 1 && mx === undefined) q = 'At least one of'
|
|
else if (mn === undefined && mx === 1) q = 'At most one of'
|
|
else q = `Between ${mn} and ${mx} of`
|
|
return q + ': ' + names.join(', ')
|
|
}
|
|
return null
|
|
}
|
|
|
|
// ---- gather rows -----------------------------------------------------------
|
|
let argEntries = model.positionals.map(function (p) {
|
|
return { left: (p.name || p.id) + (p.repeatable ? '...' : ''), summary: withValues(p.summary, p.values) }
|
|
})
|
|
let optEntries = model.flags.map(function (e) {
|
|
return { left: optionLeft(e), summary: optionSummary(e) }
|
|
})
|
|
let subEntries = model.subcommands.map(function (s) {
|
|
return { left: s.name, summary: s.summary || '' }
|
|
})
|
|
|
|
// shared left-column width (capped so a long flag does not push everything out)
|
|
let leftW = 4
|
|
argEntries.concat(optEntries, subEntries).forEach(function (e) { if (e.left.length > leftW) leftW = e.left.length })
|
|
if (leftW > 30) leftW = 30
|
|
|
|
// ---- render ----------------------------------------------------------------
|
|
let title = model.name || token
|
|
println(model.summary ? `${title} - ${model.summary}` : title)
|
|
println()
|
|
|
|
let usage = syn.getUsage(token)
|
|
if (usage) {
|
|
println("Usage:")
|
|
println(" " + usage)
|
|
println()
|
|
}
|
|
|
|
if (model.description) {
|
|
wrap(model.description, termW).forEach(function (l) { println(l) })
|
|
println()
|
|
}
|
|
|
|
if (subEntries.length) {
|
|
println("Commands:")
|
|
subEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
|
println()
|
|
}
|
|
|
|
if (argEntries.length) {
|
|
println("Arguments:")
|
|
argEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
|
println()
|
|
}
|
|
|
|
if (optEntries.length) {
|
|
println("Options:")
|
|
optEntries.forEach(function (e) { row(e.left, e.summary, leftW, 4) })
|
|
println()
|
|
}
|
|
|
|
if (model.constraints && model.constraints.length) {
|
|
let lines = model.constraints.map(constraintText).filter(function (t) { return t })
|
|
if (lines.length) {
|
|
println("Constraints:")
|
|
lines.forEach(function (l) { println(" " + l) })
|
|
println()
|
|
}
|
|
}
|
|
|
|
return 0
|