Refactor viewer module

This patch refactors the viewer module:
- The `text` option is renamed to `plain`.
- The TextViewer and RichViewer structs are replaced by a new TextViewer
  struct that handles the basic documentation structure and uses a
  Printer implementation for formatting.
- The new structs PlainTextRenderer and RichTextRenderer implement the
  Printer trait.
- The Printer trait and the TextViewer struct are placed in the
  viewer::text module.  The Printer implementations are placed in the
  plain and rich submodules.

This reduces code duplication and makes it easier to render more complex
documentation items.
This commit is contained in:
Robin Krahl 2020-07-21 21:48:24 +02:00
parent 06851357a2
commit a97c9e5a0f
No known key found for this signature in database
GPG Key ID: 8E9B0870524F69D8
5 changed files with 154 additions and 134 deletions

View File

@ -20,9 +20,9 @@
//! `index` module.
//!
//! If we found a documentation item, we use a viewer to open it see the `viewer` module.
//! Currently, there are two viewer implementations: `viewer::text::TextViewer` converts the
//! documentaion to plain text, `viewer::text::RichViewer` adds some formatting to it. Both
//! viewers pipe their output through a pager, if available.
//! Currently, there are two viewer implementations: `PlainTextViewer` converts the documentaion
//! to plain text, `RichTextViewer` adds some formatting to it. Both viewers pipe their output
//! through a pager, if available.
//!
//! The documentation is scraped from the HTML files generated by `rustdoc`. See the `parser`
//! module for the scraping and the `doc::Doc` struct for the structure of the documentation items.
@ -57,7 +57,7 @@ struct Opt {
#[structopt(name = "source", short, long, number_of_values = 1)]
source_paths: Vec<String>,
/// The viewer for the rustdoc documentation
/// The viewer for the rustdoc documentation (one of: plain, rich)
#[structopt(long, parse(try_from_str = viewer::get_viewer))]
viewer: Option<Box<dyn viewer::Viewer>>,

View File

@ -1,7 +1,6 @@
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
mod rich;
mod text;
use std::cmp;
@ -15,25 +14,22 @@ pub trait Viewer: fmt::Debug {
}
pub fn get_viewer(s: &str) -> anyhow::Result<Box<dyn Viewer>> {
match s.to_lowercase().as_ref() {
"rich" => Ok(Box::new(rich::RichViewer::new())),
"text" => Ok(Box::new(text::TextViewer::new())),
_ => Err(anyhow::anyhow!("The viewer {} is not supported", s)),
}
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()),
_ => anyhow::bail!("The viewer {} is not supported", s),
};
Ok(viewer)
}
pub fn get_default() -> Box<dyn Viewer> {
if termion::is_tty(&io::stdout()) {
Box::new(rich::RichViewer::new())
Box::new(text::TextViewer::with_rich_text())
} else {
Box::new(text::TextViewer::new())
Box::new(text::TextViewer::with_plain_text())
}
}
pub fn spawn_pager() {
pager::Pager::with_default_pager("less -cr").setup()
}
pub fn get_line_length() -> usize {
if let Ok((cols, _)) = termion::terminal_size() {
cmp::min(cols.into(), 100)

106
src/viewer/text/mod.rs Normal file
View File

@ -0,0 +1,106 @@
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
mod plain;
mod rich;
use std::fmt;
use std::io;
use crate::doc;
use crate::viewer;
pub trait Printer: fmt::Debug {
fn print_heading(&self, level: usize, s: &str) -> io::Result<()>;
fn print_html(&self, s: &str) -> io::Result<()>;
fn println(&self) -> io::Result<()>;
}
#[derive(Clone, Debug)]
pub struct TextViewer<P: Printer> {
printer: P,
}
impl<P: Printer> TextViewer<P> {
pub fn new(printer: P) -> Self {
Self { printer }
}
fn print_doc(&self, doc: &doc::Doc) -> io::Result<()> {
if let Some(title) = &doc.title {
self.printer.print_heading(1, title)?;
} else {
self.printer.print_heading(1, doc.name.as_ref())?;
}
self.print_opt(doc.definition.as_deref())?;
self.print_opt(doc.description.as_deref())?;
for (heading, items) in &doc.members {
if !items.is_empty() {
self.printer.println()?;
self.printer.print_heading(2, heading)?;
self.print_list(items.iter())?;
}
}
Ok(())
}
fn print_opt(&self, s: Option<&str>) -> io::Result<()> {
if let Some(s) = s {
self.printer.println()?;
self.printer.print_html(s)
} else {
Ok(())
}
}
fn print_list<I, D>(&self, items: I) -> io::Result<()>
where
I: Iterator<Item = D>,
D: fmt::Display,
{
let html = items
.map(|i| format!("<li>{}</li>", i))
.collect::<Vec<_>>()
.join("");
self.printer.print_html(&format!("<ul>{}</ul>", html))
}
}
impl TextViewer<plain::PlainTextRenderer> {
pub fn with_plain_text() -> Self {
Self::new(plain::PlainTextRenderer::new())
}
}
impl TextViewer<rich::RichTextRenderer> {
pub fn with_rich_text() -> Self {
Self::new(rich::RichTextRenderer::new())
}
}
impl<P: Printer> viewer::Viewer for TextViewer<P> {
fn open(&self, doc: &doc::Doc) -> anyhow::Result<()> {
spawn_pager();
self.print_doc(doc)
.or_else(ignore_pipe_error)
.map_err(Into::into)
}
}
pub fn spawn_pager() {
pager::Pager::with_default_pager("less -cr").setup()
}
fn ignore_pipe_error(error: io::Error) -> io::Result<()> {
// If the pager is terminated before we can write everything to stdout, we will receive a
// BrokenPipe error. But we dont want to report this error to the user. See also:
// https://github.com/rust-lang/rust/issues/46016
if error.kind() == io::ErrorKind::BrokenPipe {
Ok(())
} else {
Err(error)
}
}

View File

@ -1,15 +1,14 @@
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
use std::fmt;
use std::io::{self, Write};
use html2text::render::text_renderer;
use crate::doc;
use crate::viewer;
#[derive(Clone, Debug)]
pub struct TextViewer {
pub struct PlainTextRenderer {
line_length: usize,
}
@ -18,61 +17,33 @@ struct Decorator {
ignore_next_link: bool,
}
impl TextViewer {
impl PlainTextRenderer {
pub fn new() -> Self {
Self {
line_length: viewer::get_line_length(),
}
}
fn print(&self, s: &str) {
println!(
"{}",
html2text::from_read_with_decorator(s.as_bytes(), self.line_length, Decorator::new())
);
}
fn print_opt(&self, s: Option<&str>) {
if let Some(s) = s {
self.print(s);
}
}
fn print_heading(&self, s: &str, level: usize) {
let prefix = "#".repeat(level);
print!("{} ", prefix);
self.print(s);
}
fn print_list(&self, items: &[impl fmt::Display]) {
let html = items
.iter()
.map(|i| format!("<li>{}</li>", i))
.collect::<Vec<_>>()
.join("");
self.print(&format!("<ul>{}</ul>", html));
}
}
impl viewer::Viewer for TextViewer {
fn open(&self, doc: &doc::Doc) -> anyhow::Result<()> {
viewer::spawn_pager();
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())
)
}
if let Some(title) = &doc.title {
self.print_heading(title, 1);
} else {
self.print_heading(doc.name.as_ref(), 1);
}
self.print_opt(doc.definition.as_deref());
self.print_opt(doc.description.as_deref());
for (heading, items) in &doc.members {
if !items.is_empty() {
println!();
self.print_heading(heading, 2);
self.print_list(items);
}
}
Ok(())
fn print_heading(&self, level: usize, s: &str) -> io::Result<()> {
self.print_html(&format!(
"<h{level}>{text}</h{level}>",
level = level,
text = s
))
}
fn println(&self) -> io::Result<()> {
writeln!(io::stdout())
}
}

View File

@ -1,47 +1,35 @@
// SPDX-FileCopyrightText: 2020 Robin Krahl <robin.krahl@ireas.org>
// SPDX-License-Identifier: MIT
use std::fmt;
use std::io::{self, Write};
use html2text::render::text_renderer;
use crate::doc;
use crate::viewer;
type RichString = text_renderer::TaggedString<Vec<text_renderer::RichAnnotation>>;
#[derive(Clone, Debug)]
pub struct RichViewer {
pub struct RichTextRenderer {
line_length: usize,
}
impl RichViewer {
impl RichTextRenderer {
pub fn new() -> Self {
Self {
line_length: viewer::get_line_length(),
}
}
fn print_doc(&self, doc: &doc::Doc) -> io::Result<()> {
if let Some(title) = &doc.title {
self.print_heading(title, 1)?;
} else {
self.print_heading(doc.name.as_ref(), 1)?;
}
self.print_opt(doc.definition.as_deref())?;
self.print_opt(doc.description.as_deref())?;
for (heading, items) in &doc.members {
if !items.is_empty() {
writeln!(io::stdout())?;
self.print_heading(heading, 2)?;
self.print_list(items)?;
}
}
Ok(())
fn render_string(&self, ts: &RichString) -> io::Result<()> {
let start_style = get_style(ts, get_start_style);
let end_style = get_style(ts, get_end_style);
write!(io::stdout(), "{}{}{}", start_style, ts.s, end_style)
}
}
fn print(&self, s: &str) -> io::Result<()> {
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);
for line in lines {
for element in line.iter() {
@ -54,56 +42,15 @@ impl RichViewer {
Ok(())
}
fn print_opt(&self, s: Option<&str>) -> io::Result<()> {
if let Some(s) = s {
writeln!(io::stdout())?;
self.print(s)
} else {
Ok(())
}
}
fn print_heading(&self, s: &str, level: usize) -> io::Result<()> {
let prefix = "#".repeat(level);
write!(io::stdout(), "{}{} ", termion::style::Bold, prefix)?;
self.print(s)?;
fn print_heading(&self, level: usize, s: &str) -> io::Result<()> {
write!(io::stdout(), "{}", termion::style::Bold)?;
let heading = format!("<h{level}>{text}</h{level}>", level = level, text = s);
self.print_html(&heading)?;
write!(io::stdout(), "{}", termion::style::Reset)
}
fn print_list(&self, items: &[impl fmt::Display]) -> io::Result<()> {
let html = items
.iter()
.map(|i| format!("<li>{}</li>", i))
.collect::<Vec<_>>()
.join("");
self.print(&format!("<ul>{}</ul>", html))
}
fn render_string(&self, ts: &RichString) -> io::Result<()> {
let start_style = get_style(ts, get_start_style);
let end_style = get_style(ts, get_end_style);
write!(io::stdout(), "{}{}{}", start_style, ts.s, end_style)
}
}
impl viewer::Viewer for RichViewer {
fn open(&self, doc: &doc::Doc) -> anyhow::Result<()> {
viewer::spawn_pager();
self.print_doc(doc)
.or_else(ignore_pipe_error)
.map_err(Into::into)
}
}
fn ignore_pipe_error(error: io::Error) -> io::Result<()> {
// If the pager is terminated before we can write everything to stdout, we will receive a
// BrokenPipe error. But we dont want to report this error to the user. See also:
// https://github.com/rust-lang/rust/issues/46016
if error.kind() == io::ErrorKind::BrokenPipe {
Ok(())
} else {
Err(error)
fn println(&self) -> io::Result<()> {
writeln!(io::stdout())
}
}