Refactor text viewers into ManRenderer
This patch introduces the ManRenderer trait that can be used by viewers that render documentation in a man-like style. This is a more general version of the previous Printer trait.
This commit is contained in:
parent
b7f5d3b855
commit
0bf2f15186
@ -23,8 +23,8 @@ pub trait Viewer: fmt::Debug {
|
||||
|
||||
pub fn get_viewer(s: &str) -> anyhow::Result<Box<dyn Viewer>> {
|
||||
let viewer: Box<dyn Viewer> = match s.to_lowercase().as_ref() {
|
||||
"plain" => Box::new(text::TextViewer::with_plain_text()),
|
||||
"rich" => Box::new(text::TextViewer::with_rich_text()),
|
||||
"plain" => Box::new(text::TextViewer::new(text::TextMode::Plain)),
|
||||
"rich" => Box::new(text::TextViewer::new(text::TextMode::Rich)),
|
||||
_ => anyhow::bail!("The viewer {} is not supported", s),
|
||||
};
|
||||
Ok(viewer)
|
||||
@ -33,9 +33,10 @@ pub fn get_viewer(s: &str) -> anyhow::Result<Box<dyn Viewer>> {
|
||||
pub fn get_default() -> Box<dyn Viewer> {
|
||||
use crossterm::tty::IsTty;
|
||||
|
||||
if io::stdout().is_tty() {
|
||||
Box::new(text::TextViewer::with_rich_text())
|
||||
let text_mode = if io::stdout().is_tty() {
|
||||
text::TextMode::Rich
|
||||
} else {
|
||||
Box::new(text::TextViewer::with_plain_text())
|
||||
}
|
||||
text::TextMode::Plain
|
||||
};
|
||||
Box::new(text::TextViewer::new(text_mode))
|
||||
}
|
||||
|
@ -4,72 +4,45 @@
|
||||
mod plain;
|
||||
mod rich;
|
||||
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use std::marker;
|
||||
|
||||
use crate::args;
|
||||
use crate::doc;
|
||||
use crate::viewer;
|
||||
|
||||
pub trait Printer: fmt::Debug + marker::Sized {
|
||||
fn new(args: args::ViewerArgs) -> anyhow::Result<Self>;
|
||||
|
||||
fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()>;
|
||||
|
||||
fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()>;
|
||||
|
||||
fn print_html(&self, indent: usize, s: &doc::Text, show_links: bool) -> io::Result<()>;
|
||||
|
||||
fn print_code(&self, indent: usize, code: &doc::Code) -> io::Result<()>;
|
||||
|
||||
fn println(&self) -> io::Result<()>;
|
||||
}
|
||||
use crate::viewer::{self, utils};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextViewer<P: Printer> {
|
||||
_printer: marker::PhantomData<P>,
|
||||
pub struct TextViewer {
|
||||
mode: TextMode,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct TextRenderer<P: Printer> {
|
||||
printer: P,
|
||||
#[derive(Copy, Clone, Debug, Eq, Ord, PartialEq, PartialOrd)]
|
||||
pub enum TextMode {
|
||||
Plain,
|
||||
Rich,
|
||||
}
|
||||
|
||||
impl<P: Printer> TextViewer<P> {
|
||||
fn new() -> Self {
|
||||
TextViewer {
|
||||
_printer: Default::default(),
|
||||
}
|
||||
impl TextViewer {
|
||||
pub fn new(mode: TextMode) -> Self {
|
||||
TextViewer { mode }
|
||||
}
|
||||
|
||||
fn exec<F>(&self, args: args::ViewerArgs, op: F) -> anyhow::Result<()>
|
||||
where
|
||||
F: FnOnce(&TextRenderer<P>) -> io::Result<()>,
|
||||
F: FnOnce(Box<dyn utils::ManRenderer<Error = io::Error>>) -> io::Result<()>,
|
||||
{
|
||||
let viewer: Box<dyn utils::ManRenderer<Error = io::Error>> = match self.mode {
|
||||
TextMode::Plain => Box::new(plain::PlainTextRenderer::new(args)),
|
||||
TextMode::Rich => Box::new(rich::RichTextRenderer::new(args)?),
|
||||
};
|
||||
|
||||
spawn_pager();
|
||||
|
||||
let printer = P::new(args)?;
|
||||
let renderer = TextRenderer::new(printer);
|
||||
op(&renderer).or_else(ignore_pipe_error).map_err(Into::into)
|
||||
op(viewer).or_else(ignore_pipe_error).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl TextViewer<plain::PlainTextRenderer> {
|
||||
pub fn with_plain_text() -> Self {
|
||||
TextViewer::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl TextViewer<rich::RichTextRenderer> {
|
||||
pub fn with_rich_text() -> Self {
|
||||
TextViewer::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Printer> viewer::Viewer for TextViewer<P> {
|
||||
impl viewer::Viewer for TextViewer {
|
||||
fn open(&self, args: args::ViewerArgs, doc: &doc::Doc) -> anyhow::Result<()> {
|
||||
self.exec(args, |r| r.print_doc(doc))
|
||||
self.exec(args, |mut viewer| viewer.render_doc(doc))
|
||||
}
|
||||
|
||||
fn open_examples(
|
||||
@ -78,96 +51,7 @@ impl<P: Printer> viewer::Viewer for TextViewer<P> {
|
||||
doc: &doc::Doc,
|
||||
examples: Vec<doc::Example>,
|
||||
) -> anyhow::Result<()> {
|
||||
self.exec(args, |r| r.print_examples(doc, examples))
|
||||
}
|
||||
}
|
||||
|
||||
impl<P: Printer> TextRenderer<P> {
|
||||
pub fn new(printer: P) -> Self {
|
||||
Self { printer }
|
||||
}
|
||||
|
||||
fn print_doc(&self, doc: &doc::Doc) -> io::Result<()> {
|
||||
self.print_title(doc)?;
|
||||
|
||||
if let Some(text) = &doc.definition {
|
||||
self.print_heading(1, "Synopsis")?;
|
||||
self.printer.print_code(6, text)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
|
||||
if let Some(text) = &doc.description {
|
||||
self.print_heading(1, "Description")?;
|
||||
self.printer.print_html(6, text, true)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
|
||||
for (ty, groups) in &doc.groups {
|
||||
self.print_heading(1, ty.group_name())?;
|
||||
|
||||
for group in groups {
|
||||
if let Some(title) = &group.title {
|
||||
self.print_heading(2, title)?;
|
||||
}
|
||||
|
||||
for member in &group.members {
|
||||
// TODO: use something link strip_prefix instead of last()
|
||||
self.print_heading(3, member.name.last())?;
|
||||
if let Some(definition) = &member.definition {
|
||||
self.printer.print_code(12, definition)?;
|
||||
}
|
||||
if member.definition.is_some() && member.description.is_some() {
|
||||
self.printer.println()?;
|
||||
}
|
||||
if let Some(description) = &member.description {
|
||||
self.printer.print_html(12, description, true)?;
|
||||
}
|
||||
if member.definition.is_some() || member.description.is_some() {
|
||||
self.printer.println()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> io::Result<()> {
|
||||
self.print_title(doc)?;
|
||||
self.print_heading(1, "Examples")?;
|
||||
|
||||
let n = examples.len();
|
||||
for (i, example) in examples.iter().enumerate() {
|
||||
if n > 1 {
|
||||
self.print_heading(2, &format!("Example {} of {}", i + 1, n))?;
|
||||
}
|
||||
if let Some(description) = &example.description {
|
||||
self.printer.print_html(6, description, true)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
self.printer.print_code(6, &example.code)?;
|
||||
self.printer.println()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_title(&self, doc: &doc::Doc) -> io::Result<()> {
|
||||
let title = format!("{} {}", doc.ty.name(), doc.name.as_ref());
|
||||
self.printer
|
||||
.print_title(doc.name.krate(), &title, "rusty-man")
|
||||
}
|
||||
|
||||
fn print_heading(&self, level: usize, s: &str) -> io::Result<()> {
|
||||
let text = match level {
|
||||
1 => std::borrow::Cow::from(s.to_uppercase()),
|
||||
_ => std::borrow::Cow::from(s),
|
||||
};
|
||||
let indent = match level {
|
||||
1 => 0,
|
||||
2 => 3,
|
||||
_ => 6,
|
||||
};
|
||||
self.printer.print_heading(indent, level, text.as_ref())
|
||||
self.exec(args, |mut viewer| viewer.render_examples(doc, &examples))
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -20,42 +20,46 @@ struct Decorator {
|
||||
show_links: bool,
|
||||
}
|
||||
|
||||
impl super::Printer for PlainTextRenderer {
|
||||
fn new(args: args::ViewerArgs) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
impl PlainTextRenderer {
|
||||
pub fn new(args: args::ViewerArgs) -> Self {
|
||||
Self {
|
||||
line_length: utils::get_line_length(&args),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()> {
|
||||
impl utils::ManRenderer for PlainTextRenderer {
|
||||
type Error = io::Error;
|
||||
|
||||
fn print_title(&mut self, left: &str, middle: &str, right: &str) -> io::Result<()> {
|
||||
super::print_title(self.line_length, left, middle, right)?;
|
||||
writeln!(io::stdout())
|
||||
}
|
||||
|
||||
fn print_html(&self, indent: usize, s: &doc::Text, show_links: bool) -> io::Result<()> {
|
||||
fn print_text(&mut self, indent: u8, s: &doc::Text) -> io::Result<()> {
|
||||
let lines = html2text::from_read_with_decorator(
|
||||
s.html.as_bytes(),
|
||||
self.line_length - indent,
|
||||
Decorator::new(show_links),
|
||||
self.line_length - usize::from(indent),
|
||||
Decorator::new(true),
|
||||
);
|
||||
for line in lines.trim().split('\n') {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?;
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent.into()), line)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_code(&self, indent: usize, code: &doc::Code) -> io::Result<()> {
|
||||
fn print_code(&mut self, indent: u8, code: &doc::Code) -> io::Result<()> {
|
||||
for line in code.split('\n') {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?;
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent.into()), line)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_heading(&self, indent: usize, _level: usize, s: &str) -> io::Result<()> {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), s)
|
||||
fn print_heading(&mut self, indent: u8, s: &str) -> io::Result<()> {
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent.into()), s)
|
||||
}
|
||||
|
||||
fn println(&self) -> io::Result<()> {
|
||||
fn println(&mut self) -> io::Result<()> {
|
||||
writeln!(io::stdout())
|
||||
}
|
||||
}
|
||||
|
@ -19,8 +19,8 @@ pub struct RichTextRenderer {
|
||||
theme: syntect::highlighting::Theme,
|
||||
}
|
||||
|
||||
impl super::Printer for RichTextRenderer {
|
||||
fn new(args: args::ViewerArgs) -> anyhow::Result<Self> {
|
||||
impl RichTextRenderer {
|
||||
pub fn new(args: args::ViewerArgs) -> anyhow::Result<Self> {
|
||||
Ok(Self {
|
||||
line_length: utils::get_line_length(&args),
|
||||
highlight: !args.no_syntax_highlight,
|
||||
@ -28,14 +28,19 @@ impl super::Printer for RichTextRenderer {
|
||||
theme: utils::get_syntect_theme(&args)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()> {
|
||||
impl utils::ManRenderer for RichTextRenderer {
|
||||
type Error = io::Error;
|
||||
|
||||
fn print_title(&mut self, left: &str, middle: &str, right: &str) -> io::Result<()> {
|
||||
write!(io::stdout(), "{}", crossterm::style::Attribute::Bold)?;
|
||||
super::print_title(self.line_length, left, middle, right)?;
|
||||
writeln!(io::stdout(), "{}", crossterm::style::Attribute::Reset)
|
||||
}
|
||||
|
||||
fn print_html(&self, indent: usize, s: &doc::Text, _show_links: bool) -> io::Result<()> {
|
||||
fn print_text(&mut self, indent: u8, s: &doc::Text) -> io::Result<()> {
|
||||
let indent = usize::from(indent);
|
||||
let indent = if indent >= self.line_length / 2 {
|
||||
0
|
||||
} else {
|
||||
@ -54,7 +59,8 @@ impl super::Printer for RichTextRenderer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_code(&self, indent: usize, code: &doc::Code) -> io::Result<()> {
|
||||
fn print_code(&mut self, indent: u8, code: &doc::Code) -> io::Result<()> {
|
||||
let indent = usize::from(indent);
|
||||
if self.highlight {
|
||||
let syntax = self.syntax_set.find_syntax_by_extension("rs").unwrap();
|
||||
let mut h = syntect::easy::HighlightLines::new(syntax, &self.theme);
|
||||
@ -77,16 +83,14 @@ impl super::Printer for RichTextRenderer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()> {
|
||||
fn print_heading(&mut self, indent: u8, s: &str) -> io::Result<()> {
|
||||
use crossterm::style::Attribute;
|
||||
let mut text = crossterm::style::style(s);
|
||||
if level < 4 {
|
||||
use crossterm::style::Attribute;
|
||||
text = text.attribute(Attribute::Bold).attribute(Attribute::Reset);
|
||||
}
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(indent), &text)
|
||||
text = text.attribute(Attribute::Bold).attribute(Attribute::Reset);
|
||||
writeln!(io::stdout(), "{}{}", " ".repeat(usize::from(indent)), &text)
|
||||
}
|
||||
|
||||
fn println(&self) -> io::Result<()> {
|
||||
fn println(&mut self) -> io::Result<()> {
|
||||
writeln!(io::stdout())
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,109 @@ use std::cmp;
|
||||
use anyhow::Context as _;
|
||||
|
||||
use crate::args;
|
||||
use crate::doc;
|
||||
|
||||
/// A trait for viewer implementations that display the documentation in a man-like style.
|
||||
pub trait ManRenderer {
|
||||
type Error: std::error::Error + Sized + Send;
|
||||
|
||||
fn print_title(&mut self, left: &str, center: &str, right: &str) -> Result<(), Self::Error>;
|
||||
fn print_heading(&mut self, indent: u8, text: &str) -> Result<(), Self::Error>;
|
||||
fn print_code(&mut self, indent: u8, code: &doc::Code) -> Result<(), Self::Error>;
|
||||
fn print_text(&mut self, indent: u8, text: &doc::Text) -> Result<(), Self::Error>;
|
||||
fn println(&mut self) -> Result<(), Self::Error>;
|
||||
|
||||
fn render_doc(&mut self, doc: &doc::Doc) -> Result<(), Self::Error> {
|
||||
print_title(self, doc)?;
|
||||
|
||||
if let Some(text) = &doc.definition {
|
||||
print_heading(self, 1, "Synopsis")?;
|
||||
self.print_code(6, text)?;
|
||||
self.println()?;
|
||||
}
|
||||
|
||||
if let Some(text) = &doc.description {
|
||||
print_heading(self, 1, "Description")?;
|
||||
self.print_text(6, text)?;
|
||||
self.println()?;
|
||||
}
|
||||
|
||||
for (ty, groups) in &doc.groups {
|
||||
print_heading(self, 1, ty.group_name())?;
|
||||
|
||||
for group in groups {
|
||||
if let Some(title) = &group.title {
|
||||
print_heading(self, 2, title)?;
|
||||
}
|
||||
|
||||
for member in &group.members {
|
||||
// TODO: use something link strip_prefix instead of last()
|
||||
print_heading(self, 3, member.name.last())?;
|
||||
if let Some(definition) = &member.definition {
|
||||
self.print_code(12, definition)?;
|
||||
}
|
||||
if member.definition.is_some() && member.description.is_some() {
|
||||
self.println()?;
|
||||
}
|
||||
if let Some(description) = &member.description {
|
||||
self.print_text(12, description)?;
|
||||
}
|
||||
if member.definition.is_some() || member.description.is_some() {
|
||||
self.println()?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_examples(
|
||||
&mut self,
|
||||
doc: &doc::Doc,
|
||||
examples: &[doc::Example],
|
||||
) -> Result<(), Self::Error> {
|
||||
print_title(self, doc)?;
|
||||
print_heading(self, 1, "Examples")?;
|
||||
|
||||
let n = examples.len();
|
||||
for (i, example) in examples.iter().enumerate() {
|
||||
if n > 1 {
|
||||
print_heading(self, 2, &format!("Example {} of {}", i + 1, n))?;
|
||||
}
|
||||
if let Some(description) = &example.description {
|
||||
self.print_text(6, description)?;
|
||||
self.println()?;
|
||||
}
|
||||
self.print_code(6, &example.code)?;
|
||||
self.println()?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn print_title<M: ManRenderer + ?Sized>(viewer: &mut M, doc: &doc::Doc) -> Result<(), M::Error> {
|
||||
let title = format!("{} {}", doc.ty.name(), doc.name);
|
||||
viewer.print_title(doc.name.krate(), &title, "rusty-man")
|
||||
}
|
||||
|
||||
fn print_heading<M: ManRenderer + ?Sized>(
|
||||
viewer: &mut M,
|
||||
level: u8,
|
||||
text: &str,
|
||||
) -> Result<(), M::Error> {
|
||||
let text = match level {
|
||||
1 => std::borrow::Cow::from(text.to_uppercase()),
|
||||
_ => std::borrow::Cow::from(text),
|
||||
};
|
||||
let indent = match level {
|
||||
1 => 0,
|
||||
2 => 3,
|
||||
_ => 6,
|
||||
};
|
||||
viewer.print_heading(indent, text.as_ref())
|
||||
}
|
||||
|
||||
pub fn get_line_length(args: &args::ViewerArgs) -> usize {
|
||||
if let Some(width) = args.width {
|
||||
|
Loading…
Reference in New Issue
Block a user