Add -e/--example option to only show examples

As requested here [0], this patch adds a new -e/--example option that
extracts the examples from the documentation string instead of printing
the full documentation for an item.  Syntax highlighting will be added
in the future.

[0] https://old.reddit.com/r/rust/comments/hx16j0/rustyman_a_commandline_viewer_for_rustdoc/fz3utjf/
This commit is contained in:
Robin Krahl 2020-07-25 12:58:52 +02:00
parent 1a242e5474
commit 34857816e1
No known key found for this signature in database
GPG Key ID: 8E9B0870524F69D8
8 changed files with 137 additions and 10 deletions

View File

@ -10,6 +10,8 @@ SPDX-License-Identifier: MIT
- Add `env_logger` dependency in version 0.7.1.
- Add `log` dependency in version 0.4.11.
- Show the definition for global functions.
- Add the `-e`/`--examples` option to only show the examples instead of opening
the full documentation for an item.
# v0.1.1 (2020-07-24)

View File

@ -80,6 +80,12 @@ pub struct MemberGroup {
pub members: Vec<Doc>,
}
#[derive(Clone, Debug)]
pub struct Example {
pub description: Option<String>,
pub code: String,
}
impl Name {
pub fn is_singleton(&self) -> bool {
self.last_start == 0
@ -465,13 +471,12 @@ impl Doc {
groups: Default::default(),
}
}
}
impl MemberGroup {
pub fn new(title: Option<String>) -> Self {
MemberGroup {
title,
members: Vec::new(),
pub fn find_examples(&self) -> anyhow::Result<Vec<Example>> {
if let Some(description) = &self.description {
parser::find_examples(&description)
} else {
Ok(Vec::new())
}
}
}
@ -486,6 +491,21 @@ impl fmt::Display for Doc {
}
}
impl MemberGroup {
pub fn new(title: Option<String>) -> Self {
MemberGroup {
title,
members: Vec::new(),
}
}
}
impl Example {
pub fn new(description: Option<String>, code: String) -> Self {
Example { description, code }
}
}
#[cfg(test)]
mod tests {
use super::Name;

View File

@ -86,6 +86,10 @@ struct Opt {
/// indexes are not read.
#[structopt(long)]
no_search: bool,
/// Show all examples for the item instead of opening the full documentation.
#[structopt(short, long)]
examples: bool,
}
fn main() -> anyhow::Result<()> {
@ -103,7 +107,17 @@ fn main() -> anyhow::Result<()> {
if let Some(doc) = doc {
let viewer = opt.viewer.unwrap_or_else(viewer::get_default);
viewer.open(&doc)
if opt.examples {
let examples = doc.find_examples()?;
anyhow::ensure!(
!examples.is_empty(),
"Could not find examples for {}",
&opt.keyword
);
viewer.open_examples(&doc, examples)
} else {
viewer.open(&doc)
}
} else {
// item selection cancelled by user
Ok(())

View File

@ -28,6 +28,15 @@ fn parse_file<P: AsRef<path::Path>>(path: P) -> anyhow::Result<kuchiki::NodeRef>
.context("Could not read HTML file")
}
fn parse_string(s: impl Into<String>) -> anyhow::Result<kuchiki::NodeRef> {
use kuchiki::traits::TendrilSink;
kuchiki::parse_html()
.from_utf8()
.read_from(&mut s.into().as_bytes())
.context("Could not read HTML string")
}
pub fn find_item<P: AsRef<path::Path>>(path: P, item: &str) -> anyhow::Result<Option<String>> {
use std::ops::Deref;
@ -77,6 +86,27 @@ fn select_first(
select(element, selector).map(|mut i| i.next())
}
pub fn find_examples(s: &str) -> anyhow::Result<Vec<doc::Example>> {
let element = parse_string(s)?;
let examples = select(&element, ".rust-example-rendered")?;
Ok(examples.map(|n| get_example(n.as_node())).collect())
}
fn get_example(node: &kuchiki::NodeRef) -> doc::Example {
let code = node.text_contents();
let description_element = node.parent().as_ref().and_then(previous_sibling_element);
let description = description_element
.and_then(|n| {
if n.text_contents().ends_with(':') {
Some(n)
} else {
None
}
})
.and_then(|n| get_html(&n).ok());
doc::Example::new(description, code)
}
pub fn parse_item_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
let document = parse_file(&item.path)?;
let definition_selector = if item.ty == doc::ItemType::Function {
@ -480,6 +510,17 @@ fn next_sibling_element(node: &kuchiki::NodeRef) -> Option<kuchiki::NodeRef> {
next
}
fn previous_sibling_element(node: &kuchiki::NodeRef) -> Option<kuchiki::NodeRef> {
let mut previous = node.previous_sibling();
while let Some(node) = &previous {
if node.as_element().is_some() {
break;
}
previous = node.previous_sibling();
}
previous
}
fn is_element(node: &kuchiki::NodeRef, name: &markup5ever::LocalName) -> bool {
node.as_element()
.map(|e| &e.name.local == name)

View File

@ -11,6 +11,8 @@ use crate::doc;
pub trait Viewer: fmt::Debug {
fn open(&self, doc: &doc::Doc) -> anyhow::Result<()>;
fn open_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> anyhow::Result<()>;
}
pub fn get_viewer(s: &str) -> anyhow::Result<Box<dyn Viewer>> {

View File

@ -17,6 +17,8 @@ pub trait Printer: fmt::Debug {
fn print_html(&self, indent: usize, s: &str, show_links: bool) -> io::Result<()>;
fn print_code(&self, indent: usize, code: &str) -> io::Result<()>;
fn println(&self) -> io::Result<()>;
}
@ -31,9 +33,7 @@ impl<P: Printer> TextViewer<P> {
}
fn print_doc(&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")?;
self.print_title(doc)?;
self.print_opt("SYNOPSIS", doc.definition.as_deref(), false)?;
self.print_opt("DESCRIPTION", doc.description.as_deref(), true)?;
for (ty, groups) in &doc.groups {
@ -65,6 +65,32 @@ impl<P: Printer> TextViewer<P> {
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_opt(&self, title: &str, s: Option<&str>, show_links: bool) -> io::Result<()> {
if let Some(s) = s {
self.print_heading(1, title)?;
@ -109,6 +135,14 @@ impl<P: Printer> viewer::Viewer for TextViewer<P> {
.or_else(ignore_pipe_error)
.map_err(Into::into)
}
fn open_examples(&self, doc: &doc::Doc, examples: Vec<doc::Example>) -> anyhow::Result<()> {
spawn_pager();
self.print_examples(doc, examples)
.or_else(ignore_pipe_error)
.map_err(Into::into)
}
}
pub fn spawn_pager() {

View File

@ -44,6 +44,13 @@ impl super::Printer for PlainTextRenderer {
Ok(())
}
fn print_code(&self, indent: usize, code: &str) -> io::Result<()> {
for line in code.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)
}

View File

@ -53,6 +53,13 @@ impl super::Printer for RichTextRenderer {
Ok(())
}
fn print_code(&self, indent: usize, code: &str) -> io::Result<()> {
for line in code.split('\n') {
writeln!(io::stdout(), "{}{}", " ".repeat(indent), line)?;
}
Ok(())
}
fn print_heading(&self, indent: usize, level: usize, s: &str) -> io::Result<()> {
let mut text = crossterm::style::style(s);
if level < 4 {