use orn::Or2;
use crate::{
meta::{Meta, Name},
parse::Key,
style::{Format, Item, Line, Style},
};
use core::{
fmt::{self, Write},
mem::{replace, take},
slice::from_ref,
};
use std::{
borrow::Cow,
fs,
ops::{ControlFlow, Deref},
};
struct Helper<'a, S: Style + ?Sized> {
buffer: &'a mut String,
path: &'a [Key],
style: &'a S,
indent: usize,
}
#[derive(Default)]
struct Columns {
short: usize,
long: usize,
types: usize,
}
struct Wrap<F>(F);
impl<F: Format> fmt::Display for Wrap<F> {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
self.0.format(f)
}
}
impl<'a, S: Style + ?Sized + 'a> Helper<'a, S> {
fn space(&mut self, width: usize) -> Result<usize, fmt::Error> {
for _ in 0..width {
write!(self.buffer, " ")?;
}
Ok(width)
}
fn own(&mut self) -> Helper<S> {
Helper {
buffer: self.buffer,
path: self.path,
style: self.style,
indent: self.indent,
}
}
fn indent(&mut self) -> Helper<S> {
self.indent_with(self.style.indent())
}
fn indent_with(&mut self, by: usize) -> Helper<S> {
let mut helper = self.own();
helper.indent += by;
helper
}
fn indentation(&mut self) -> Result<usize, fmt::Error> {
self.space(self.indent)?;
Ok(self.indent)
}
fn scope<T>(
&mut self,
scope: impl FnOnce(Helper<S>) -> Result<T, fmt::Error>,
) -> Result<String, fmt::Error> {
let buffer = take(self.buffer);
scope(self.own())?;
Ok(replace(self.buffer, buffer))
}
fn names(
&mut self,
metas: &[Meta],
short: bool,
long: bool,
prefix: impl Format,
suffix: impl Format,
) -> Result<usize, fmt::Error> {
self.join(metas, prefix, suffix, ", ", |meta| match meta {
Meta::Name(Name::Plain, value) => Some(Cow::Borrowed(value)),
Meta::Name(Name::Short, value) if short => Some(Cow::Borrowed(value)),
Meta::Name(Name::Long, value) if long => Some(Cow::Borrowed(value)),
Meta::Position(position) if short => Some(Cow::Owned(format!("[{position}]"))),
_ => None,
})
}
fn types(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
) -> Result<usize, fmt::Error> {
let mut name = None;
for meta in Meta::visible(metas) {
if let Meta::Type(value) = meta {
name = Some(value);
}
}
match name {
Some(name) => Ok(self.write(prefix)? + self.write(name)? + self.write(suffix)?),
None => Ok(0),
}
}
fn versions(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
) -> Result<usize, fmt::Error> {
self.join(metas, prefix, suffix, ", ", |meta| match meta {
Meta::Version(value) => Some(Cow::Borrowed(value)),
_ => None,
})
}
fn authors(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
) -> Result<usize, fmt::Error> {
self.join(metas, prefix, suffix, ", ", |meta| match meta {
Meta::Author(value) => Some(Cow::Borrowed(value)),
_ => None,
})
}
fn join(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
separator: impl Format,
mut find: impl FnMut(&Meta) -> Option<Cow<str>>,
) -> Result<usize, fmt::Error> {
let mut width = 0;
let mut prefix = Some(prefix);
for meta in Meta::visible(metas) {
if let Some(value) = find(meta) {
match prefix.take() {
Some(prefix) => width += self.write(prefix)?,
None => width += self.write(&separator)?,
}
width += self.write(value)?;
}
}
if prefix.is_none() {
width += self.write(suffix)?;
}
Ok(width)
}
fn wrap(
&mut self,
value: &str,
prefix: impl Format,
suffix: impl Format,
wrap: impl Format,
cursor: &mut usize,
has: &mut bool,
) -> Result<usize, fmt::Error> {
if value.is_empty() {
return Ok(0);
}
let mut width = 0;
let mut prefix = Some(prefix);
for line in value.split('\n') {
match prefix.take() {
Some(prefix) if !replace(has, true) => {
width += self.write(prefix)?;
}
_ => {
width += self.write_line(())?;
*cursor = self.indentation()?;
width += self.write(&wrap)?;
}
}
let mut has = false;
for word in line.split(' ') {
if replace(&mut has, true) {
width += self.write(" ")?;
}
if *cursor + word.len() > self.style.width() {
width += self.write_line(())?;
*cursor = self.indentation()?;
width += self.write(&wrap)?;
}
*cursor += self.write(word)?;
}
}
if width > 0 {
width += self.write(suffix)?;
}
Ok(width)
}
fn description(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
line: impl Format,
) -> Result<usize, fmt::Error> {
let mut count = 0;
for meta in Meta::visible(metas) {
if let Meta::Summary(value) = meta {
if !value.chars().all(char::is_whitespace) {
count += self.write_line(())?;
count += self.indentation()?;
count += self.wrap(value, &prefix, "", &line, &mut 0, &mut false)?;
}
}
}
if count > 0 {
count += self.write(suffix)?;
}
Ok(count)
}
fn summary(
&mut self,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
mut cursor: usize,
) -> Result<usize, fmt::Error> {
let mut has = false;
let mut width = 0;
for meta in Meta::visible(metas) {
if let Meta::Summary(value) = meta {
width += self.wrap(value, &prefix, &suffix, "", &mut cursor, &mut has)?;
}
}
if width == 0 {
for meta in Meta::visible(metas) {
if let Meta::Help(value) = meta {
width += self.wrap(value, &prefix, &suffix, "", &mut cursor, &mut has)?;
}
}
}
Ok(width)
}
fn usage(
&mut self,
root: &Meta,
metas: &[Meta],
prefix: impl Format,
suffix: impl Format,
) -> Result<usize, fmt::Error> {
let mut width = self.join(metas, &prefix, &suffix, " ", |meta| match meta {
Meta::Usage(value) => Some(Cow::Borrowed(value)),
_ => None,
})?;
if width == 0 {
width += self.write(prefix)?;
width += self.write("Usage:")?;
for key in root.key().as_ref().into_iter().chain(self.path) {
width += self.write((' ', key))?;
}
match Meta::descend(
metas,
(),
false,
usize::MAX,
|state, meta| match meta {
Meta::Option(_) => ControlFlow::Break(self.write(" [OPTIONS]")),
_ => ControlFlow::Continue(state),
},
|state, _| ControlFlow::Continue(state),
) {
ControlFlow::Break(result) => width += result?,
ControlFlow::Continue(_) => {}
}
fn control(result: Result<usize, fmt::Error>) -> ControlFlow<fmt::Error, usize> {
result.map_or_else(ControlFlow::Break, ControlFlow::Continue)
}
match Meta::descend(
metas,
None,
false,
usize::MAX,
|state, _| ControlFlow::Continue(state),
|state, meta| match meta {
Meta::Require(value) => ControlFlow::Continue(Some(value)),
Meta::Group(_) => {
if let Some(value) = state {
width += control(self.write((' ', '<', value, '>')))?;
}
ControlFlow::Continue(None)
}
Meta::Option(_) | Meta::Verb(_) => ControlFlow::Continue(None),
_ => ControlFlow::Continue(state),
},
) {
ControlFlow::Break(error) => Err(error),
ControlFlow::Continue(_) => Ok(width + self.write(suffix)?),
}
} else {
Ok(width)
}
}
fn columns(&self, metas: &[Meta], depth: usize) -> Columns {
let (mut short, mut long) = (false, false);
let mut columns = Columns::default();
for meta in Meta::visible(metas) {
match meta {
Meta::Position(position) if *position < 10 && depth == 0 => {
columns.short += 3 + if replace(&mut short, true) { 2 } else { 0 }
}
Meta::Position(_) if depth == 0 => {
columns.short += 4 + if replace(&mut short, true) { 2 } else { 0 }
}
Meta::Name(Name::Short, value) if depth == 0 => {
columns.short += value.len() + if replace(&mut short, true) { 2 } else { 0 }
}
Meta::Name(Name::Long, value) if depth == 0 => {
columns.long += value.len() + if replace(&mut long, true) { 2 } else { 0 }
}
Meta::Type(value) if depth == 0 => {
columns.types = value.len();
columns.types += self.style.begin(Item::Type).width();
columns.types += self.style.end(Item::Type).width();
}
Meta::Option(metas) | Meta::Verb(metas) | Meta::Group(metas) if depth > 0 => {
let child = self.columns(metas, depth - 1);
columns.short = columns.short.max(child.short);
columns.long = columns.long.max(child.long);
columns.types = columns.types.max(child.types);
}
_ => {}
}
}
columns
}
fn tags(&mut self, metas: &[Meta]) -> Result<usize, fmt::Error> {
let mut width = self.join(metas, "", "", ", ", |meta| match meta {
Meta::Require(_) => Some(Cow::Borrowed("require")),
Meta::Swizzle => Some(Cow::Borrowed("swizzle")),
Meta::Many(_) => Some(Cow::Borrowed("many")),
_ => None,
})?;
let prefix = if width > 0 { ", " } else { "" };
width += self.join(metas, (prefix, "valid: "), "", " | ", |meta| match meta {
Meta::Valid(value) => Some(Cow::Borrowed(value)),
_ => None,
})?;
let prefix = if width > 0 { ", " } else { "" };
width += self.join(metas, (prefix, "default: "), "", " | ", |meta| match meta {
Meta::Default(value) => Some(Cow::Borrowed(value)),
Meta::Environment(value) => Some(Cow::Owned(format!("${value}"))),
_ => None,
})?;
Ok(width)
}
fn node(&mut self, root: &Meta, metas: &[Meta], depth: usize) -> fmt::Result {
let columns = self.columns(metas, 1);
let mut helper = self.own();
for meta in Meta::visible(metas) {
match meta {
Meta::Help(value) => {
helper.indentation()?;
helper.wrap(
value,
helper.style.begin(Item::Help),
helper.style.end(Item::Help),
"",
&mut 0,
&mut false,
)?;
helper.write_line("")?;
}
Meta::Line => {
helper.write_line("")?;
}
Meta::Note(value) => {
helper.indentation()?;
helper.wrap(
value,
helper.style.begin(Item::Note),
helper.style.end(Item::Note),
"",
&mut 0,
&mut false,
)?;
helper.write_line("")?;
}
Meta::Group(metas) if depth == 0 => {
helper.write_header(root, metas)?;
helper.node(root, metas, depth + 1)?;
}
Meta::Group(metas) => {
helper.indentation()?;
let width = helper.names(
metas,
true,
true,
helper.style.begin(Item::Group),
helper.style.end(Item::Group),
)?;
if width > 0 {
helper.write_line("")?;
helper.indent().node(root, metas, depth + 1)?;
helper.write_line("")?;
} else {
helper.node(root, metas, depth + 1)?;
}
}
Meta::Verb(metas) if depth == 0 => {
helper.write_header(root, metas)?;
helper.node(root, metas, depth + 1)?;
}
Meta::Verb(metas) => {
helper.indentation()?;
let mut helper = helper.write_columns(metas, &columns, true)?;
helper.summary(
metas,
helper.style.begin(Item::Summary),
helper.style.end(Item::Summary),
helper.indent,
)?;
helper.write_line("")?;
}
Meta::Option(metas) => {
helper.indentation()?;
let mut helper = helper.write_columns(metas, &columns, false)?;
let mut width = helper.indent;
width += helper.summary(
metas,
helper.style.begin(Item::Summary),
helper.style.end(Item::Summary),
width,
)?;
let buffer = helper.scope(|mut helper| helper.tags(metas))?;
if width + buffer.len() > helper.style.width() {
helper.write_line("")?;
width = helper.indentation()?;
} else if width > helper.indent {
width += helper.write(" ")?;
}
helper.wrap(
&buffer,
helper.style.begin(Item::Tag),
helper.style.end(Item::Tag),
"",
&mut width,
&mut false,
)?;
helper.write_line("")?;
}
_ => {}
}
}
Ok(())
}
fn write_header(&mut self, root: &Meta, metas: &[Meta]) -> Result<usize, fmt::Error> {
let mut width = 0;
width += self.write_line(())?;
width += self.indentation()?;
let count = self.names(
metas,
true,
true,
(
'\n',
self.style.begin(Item::Bar(Line::Head)),
self.style.end(Item::Bar(Line::Head)),
' ',
self.style.begin(Item::Head),
),
self.style.end(Item::Head),
)?;
if count > 0 {
width += count;
width += self.versions(
metas,
(self.style.begin(Item::Version), ' '),
self.style.end(Item::Version),
)?;
width += self.authors(
metas,
(self.style.begin(Item::Author), " by "),
self.style.end(Item::Author),
)?;
width += self.write_line(())?;
}
let mut has = false;
let buffer = self.scope(|mut helper| {
let line = (
helper.style.begin(Item::Bar(Line::Description)),
helper.style.end(Item::Bar(Line::Description)),
helper.style.begin(Item::Arrow(Line::Description)),
helper.style.end(Item::Arrow(Line::Description)),
' ',
helper.style.begin(Item::Description),
);
let count = helper.description(
metas,
line,
helper.style.end(Item::Description),
(helper.style.end(Item::Description), line),
)?;
width += count;
has |= count > 0;
let count = helper.join(
metas,
(
'\n',
helper.style.begin(Item::Bar(Line::Link)),
helper.style.end(Item::Bar(Line::Link)),
helper.style.begin(Item::Arrow(Line::Link)),
helper.style.end(Item::Arrow(Line::Link)),
' ',
helper.style.begin(Item::Link),
),
helper.style.end(Item::Link),
" ",
|meta| match meta {
Meta::Home(value) => Some(Cow::Borrowed(value)),
_ => None,
},
)?;
width += count;
has |= count > 0;
let count = helper.join(
metas,
(
'\n',
helper.style.begin(Item::Bar(Line::Link)),
helper.style.end(Item::Bar(Line::Link)),
helper.style.begin(Item::Arrow(Line::Link)),
helper.style.end(Item::Arrow(Line::Link)),
' ',
helper.style.begin(Item::Link),
),
helper.style.end(Item::Link),
" ",
|meta| match meta {
Meta::Repository(value) => Some(Cow::Borrowed(value)),
_ => None,
},
)?;
width += count;
has |= count > 0;
let count = helper.usage(
root,
metas,
(
if has {
Or2::T0((
'\n',
helper.style.begin(Item::Bar(Line::Usage)),
helper.style.end(Item::Bar(Line::Usage)),
))
} else {
Or2::T1(())
},
'\n',
helper.style.begin(Item::Bar(Line::Usage)),
helper.style.end(Item::Bar(Line::Usage)),
helper.style.begin(Item::Arrow(Line::Usage)),
helper.style.end(Item::Arrow(Line::Usage)),
' ',
helper.style.begin(Item::Usage),
),
helper.style.end(Item::Usage),
)?;
width += count;
has |= count > 0;
Ok(())
})?;
if has {
width += self.write(self.style.begin(Item::Bar(Line::Head)))?;
width += self.write(self.style.end(Item::Bar(Line::Head)))?;
self.write_line(buffer)?;
}
self.write_line(())?;
Ok(width)
}
fn write_columns(
&mut self,
metas: &[Meta],
columns: &Columns,
verb: bool,
) -> Result<Helper<S>, fmt::Error> {
let item = if verb { Item::Verb } else { Item::Option };
let mut width = 0;
let pad = self.style.indent();
width += self.write_column(columns.short, pad, |helper| {
helper.names(
metas,
true,
false,
helper.style.begin(item),
helper.style.end(item),
)
})?;
width += self.write_column(columns.long, pad, |helper| {
helper.names(
metas,
false,
true,
helper.style.begin(item),
helper.style.end(item),
)
})?;
width += self.write_column(columns.types, pad, |helper| {
helper.types(
metas,
helper.style.begin(Item::Type),
helper.style.end(Item::Type),
)
})?;
Ok(self.indent_with(width))
}
#[inline]
fn write(&mut self, value: impl Format) -> Result<usize, fmt::Error> {
let width = value.width();
write!(self.buffer, "{}", Wrap(value))?;
Ok(width)
}
#[inline]
fn write_line(&mut self, value: impl Format) -> Result<usize, fmt::Error> {
let width = value.width();
writeln!(self.buffer, "{}", Wrap(value))?;
Ok(width)
}
fn write_column(
&mut self,
width: usize,
pad: usize,
write: impl FnOnce(&mut Self) -> Result<usize, fmt::Error>,
) -> Result<usize, fmt::Error> {
if width == 0 {
Ok(0)
} else {
let count = write(self)?;
Ok(count + self.space(width.saturating_sub(count) + pad)?)
}
}
}
pub(crate) fn help<S: Style + ?Sized>(
root: &Meta,
meta: &Meta,
path: &[Key],
style: &S,
) -> Option<String> {
let mut buffer = String::new();
let mut writer = Helper {
buffer: &mut buffer,
path,
style,
indent: 0,
};
writer.node(root, from_ref(meta), 0).ok()?;
Some(buffer)
}
pub(crate) fn version(meta: &Meta, depth: usize) -> Option<String> {
join(meta, depth, |meta| match meta {
Meta::Version(version) => Some(Cow::Borrowed(version)),
_ => None,
})
}
pub(crate) fn license(meta: &Meta, depth: usize) -> Option<String> {
join(meta, depth, |meta| match meta {
Meta::License(name, file) => match fs::read_to_string(file.deref()) {
Ok(content) => Some(Cow::Owned(content)),
Err(_) if file.chars().all(char::is_whitespace) => Some(Cow::Borrowed(name)),
Err(_) => Some(Cow::Borrowed(file)),
},
_ => None,
})
}
pub(crate) fn author(meta: &Meta, depth: usize) -> Option<String> {
join(meta, depth, |meta| match meta {
Meta::Author(author) => Some(Cow::Borrowed(author)),
_ => None,
})
}
fn join(meta: &Meta, depth: usize, find: impl Fn(&Meta) -> Option<Cow<str>>) -> Option<String> {
fn descend(
meta: &Meta,
depth: usize,
buffer: &mut String,
find: impl Fn(&Meta) -> Option<Cow<str>> + Copy,
) -> fmt::Result {
match meta {
Meta::Option(metas) | Meta::Verb(metas) | Meta::Group(metas) if depth > 0 => {
for meta in metas {
descend(meta, depth - 1, buffer, find)?;
}
}
meta => match find(meta) {
Some(value) if buffer.is_empty() => write!(buffer, "{value}")?,
Some(value) => write!(buffer, ", {value}")?,
None => {}
},
}
Ok(())
}
let mut buffer = String::new();
descend(meta, depth, &mut buffer, &find).ok()?;
Some(buffer)
}