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:
Robin Krahl 2020-09-15 23:15:16 +02:00
parent b7f5d3b855
commit 0bf2f15186
No known key found for this signature in database
GPG Key ID: 8E9B0870524F69D8
5 changed files with 164 additions and 168 deletions

View File

@ -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))
}

View File

@ -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))
}
}

View File

@ -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())
}
}

View File

@ -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())
}
}

View File

@ -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 {