From fc7d2c7ee9c73072da1d095e0b01e55dcde6a05c Mon Sep 17 00:00:00 2001 From: Robin Krahl Date: Fri, 24 Jul 2020 12:09:40 +0200 Subject: [PATCH] Format text output look like man MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit With this patch, we change our rich and plain viewer implementations to format their output similar to the output of man. This means: - Adding a title with the current crate, documentation item and “rusty-man” - Printing headings at indent levels 0, 3, 6 and printing content with indent 6, 12 - Printing headings bold and uppercase (level 1) or bold (levels 2, 3) --- src/parser.rs | 4 +- src/viewer/text/mod.rs | 107 ++++++++++++++++++++++++--------------- src/viewer/text/plain.rs | 40 +++++++++------ src/viewer/text/rich.rs | 32 +++++++----- 4 files changed, 111 insertions(+), 72 deletions(-) diff --git a/src/parser.rs b/src/parser.rs index d16e683..973fc6c 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -205,7 +205,7 @@ fn get_methods( while let Some(subheading) = &next { if is_element(subheading, &local_name!("h3")) && has_class(subheading, "impl") { if let Some(title_element) = subheading.first_child() { - let title = get_html(&title_element)?; + let title = title_element.text_contents(); next = subheading.next_sibling(); if let Some(impl_items) = &next { if is_element(impl_items, &local_name!("div")) @@ -234,7 +234,7 @@ fn get_methods( let heading = select_first(document, "#deref-methods")?; if let Some(heading) = heading { - let title = get_html(heading.as_node())?; + let title = heading.as_node().text_contents(); if let Some(impl_items) = heading.as_node().next_sibling() { let group = get_method_group( parent, diff --git a/src/viewer/text/mod.rs b/src/viewer/text/mod.rs index 8a5c2e0..caca73b 100644 --- a/src/viewer/text/mod.rs +++ b/src/viewer/text/mod.rs @@ -11,9 +11,11 @@ use crate::doc; use crate::viewer; pub trait Printer: fmt::Debug { - fn print_heading(&self, level: usize, s: &str) -> io::Result<()>; + fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()>; - fn print_html(&self, s: &str) -> io::Result<()>; + fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()>; + + fn print_html(&self, indent: usize, s: &str, show_links: bool) -> io::Result<()>; fn println(&self) -> io::Result<()>; } @@ -29,41 +31,33 @@ impl TextViewer

{ } fn print_doc(&self, doc: &doc::Doc) -> io::Result<()> { + let title = format!("{} {}", doc.ty.name(), doc.name.as_ref()); self.printer - .print_heading(1, &format!("{} {}", doc.ty.name(), doc.name.as_ref()))?; - self.print_opt(doc.definition.as_deref())?; - self.print_opt(doc.description.as_deref())?; + .print_title(doc.name.krate(), &title, "rusty-man")?; + self.print_opt("SYNOPSIS", doc.definition.as_deref(), false)?; + self.print_opt("DESCRIPTION", doc.description.as_deref(), true)?; for (ty, groups) in &doc.groups { - self.printer.println()?; - self.printer.print_heading(2, ty.group_name())?; + self.print_heading(1, ty.group_name())?; for group in groups { if let Some(title) = &group.title { - self.printer.println()?; - self.printer.print_heading(3, title)?; + self.print_heading(2, title)?; } - if doc.ty == doc::ItemType::Module { - self.print_list(group.members.iter().map(|i| { - if let Some(description) = &i.description { - format!("{}
{}", i.name.last(), description) - } else { - i.name.last().to_owned() - } - }))?; - } else { - for member in &group.members { + 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_html(12, definition, false)?; + } + 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()?; - self.printer.print_heading(4, member.name.last())?; - if let Some(definition) = &member.definition { - self.printer.print_html(definition)?; - } - if member.definition.is_some() && member.description.is_some() { - self.printer.println()?; - } - if let Some(description) = &member.description { - self.printer.print_html(description)?; - } } } } @@ -71,25 +65,27 @@ impl TextViewer

{ Ok(()) } - fn print_opt(&self, s: Option<&str>) -> io::Result<()> { + fn print_opt(&self, title: &str, s: Option<&str>, show_links: bool) -> io::Result<()> { if let Some(s) = s { - self.printer.println()?; - self.printer.print_html(s) + self.print_heading(1, title)?; + self.printer.print_html(6, s, show_links)?; + self.printer.println() } else { Ok(()) } } - fn print_list(&self, items: I) -> io::Result<()> - where - I: Iterator, - D: fmt::Display, - { - let html = items - .map(|i| format!("

  • {}
  • ", i)) - .collect::>() - .join(""); - self.printer.print_html(&format!("
      {}
    ", html)) + 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()) } } @@ -129,3 +125,30 @@ fn ignore_pipe_error(error: io::Error) -> io::Result<()> { Err(error) } } + +pub fn print_title(line_length: usize, left: &str, middle: &str, right: &str) -> io::Result<()> { + use io::Write; + + write!(io::stdout(), "{}", left)?; + + let mut idx = left.len(); + let middle_idx = line_length / 2; + let offset = middle.len() / 2; + + let spacing = if idx + offset >= middle_idx { + 1 + } else { + middle_idx - offset - idx + }; + write!(io::stdout(), "{}{}", " ".repeat(spacing), middle)?; + idx += middle.len() + spacing; + + let end_idx = line_length; + let offset = right.len(); + let spacing = if idx + offset >= end_idx { + 1 + } else { + end_idx - offset - idx + }; + writeln!(io::stdout(), "{}{}", " ".repeat(spacing), right) +} diff --git a/src/viewer/text/plain.rs b/src/viewer/text/plain.rs index 63aee63..da83469 100644 --- a/src/viewer/text/plain.rs +++ b/src/viewer/text/plain.rs @@ -15,6 +15,7 @@ pub struct PlainTextRenderer { struct Decorator { links: Vec, ignore_next_link: bool, + show_links: bool, } impl PlainTextRenderer { @@ -26,20 +27,25 @@ impl PlainTextRenderer { } impl super::Printer for PlainTextRenderer { - fn print_html(&self, s: &str) -> io::Result<()> { - writeln!( - io::stdout(), - "{}", - html2text::from_read_with_decorator(s.as_bytes(), self.line_length, Decorator::new()) - ) + fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()> { + super::print_title(self.line_length, left, middle, right)?; + writeln!(io::stdout()) } - fn print_heading(&self, level: usize, s: &str) -> io::Result<()> { - self.print_html(&format!( - "{text}", - level = level, - text = s - )) + fn print_html(&self, indent: usize, s: &str, show_links: bool) -> io::Result<()> { + let lines = html2text::from_read_with_decorator( + s.as_bytes(), + self.line_length - indent, + Decorator::new(show_links), + ); + for line in lines.trim().split('\n') { + writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?; + } + Ok(()) + } + + fn print_heading(&self, indent: usize, _level: usize, s: &str) -> io::Result<()> { + writeln!(io::stdout(), "{}{}", " ".repeat(indent), s) } fn println(&self) -> io::Result<()> { @@ -48,16 +54,18 @@ impl super::Printer for PlainTextRenderer { } impl Decorator { - pub fn new() -> Self { + pub fn new(show_links: bool) -> Self { Self { links: Vec::new(), ignore_next_link: false, + show_links, } } fn show_link(&self, url: &str) -> bool { - // only show absolute links -- local links are most likely not helpful - (url.starts_with("http") || url.starts_with("https")) && + self.show_links && + // only show absolute links -- local links are most likely not helpful + (url.starts_with("http") || url.starts_with("https")) && // ignore playground links -- typically, these links are too long to display in a // sensible fasshion !url.starts_with("http://play.rust-lang.org") && @@ -129,6 +137,6 @@ impl text_renderer::TextDecorator for Decorator { } fn make_subblock_decorator(&self) -> Self { - Decorator::new() + Decorator::new(self.show_links) } } diff --git a/src/viewer/text/rich.rs b/src/viewer/text/rich.rs index b58c887..d2dfdf9 100644 --- a/src/viewer/text/rich.rs +++ b/src/viewer/text/rich.rs @@ -29,9 +29,21 @@ impl RichTextRenderer { } impl super::Printer for RichTextRenderer { - fn print_html(&self, s: &str) -> io::Result<()> { - let lines = html2text::from_read_rich(s.as_bytes(), self.line_length); + fn print_title(&self, left: &str, middle: &str, right: &str) -> io::Result<()> { + write!(io::stdout(), "{}", termion::style::Bold)?; + super::print_title(self.line_length, left, middle, right)?; + writeln!(io::stdout(), "{}", termion::style::Reset) + } + + fn print_html(&self, indent: usize, s: &str, _show_links: bool) -> io::Result<()> { + let indent = if indent >= self.line_length / 2 { + 0 + } else { + indent + }; + let lines = html2text::from_read_rich(s.as_bytes(), self.line_length - indent); for line in lines { + write!(io::stdout(), "{}", " ".repeat(indent))?; for element in line.iter() { if let text_renderer::TaggedLineElement::Str(ts) = element { self.render_string(ts)?; @@ -42,16 +54,12 @@ impl super::Printer for RichTextRenderer { Ok(()) } - fn print_heading(&self, level: usize, s: &str) -> io::Result<()> { - if level < 4 { - write!(io::stdout(), "{}", termion::style::Bold)?; - } - let heading = format!("{text}", level = level, text = s); - self.print_html(&heading)?; - if level < 4 { - write!(io::stdout(), "{}", termion::style::Reset)?; - } - Ok(()) + fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()> { + let text = match level { + 1..=3 => format!("{}{}{}", termion::style::Bold, s, termion::style::Reset), + _ => s.to_owned(), + }; + writeln!(io::stdout(), "{}{}", " ".repeat(indent), &text) } fn println(&self) -> io::Result<()> {