Refactor documentation lookup into source

Previously, the Crate, Item and Doc struct made assumptions about the
structure of the documentation source by calling the parser directly.
This works with our current setup with only one documentation format and
one documentation source.  But as we intend to add other formats in the
future, we refactor the documentation lookup into the source module
where the format and the source can be taken into account.
This commit is contained in:
Robin Krahl 2020-08-15 00:18:20 +02:00
parent 76845785ee
commit d1c171c385
No known key found for this signature in database
GPG Key ID: 8E9B0870524F69D8
4 changed files with 168 additions and 213 deletions

View File

@ -4,7 +4,6 @@
use std::convert;
use std::fmt;
use std::ops;
use std::path;
use std::str;
use crate::parser;
@ -58,19 +57,6 @@ pub enum ItemType {
TraitAlias = 25,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Crate {
pub name: String,
pub path: path::PathBuf,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
pub name: Fqn,
pub ty: ItemType,
pub path: path::PathBuf,
}
#[derive(Clone, Debug)]
pub struct Doc {
pub name: Fqn,
@ -387,91 +373,6 @@ impl str::FromStr for ItemType {
}
}
impl Crate {
pub fn new(name: String, path: path::PathBuf) -> Self {
Crate { name, path }
}
pub fn find_item(&self, name: &Fqn) -> anyhow::Result<Option<Item>> {
log::info!("Searching item '{}' in crate '{}'", name, self.name);
if self.name == name.krate() {
if let Some(local_name) = name.rest() {
if let Some(path) = parser::find_item(self.path.join("all.html"), local_name)? {
let path = path::PathBuf::from(path);
let file_name = path.file_name().unwrap().to_str().unwrap();
let item_type: ItemType = file_name.splitn(2, '.').next().unwrap().parse()?;
return Ok(Some(Item::new(
name.clone(),
self.path.join(path),
item_type,
)));
}
}
}
Ok(None)
}
pub fn find_module(&self, name: &Fqn) -> Option<Item> {
log::info!("Searching module '{}' in crate '{}'", name, self.name);
if self.name == name.krate() {
let module_path = if let Some(rest) = name.rest() {
rest.split("::").fold(path::PathBuf::new(), |mut p, s| {
p.push(s);
p
})
} else {
path::PathBuf::new()
};
let path = self.path.join(module_path).join("index.html");
if path.is_file() {
Some(Item::new(name.clone(), path, ItemType::Module))
} else {
None
}
} else {
None
}
}
pub fn find_member(&self, name: &Fqn) -> Option<Item> {
log::info!("Searching member '{}' in crate '{}'", name, self.name);
if let Some(parent) = name.parent() {
// TODO: error
self.find_item(&parent)
.unwrap()
.and_then(|i| i.find_member(name))
} else {
None
}
}
}
impl Item {
pub fn new(name: Fqn, path: path::PathBuf, ty: ItemType) -> Self {
Item { name, ty, path }
}
pub fn load_doc(&self) -> anyhow::Result<Doc> {
log::info!("Loading documentation for '{}'", self.name);
match self.ty {
ItemType::TyMethod
| ItemType::Method
| ItemType::StructField
| ItemType::Variant
| ItemType::AssocType
| ItemType::AssocConst => parser::parse_member_doc(&self),
ItemType::Module => parser::parse_module_doc(&self),
_ => parser::parse_item_doc(&self),
}
}
pub fn find_member(&self, name: &Fqn) -> Option<Item> {
log::info!("Searching member '{}' in item '{}'", name, self.name);
// TODO: error handling
parser::find_member(&self.path, name).unwrap()
}
}
impl Doc {
pub fn new(name: Fqn, ty: ItemType) -> Self {
Self {

View File

@ -5,20 +5,16 @@
//!
//! rusty-man opens the documentation for a given keyword. It performs these steps to find the
//! documentation for an item:
//! 1. The sources, currently only local directories, are loaded, see the `load_sources` funnction
//! 1. The sources, currently only local directories, are loaded, see the `load_sources` function
//! and the `source` module. Per default, we look for documentation in the directory
//! `share/doc/rust{,-doc}/html` relative to the Rust installation path (`rustc --print sysroot`
//! or `usr`) and in `./target/doc`.
//! 2. We split the keyword `{crate}::{item}` into the crate and the item and try to find the crate
//! in one of the sources see the `find_crate` function.
//! 3. If we found a crate, we look up the item in the `all.html` file of the crate and load the
//! documentation linked there. If we cant find the item in the index, we check whether it is
//! a module by trying to open the `{item}/index.html` file. If this fails too, we check
//! whether the item `{parent}::{member}` is a member of another type. See the `find_doc`
//! function and the `doc` module.
//! 4. If we didnt find a match in the previous step, we load the search index from the
//! 2. We try to look up the given keyword in all acailable sources, see the `find_doc` function
//! and the `source` module for the lookup logic and the `doc` module for the loaded
//! documentation.
//! 3. If we didnt find a match in the previous step, we load the search index from the
//! `search-index.js` file for all sources and try to find a matching item. If we find one, we
//! open the documentation for that item as in step 3. See the `search_doc` function and the
//! open the documentation for that item as in step 2. See the `search_doc` function and the
//! `index` module.
//!
//! If we found a documentation item, we use a viewer to open it see the `viewer` module.
@ -135,24 +131,15 @@ fn find_doc(
sources: &[Box<dyn source::Source>],
name: &doc::Name,
) -> anyhow::Result<Option<doc::Doc>> {
let fqn: doc::Fqn = name.clone().into();
if let Some(krate) = find_crate(sources, fqn.krate()) {
krate
.find_item(&fqn)?
.or_else(|| krate.find_module(&fqn))
.or_else(|| krate.find_member(&fqn))
.map(|i| i.load_doc())
.transpose()
} else {
log::info!("Could not find crate '{}'", fqn.krate());
let fqn = name.clone().into();
for source in sources {
if let Some(doc) = source.find_doc(&fqn)? {
return Ok(Some(doc));
}
}
log::info!("Could not find item '{}'", fqn);
Ok(None)
}
}
/// Find the crate with the given name.
fn find_crate(sources: &[Box<dyn source::Source>], name: &str) -> Option<doc::Crate> {
sources.iter().filter_map(|s| s.find_crate(name)).next()
}
/// Use the search index to find the documentation for an item that partially matches the given
/// keyword.

View File

@ -104,26 +104,9 @@ pub fn find_item<P: AsRef<path::Path>>(path: P, item: &str) -> anyhow::Result<Op
Ok(item)
}
pub fn find_member<P: AsRef<path::Path>>(
path: P,
name: &doc::Fqn,
) -> anyhow::Result<Option<doc::Item>> {
pub fn find_member<P: AsRef<path::Path>>(path: P, name: &doc::Fqn) -> anyhow::Result<bool> {
let document = parse_file(path.as_ref())?;
if let Some(member) = get_member(&document, name.last())? {
let parent = member
.as_node()
.parent()
.context("Member element does not have a parent")?;
if let Some(parent_id) = get_node_attribute(&parent, "id") {
let item_type: doc::ItemType = parent_id.splitn(2, '.').next().unwrap().parse()?;
return Ok(Some(doc::Item::new(
name.clone(),
path.as_ref().to_owned(),
item_type,
)));
}
}
Ok(None)
get_member(&document, name.last()).map(|member| member.is_some())
}
fn select(
@ -179,10 +162,14 @@ fn get_example(node: &kuchiki::NodeRef) -> doc::Example {
doc::Example::new(description, node.into())
}
pub fn parse_item_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
log::info!("Parsing item documentation for '{}'", item.name);
let document = parse_file(&item.path)?;
let definition_selector = match item.ty {
pub fn parse_item_doc(
path: impl AsRef<path::Path>,
name: &doc::Fqn,
ty: doc::ItemType,
) -> anyhow::Result<doc::Doc> {
log::info!("Parsing item documentation for '{}'", name);
let document = parse_file(path)?;
let definition_selector = match ty {
doc::ItemType::Constant => "pre.const",
doc::ItemType::Function => "pre.fn",
doc::ItemType::Typedef => "pre.typedef",
@ -191,27 +178,27 @@ pub fn parse_item_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
let definition = select_first(&document, definition_selector)?;
let description = select_first(&document, "#main > .docblock:not(.type-decl)")?;
let mut doc = doc::Doc::new(item.name.clone(), item.ty);
let mut doc = doc::Doc::new(name.clone(), ty);
doc.description = description.map(From::from);
doc.definition = definition.map(From::from);
let (ty, groups) = get_variants(&document, item)?;
let (ty, groups) = get_variants(&document, name)?;
if !groups.is_empty() {
doc.groups.push((ty, groups));
}
let (ty, groups) = get_fields(&document, item)?;
let (ty, groups) = get_fields(&document, name)?;
if !groups.is_empty() {
doc.groups.push((ty, groups));
}
let (ty, groups) = get_assoc_types(&document, item)?;
let (ty, groups) = get_assoc_types(&document, name)?;
if !groups.is_empty() {
doc.groups.push((ty, groups));
}
let (ty, groups) = get_methods(&document, item)?;
let (ty, groups) = get_methods(&document, name)?;
if !groups.is_empty() {
doc.groups.push((ty, groups));
}
let (ty, groups) = get_implementations(&document, item)?;
let (ty, groups) = get_implementations(&document, name)?;
if !groups.is_empty() {
doc.groups.push((ty, groups));
}
@ -235,16 +222,16 @@ const MODULE_MEMBER_TYPES: &[doc::ItemType] = &[
doc::ItemType::Union,
];
pub fn parse_module_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
log::info!("Parsing module documentation for '{}'", item.name);
let document = parse_file(&item.path)?;
pub fn parse_module_doc(path: impl AsRef<path::Path>, name: &doc::Fqn) -> anyhow::Result<doc::Doc> {
log::info!("Parsing module documentation for '{}'", name);
let document = parse_file(path)?;
let description = select_first(&document, ".docblock")?;
let mut doc = doc::Doc::new(item.name.clone(), item.ty);
let mut doc = doc::Doc::new(name.clone(), doc::ItemType::Module);
doc.description = description.map(From::from);
for item_type in MODULE_MEMBER_TYPES {
let mut group = doc::MemberGroup::new(None);
group.members = get_members(&document, item, *item_type)?;
group.members = get_members(&document, name, *item_type)?;
if !group.members.is_empty() {
doc.groups.push((*item_type, vec![group]));
}
@ -252,18 +239,21 @@ pub fn parse_module_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
Ok(doc)
}
pub fn parse_member_doc(item: &doc::Item) -> anyhow::Result<doc::Doc> {
log::info!("Parsing member documentation for '{}'", item.name);
let document = parse_file(&item.path)?;
let member = get_member(&document, item.name.last())?
.with_context(|| format!("Could not find member {}", &item.name))?;
pub fn parse_member_doc(path: impl AsRef<path::Path>, name: &doc::Fqn) -> anyhow::Result<doc::Doc> {
log::info!("Parsing member documentation for '{}'", name);
let document = parse_file(path)?;
let member = get_member(&document, name.last())?
.with_context(|| format!("Could not find member {}", name))?;
let heading = member
.as_node()
.parent()
.with_context(|| format!("The member {} does not have a parent", &item.name))?;
.with_context(|| format!("The member {} does not have a parent", name))?;
let parent_id = get_node_attribute(&heading, "id")
.with_context(|| format!("The heading for member {} does not have an ID", name))?;
let ty: doc::ItemType = parent_id.splitn(2, '.').next().unwrap().parse()?;
let docblock = heading.next_sibling();
let mut doc = doc::Doc::new(item.name.clone(), item.ty);
let mut doc = doc::Doc::new(name.clone(), ty);
doc.definition = Some(member.into());
doc.description = docblock.map(From::from);
Ok(doc)
@ -275,7 +265,7 @@ fn get_id_part(node: &kuchiki::NodeRef, i: usize) -> Option<String> {
fn get_fields(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
let ty = doc::ItemType::StructField;
let mut fields = MemberDocs::new(parent, ty);
@ -306,7 +296,7 @@ fn get_fields(
fn get_methods(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
let ty = doc::ItemType::Method;
let mut groups: Vec<doc::MemberGroup> = Vec::new();
@ -384,7 +374,7 @@ fn get_methods(
fn get_assoc_types(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
let ty = doc::ItemType::AssocType;
let mut groups: Vec<doc::MemberGroup> = Vec::new();
@ -410,7 +400,7 @@ fn get_assoc_types(
fn get_method_groups(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
heading_id: String,
ty: doc::ItemType,
subheading_type: &markup5ever::LocalName,
@ -451,7 +441,7 @@ fn get_method_groups(
}
fn get_method_group(
parent: &doc::Item,
parent: &doc::Fqn,
title: Option<String>,
impl_items: &kuchiki::NodeRef,
ty: doc::ItemType,
@ -476,7 +466,7 @@ fn get_method_group(
fn get_variants(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
let ty = doc::ItemType::Variant;
let mut variants = MemberDocs::new(parent, ty);
@ -507,7 +497,7 @@ fn get_variants(
fn get_implementations(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
) -> anyhow::Result<(doc::ItemType, Vec<doc::MemberGroup>)> {
let mut groups: Vec<doc::MemberGroup> = Vec::new();
@ -534,7 +524,7 @@ fn get_implementations(
fn get_implementation_group(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
title: &str,
list_id: &str,
) -> anyhow::Result<Option<doc::MemberGroup>> {
@ -564,7 +554,7 @@ fn get_implementation_group(
fn get_members(
document: &kuchiki::NodeRef,
parent: &doc::Item,
parent: &doc::Fqn,
ty: doc::ItemType,
) -> anyhow::Result<Vec<doc::Doc>> {
let mut members: Vec<doc::Doc> = Vec::new();
@ -574,7 +564,7 @@ fn get_members(
let item_name = item.as_node().text_contents();
let docblock = item.as_node().parent().and_then(|n| n.next_sibling());
let mut doc = doc::Doc::new(parent.name.child(&item_name), ty);
let mut doc = doc::Doc::new(parent.child(&item_name), ty);
doc.description = docblock.map(From::from);
members.push(doc);
}
@ -634,12 +624,12 @@ fn has_class(node: &kuchiki::NodeRef, class: &str) -> bool {
struct MemberDocs<'a> {
docs: Vec<doc::Doc>,
parent: &'a doc::Item,
parent: &'a doc::Fqn,
ty: doc::ItemType,
}
impl<'a> MemberDocs<'a> {
pub fn new(parent: &'a doc::Item, ty: doc::ItemType) -> Self {
pub fn new(parent: &'a doc::Fqn, ty: doc::ItemType) -> Self {
Self {
docs: Vec::new(),
parent,
@ -665,7 +655,7 @@ impl<'a> MemberDocs<'a> {
let definition = definition.take();
if let Some(name) = name {
let mut doc = doc::Doc::new(self.parent.name.child(&name), self.ty);
let mut doc = doc::Doc::new(self.parent.child(&name), self.ty);
doc.definition = definition;
doc.description = description;
self.docs.push(doc);
@ -714,8 +704,7 @@ mod tests {
let path = crate::tests::ensure_docs();
let path = path.join("kuchiki").join("struct.NodeRef.html");
let name: doc::Fqn = "kuchiki::NodeRef".to_owned().into();
let item = doc::Item::new(name.clone(), path, doc::ItemType::Struct);
let doc = super::parse_item_doc(&item).unwrap();
let doc = super::parse_item_doc(&path, &name, doc::ItemType::Struct).unwrap();
assert_eq!(name, doc.name);
assert_eq!(doc::ItemType::Struct, doc.ty);
@ -728,8 +717,7 @@ mod tests {
let path = crate::tests::ensure_docs();
let path = path.join("kuchiki").join("struct.NodeDataRef.html");
let name: doc::Fqn = "kuchiki::NodeDataRef::as_node".to_owned().into();
let item = doc::Item::new(name.clone(), path, doc::ItemType::Method);
let doc = super::parse_member_doc(&item).unwrap();
let doc = super::parse_member_doc(&path, &name, doc::ItemType::Method).unwrap();
assert_eq!(name, doc.name);
assert_eq!(doc::ItemType::Method, doc.ty);

View File

@ -10,10 +10,11 @@ use anyhow::anyhow;
use crate::doc;
use crate::index;
use crate::parser;
/// Documentation source, for example a local directory.
pub trait Source {
fn find_crate(&self, name: &str) -> Option<doc::Crate>;
fn find_doc(&self, name: &doc::Fqn) -> anyhow::Result<Option<doc::Doc>>;
fn load_index(&self) -> anyhow::Result<Option<index::Index>>;
}
@ -32,10 +33,8 @@ impl DirSource {
log::info!("Created directory source at '{}'", path.display());
Self { path }
}
}
impl Source for DirSource {
fn find_crate(&self, name: &str) -> Option<doc::Crate> {
fn get_crate(&self, name: &str) -> Option<path::PathBuf> {
log::info!(
"Searching crate '{}' in dir source '{}'",
name,
@ -44,13 +43,118 @@ impl Source for DirSource {
let crate_path = self.path.join(name.replace('-', "_"));
if crate_path.join("all.html").is_file() {
log::info!("Found crate '{}': '{}'", name, crate_path.display());
Some(doc::Crate::new(name.to_owned(), crate_path))
Some(crate_path)
} else {
log::info!("Did not find crate '{}' in '{}'", name, self.path.display());
None
}
}
fn get_item(&self, root: &path::Path, name: &doc::Fqn) -> anyhow::Result<Option<doc::Doc>> {
log::info!(
"Searching item '{}' in directory '{}'",
name,
root.display()
);
if let Some(local_name) = name.rest() {
if let Some(path) = parser::find_item(root.join("all.html"), local_name)? {
let file_name = path::Path::new(&path)
.file_name()
.unwrap()
.to_str()
.unwrap();
let ty: doc::ItemType = file_name.splitn(2, '.').next().unwrap().parse()?;
parser::parse_item_doc(root.join(path), name, ty).map(Some)
} else {
Ok(None)
}
} else {
Ok(None)
}
}
fn get_module(&self, root: &path::Path, name: &doc::Fqn) -> anyhow::Result<Option<doc::Doc>> {
log::info!(
"Searching module '{}' in directory '{}'",
name,
root.display()
);
let module_path = if let Some(local_name) = name.rest() {
local_name
.split("::")
.fold(path::PathBuf::new(), |mut p, s| {
p.push(s);
p
})
} else {
path::PathBuf::new()
};
let path = root.join(module_path).join("index.html");
if path.is_file() {
parser::parse_module_doc(path, name).map(Some)
} else {
Ok(None)
}
}
fn get_member(&self, root: &path::Path, name: &doc::Fqn) -> anyhow::Result<Option<doc::Doc>> {
log::info!(
"Searching member '{}' in directory '{}'",
name,
root.display()
);
if let Some(parent) = name.parent() {
if let Some(rest) = parent.rest() {
if let Some(path) = parser::find_item(&root.join("all.html"), rest)? {
let path = root.join(path);
if parser::find_member(&path, name)? {
return parser::parse_member_doc(&path, name).map(Some);
}
}
}
}
Ok(None)
}
}
impl Source for DirSource {
fn find_doc(&self, name: &doc::Fqn) -> anyhow::Result<Option<doc::Doc>> {
log::info!(
"Searching documentation for '{}' in dir source '{}'",
name,
self.path.display()
);
if let Some(crate_path) = self.get_crate(name.krate()) {
let doc = self
.get_item(&crate_path, name)
.transpose()
.or_else(|| self.get_module(&crate_path, name).transpose())
.or_else(|| self.get_member(&crate_path, name).transpose())
.transpose()?;
if doc.is_some() {
log::info!(
"Found documentation for '{}' in dir source '{}'",
name,
self.path.display()
)
} else {
log::info!(
"Did not find documentation for '{}' in dir source '{}'",
name,
self.path.display()
)
}
Ok(doc)
} else {
log::info!(
"Did not find crate '{}' in dir source '{}'",
name.krate(),
self.path.display()
);
Ok(None)
}
}
fn load_index(&self) -> anyhow::Result<Option<index::Index>> {
log::info!("Searching search index for '{}'", self.path.display());
// use the first file that matches the pattern search-index*.js
@ -80,28 +184,3 @@ pub fn get_source<P: AsRef<path::Path>>(path: P) -> anyhow::Result<Box<dyn Sourc
))
}
}
#[cfg(test)]
mod tests {
use std::path;
use super::Source;
#[test]
fn dir_source_find_crate() {
fn assert_crate(source: &dyn super::Source, path: &path::PathBuf, name: &str) {
assert_eq!(
source.find_crate(name),
Some(super::doc::Crate::new(name.to_owned(), path.join(name)))
);
}
let doc = crate::tests::ensure_docs();
let source = super::DirSource::new(doc.clone());
assert_crate(&source, &doc, "clap");
assert_crate(&source, &doc, "lazy_static");
assert_eq!(source.find_crate("lazystatic"), None);
}
}