use anyhow::{anyhow, Context};
use fs_err as fs;
use std::cell::Cell;
use std::path::Path;
mod extract_doc;
mod options;
pub mod transform;
pub mod utils;
pub use extract_doc::extract_doc_from_source_file;
pub use self::options::RustDocOptions;
pub fn parse(options: &options::RustDocOptions) -> anyhow::Result<String> {
let project: Project = match options.workspace_project {
None => Project::from_current_dir()?,
Some(ref project) => Project::from_current_dir_workspace_project(project)?,
};
let entryfile: &Path = &options.source;
let doc: Doc = extract_doc_from_source_file(entryfile)?
.ok_or_else(|| anyhow!("crate-level rustdoc not found"))?;
let doc = transform_doc(&doc, &project, entryfile, &options)?;
Ok(doc.content)
}
fn transform_doc(
doc: &Doc,
project: &Project,
entrypoint: impl AsRef<Path>,
options: &options::RustDocOptions,
) -> anyhow::Result<Doc> {
use transform::{
DocTransform, DocTransformIntralinks, DocTransformRustMarkdownTag,
DocTransformRustRemoveComments,
};
let transform = DocTransformRustRemoveComments::new();
let doc = transform.transform(doc)?;
let transform = DocTransformRustMarkdownTag::new();
let doc = transform.transform(&doc)?;
let had_warnings = Cell::new(false);
let transform = DocTransformIntralinks::new(
project.get_package_name(),
entrypoint,
|msg| {
println!("{}", msg);
had_warnings.set(true);
},
options.intralinks.clone(),
);
Ok(transform.transform(&doc)?)
}
#[derive(PartialEq, Eq, Debug)]
struct Project {
package_name: String,
}
impl Project {
pub fn from_current_dir() -> anyhow::Result<Project> {
let cmd = cargo_metadata::MetadataCommand::new();
let metadata = cmd.exec()?;
let package = metadata
.root_package()
.context("project has no root package")?;
Ok(Project::from_package(package))
}
fn select_package<'a>(
metadata: &'a cargo_metadata::Metadata,
package_name: &str,
) -> Option<&'a cargo_metadata::Package> {
let package = metadata
.packages
.iter()
.find(|package| package.name == package_name)?;
match metadata.workspace_members.contains(&package.id) {
false => None,
true => Some(package),
}
}
pub fn from_current_dir_workspace_project(project_name: &str) -> anyhow::Result<Project> {
let cmd = cargo_metadata::MetadataCommand::new();
let metadata = cmd.exec()?;
let package =
Project::select_package(&metadata, project_name).context("project has no package")?;
Ok(Project::from_package(package))
}
fn from_package(package: &cargo_metadata::Package) -> Project {
const LIB_CRATE_KINDS: [&str; 6] =
["lib", "dylib", "staticlib", "cdylib", "rlib", "proc-macro"];
let lib_packages: Vec<&cargo_metadata::Target> = package
.targets
.iter()
.filter(|target| {
target
.kind
.iter()
.any(|k| LIB_CRATE_KINDS.contains(&k.as_str()))
})
.collect();
assert!(lib_packages.len() <= 1, "more than one lib target");
Project {
package_name: package.name.clone(),
}
}
#[must_use]
pub fn get_package_name(&self) -> &str {
&self.package_name
}
}
fn project_package_name(manifest_path: impl AsRef<Path>) -> Option<String> {
let str: String = fs::read_to_string(&manifest_path).ok()?;
let toml: toml::Value = toml::from_str(&str).ok()?;
let package_name = toml
.get("package")
.and_then(|v| v.get("name"))
.and_then(toml::Value::as_str)?;
Some(package_name.to_owned())
}
#[derive(Eq, PartialEq, Clone, Debug)]
pub struct Doc {
pub content: String,
}
impl Doc {
#[allow(clippy::should_implement_trait)]
pub fn from_str(str: impl Into<String>) -> Doc {
Doc {
content: str.into(),
}
}
fn is_toplevel_doc(attr: &syn::Attribute) -> bool {
use syn::token::Bang;
use syn::AttrStyle;
attr.style == AttrStyle::Inner(Bang::default()) && attr.path.is_ident("doc")
}
#[must_use]
pub fn as_string(&self) -> &str {
&self.content
}
}