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:
parent
06851357a2
commit
a97c9e5a0f
@ -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>>,
|
||||
|
||||
|
@ -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
106
src/viewer/text/mod.rs
Normal 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 don’t 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)
|
||||
}
|
||||
}
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
@ -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 don’t 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())
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user