#![doc = include_str!("../README.md")]
use anyhow::{anyhow, Result};
use htmd::HtmlToMarkdown;
use reqwest::blocking::Client;
use scraper::{Html, Selector};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use tempfile::tempdir;
pub fn fetch_online_docs(crate_name: &str, item_path: Option<&str>) -> Result<String> {
let client = Client::new();
let url = if let Some(path) = item_path {
let path_with_html = if !path.ends_with(".html") {
format!("{}.html", path)
} else {
path.to_string()
};
let url_path = path_with_html.replace("::", "/");
format!(
"https://docs.rs/{}/latest/{}/{}",
crate_name, crate_name, url_path
)
} else {
format!("https://docs.rs/{}/latest/{}/", crate_name, crate_name)
};
let response = client.get(&url).send()?;
if !response.status().is_success() {
return Err(anyhow!(
"Failed to fetch documentation. Status: {}",
response.status()
));
}
let html_content = response.text()?;
process_html_content(&html_content)
}
pub fn fetch_local_docs(crate_name: &str, item_path: Option<&str>) -> Result<String> {
let temp_dir = tempdir()?;
let temp_path = temp_dir.path();
let current_dir = std::env::current_dir()?;
let is_cargo_project = current_dir.join("Cargo.toml").exists();
let doc_path: PathBuf = if is_cargo_project {
let status = Command::new("cargo")
.args(["doc", "--no-deps"])
.current_dir(¤t_dir)
.status()?;
if !status.success() {
return Err(anyhow!("Failed to build documentation with cargo doc"));
}
current_dir.join("target").join("doc")
} else {
let status = Command::new("cargo")
.args(["new", "--bin", "temp_project"])
.current_dir(temp_path)
.status()?;
if !status.success() {
return Err(anyhow!("Failed to create temporary cargo project"));
}
let temp_cargo_toml = temp_path.join("temp_project").join("Cargo.toml");
let mut cargo_toml_content = fs::read_to_string(&temp_cargo_toml)?;
cargo_toml_content.push_str(&format!("\n[dependencies]\n{} = \"*\"\n", crate_name));
fs::write(&temp_cargo_toml, cargo_toml_content)?;
let status = Command::new("cargo")
.args(["doc", "--no-deps"])
.current_dir(temp_path.join("temp_project"))
.status()?;
if !status.success() {
return Err(anyhow!(
"Failed to build documentation for crate: {}",
crate_name
));
}
temp_path.join("temp_project").join("target").join("doc")
};
let crate_doc_path = doc_path.join(crate_name.replace('-', "_"));
if !crate_doc_path.exists() {
return Err(anyhow!("Documentation not found for crate: {}", crate_name));
}
let index_path = if let Some(path) = item_path {
crate_doc_path
.join(path.replace("::", "/"))
.join("index.html")
} else {
crate_doc_path.join("index.html")
};
if !index_path.exists() {
return Err(anyhow!("Documentation not found at path: {:?}", index_path));
}
let html_content = fs::read_to_string(index_path)?;
process_html_content(&html_content)
}
pub fn process_html_content(html: &str) -> Result<String> {
let document = Html::parse_document(html);
let main_content_selector = Selector::parse("#main-content").unwrap();
let main_content = document
.select(&main_content_selector)
.next()
.ok_or_else(|| anyhow!("Could not find main content section"))?;
let html_content = main_content.inner_html();
let converter = HtmlToMarkdown::builder()
.skip_tags(vec!["script", "style"])
.build();
let markdown = converter
.convert(&html_content)
.map_err(|e| anyhow!("HTML to Markdown conversion failed: {}", e))?;
let cleaned_text = clean_markdown(&markdown);
Ok(cleaned_text)
}
pub fn clean_markdown(markdown: &str) -> String {
let mut result = String::new();
let mut last_was_newline = false;
let mut newline_count = 0;
for c in markdown.chars() {
if c == '\n' {
newline_count += 1;
if newline_count <= 2 {
result.push(c);
}
last_was_newline = true;
} else {
if last_was_newline {
newline_count = 0;
last_was_newline = false;
}
result.push(c);
}
}
result
}
pub struct Config {
pub crate_name: String,
pub item_path: Option<String>,
pub online: bool,
}
impl Config {
pub fn new<S: Into<String>>(crate_name: S) -> Self {
Self {
crate_name: crate_name.into(),
item_path: None,
online: false,
}
}
pub fn with_item_path<S: Into<String>>(mut self, item_path: S) -> Self {
self.item_path = Some(item_path.into());
self
}
pub fn with_online(mut self, online: bool) -> Self {
self.online = online;
self
}
pub fn execute(&self) -> Result<String> {
if self.online {
fetch_online_docs(&self.crate_name, self.item_path.as_deref())
} else {
fetch_local_docs(&self.crate_name, self.item_path.as_deref())
}
}
}