use roff::{Inline, Roff};
use crate::{
item::ShortLong,
meta_help::{HelpItem, HelpItems, ShortLongHelp},
OptionParser,
};
struct Manpage {
roff: Roff,
}
#[derive(Debug, Clone, Copy)]
pub enum Section<'a> {
General,
SystemCall,
LibraryFunction,
SpecialFile,
FileFormat,
Game,
Misc,
Sysadmin,
Custom(&'a str),
}
impl Section<'_> {
fn as_str(&self) -> &str {
match self {
Section::General => "1",
Section::SystemCall => "2",
Section::LibraryFunction => "3",
Section::SpecialFile => "4",
Section::FileFormat => "5",
Section::Game => "6",
Section::Misc => "7",
Section::Sysadmin => "8",
Section::Custom(s) => s,
}
}
}
impl Manpage {
fn new(
application_name: &str,
section: Section,
last_update_date: Option<&str>,
vendor: Option<&str>,
application_title: Option<&str>,
) -> Self {
let mut manpage = Self { roff: Roff::new() };
manpage.roff.control(
"TH",
[
application_name,
section.as_str(),
last_update_date.unwrap_or("-"),
vendor.unwrap_or("-"),
application_title.unwrap_or(""),
]
.iter()
.copied(),
);
manpage
}
fn section<S>(&mut self, title: S) -> &mut Self
where
S: AsRef<str>,
{
self.roff.control("SH", [title.as_ref()]);
self
}
fn subsection<S>(&mut self, title: S) -> &mut Self
where
S: AsRef<str>,
{
self.roff.control("SS", [title.as_ref()]);
self
}
fn label<F>(&mut self, ops: F) -> &mut Self
where
F: FnMut(&mut Line),
{
self.roff.control("TP", []);
self.paragraph(ops)
}
fn paragraph<F>(&mut self, mut ops: F) -> &mut Self
where
F: FnMut(&mut Line),
{
let line = {
let mut l = Line {
manpage: self,
line: Vec::new(),
};
ops(&mut l);
std::mem::take(&mut l.line)
};
self.roff.text(line);
self
}
fn text(&mut self, line: impl Into<Vec<Inline>>) -> &mut Self {
self.roff.text(line);
self
}
fn render(&self) -> String {
self.roff.render()
}
}
struct Line<'a> {
manpage: &'a mut Manpage,
line: Vec<Inline>,
}
impl Drop for Line<'_> {
fn drop(&mut self) {
self.manpage.text(std::mem::take(&mut self.line));
}
}
impl Line<'_> {
fn metavar(&mut self, var: &str) -> &mut Self {
self.line.push(italic(var));
self
}
fn shortlong(&mut self, name: ShortLongHelp) -> &mut Self {
match name.0 {
ShortLong::Short(s) => self.line.push(bold(format!("-{}", s))),
ShortLong::Long(l) => self.line.push(bold(format!("--{}", l))),
ShortLong::ShortLong(s, l) => {
self.line.push(bold(format!("-{}", s)));
self.line.push(norm(", "));
self.line.push(bold(format!("--{}", l)));
}
}
self
}
fn env(&mut self, name: &str) -> &mut Self {
self.line.push(norm(", env variable "));
self.line.push(italic(name));
self
}
fn norm<S: Into<String>>(&mut self, s: S) -> &mut Self {
self.line.push(norm(s));
self
}
fn bold<S: Into<String>>(&mut self, s: S) -> &mut Self {
self.line.push(bold(s));
self
}
fn italic<S: Into<String>>(&mut self, s: S) -> &mut Self {
self.line.push(italic(s));
self
}
fn space(&mut self) -> &mut Self {
self.norm(" ")
}
fn usage(&mut self, usage: &crate::meta_usage::UsageMeta) -> &mut Self {
use crate::meta_usage::UsageMeta;
match usage {
UsageMeta::And(_) => todo!(),
UsageMeta::Or(xs) => {
for (ix, x) in xs.iter().enumerate() {
if ix > 0 {
self.norm(" | ");
};
self.usage(x);
}
}
UsageMeta::Required(u) => {
self.norm('(').usage(u).norm(')');
}
UsageMeta::Optional(meta) => {
self.norm('[').usage(meta).norm(']');
}
UsageMeta::Many(usage) => {
self.usage(usage).norm("...");
}
UsageMeta::ShortFlag(name) => {
self.norm('-').bold(*name);
}
UsageMeta::ShortArg(name, metavar) => {
self.norm('-').bold(*name).norm('=').italic(*metavar);
}
UsageMeta::LongFlag(name) => {
self.norm("--").bold(*name);
}
UsageMeta::LongArg(name, metavar) => {
self.norm("--").bold(*name).norm('=').italic(*metavar);
}
UsageMeta::Pos(x) | UsageMeta::StrictPos(x) => {
self.metavar(x);
}
UsageMeta::Command => {
self.bold("COMMAND").norm("...");
}
};
self
}
}
fn flatten_commands<'a>(help: &HelpItem<'a>, path: &str, acc: &mut Vec<(String, HelpItem<'a>)>) {
if let HelpItem::Command { name, meta, .. } = help {
acc.push((path.to_string(), *help));
let mut hi = HelpItems::default();
hi.classify(meta);
if !hi.cmds.is_empty() {
let path = format!("{} {}", path, name);
for help_item in &hi.cmds {
flatten_commands(help_item, &path, acc);
}
}
}
}
fn command_help(manpage: &mut Manpage, help: &HelpItem, path: &str) {
if let HelpItem::Command {
name,
short,
help,
meta,
info,
} = help
{
match short {
Some(short) => manpage.subsection(format!("{} {}, {}", path, name, short)),
None => manpage.subsection(format!("{} {}", path, name)),
};
if let Some(help) = help {
manpage.text([norm(*help)]);
}
if info.header.is_some() || info.footer.is_some() {
manpage.subsection("Description");
if let Some(header) = info.header {
manpage.text([norm(header), newline()]);
}
if let Some(footer) = info.footer {
manpage.text([norm(footer), newline()]);
}
}
let mut hi = HelpItems::default();
hi.classify(meta);
if !hi.psns.is_empty() {
manpage.subsection("Positional items");
for item in &hi.psns {
help_item(manpage, *item, None);
}
}
if !hi.flgs.is_empty() {
manpage.subsection("Option arguments and flags");
for item in &hi.flgs {
help_item(manpage, *item, None);
}
}
}
}
fn help_item(manpage: &mut Manpage, item: HelpItem, command_path: Option<&str>) {
match item {
HelpItem::Decor { help } => {
manpage.subsection(help);
}
HelpItem::BlankDecor => {
manpage.text([]);
}
HelpItem::Positional {
strict: _,
metavar,
help,
} => {
manpage.label(|l| {
l.metavar(metavar.0);
});
if let Some(help) = help {
manpage.text([norm(help)]);
}
}
HelpItem::Command {
name,
short: _,
help,
meta: _,
info: _,
} => {
if let Some(path) = command_path {
manpage.label(|l| {
l.bold(path).space().bold(name);
});
if let Some(help) = help {
manpage.text([norm(help)]);
}
}
}
HelpItem::Flag { name, help } => {
manpage.label(|l| {
l.shortlong(name);
});
if let Some(help) = help {
manpage.text([norm(help)]);
}
} HelpItem::Argument {
name,
metavar: mvar,
env,
help,
} => {
manpage.label(|l| {
l.shortlong(name).norm("=").metavar(mvar.0);
if let Some(env) = env {
l.env(env);
}
});
if let Some(help) = help {
manpage.text([norm(help)]);
}
} }
}
fn norm<S: Into<String>>(s: S) -> Inline {
Inline::Roman(s.into())
}
fn bold<S: Into<String>>(s: S) -> Inline {
Inline::Bold(s.into())
}
fn italic<S: Into<String>>(s: S) -> Inline {
Inline::Italic(s.into())
}
fn newline() -> Inline {
Inline::LineBreak
}
impl<T> OptionParser<T> {
#[must_use]
pub fn as_manpage(&self, app: &str, section: Section, date: &str) -> String {
let mut hi = HelpItems::default();
let meta = self.inner.meta();
hi.classify(&meta);
let mut manpage = Manpage::new(app, section, Some(date), None, None);
manpage.section("NAME");
manpage.paragraph(|line| {
match self.info.descr {
Some(descr) => line.norm(app).norm(" - ").norm(descr),
None => line.norm(app),
};
});
manpage.section("SYNOPSIS");
match meta.as_usage_meta() {
Some(usage) => manpage.paragraph(|l| {
l.bold(app).space().usage(&usage);
}),
None => manpage.text([bold(app), norm(" takes no parameters")]),
};
manpage.section("DESCRIPTION");
if let Some(header) = self.info.header {
manpage.text([norm(header), newline()]);
}
if let Some(footer) = self.info.footer {
manpage.text([norm(footer), newline()]);
}
if !hi.psns.is_empty() {
manpage.subsection("Positional items");
for item in &hi.psns {
help_item(&mut manpage, *item, None);
}
}
if !hi.flgs.is_empty() {
manpage.subsection("Option arguments and flags");
for item in &hi.flgs {
help_item(&mut manpage, *item, None);
}
}
if !hi.cmds.is_empty() {
manpage.subsection("List of all the subcommands");
let mut commands = Vec::new();
for item in &hi.cmds {
flatten_commands(item, app, &mut commands);
}
for (path, item) in &commands {
help_item(&mut manpage, *item, Some(path));
}
manpage.section("SUBCOMMANDS WITH OPTIONS");
for (path, item) in &commands {
command_help(&mut manpage, item, path);
}
}
let authors = env!("CARGO_PKG_AUTHORS").replace(':', ", ");
if !authors.is_empty() {
manpage.section("AUTHORS");
manpage.text([norm(authors)]);
}
let homepage = env!("CARGO_PKG_HOMEPAGE");
if !homepage.is_empty() {
manpage.section("See also");
manpage.text([norm(format!("Homepage: {}", homepage))]);
}
let repo = env!("CARGO_PKG_REPOSITORY");
if !repo.is_empty() {
manpage.section("REPORTING BUGS");
manpage.text([norm(repo)]);
}
manpage.render()
}
}