use scraper::{Html, Selector};
use std::collections::HashSet;
use url::Url;
pub async fn is_gitbook(url: &str) -> Result<bool, Box<dyn std::error::Error>> {
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()?;
let response = client.get(url).send().await?;
let html = response.text().await?;
let indicators = ["gitbook", "data-gitbook", "__GITBOOK__", "gitbook.com"];
let html_lower = html.to_lowercase();
Ok(indicators
.iter()
.any(|&indicator| html_lower.contains(indicator)))
}
pub async fn extract_gitbook_links(
base_url: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let client = reqwest::Client::builder()
.user_agent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36")
.build()?;
let base = Url::parse(base_url)?;
let mut visited = HashSet::new();
let mut to_visit = vec![base_url.to_string()];
let mut all_links = HashSet::new();
let link_selector = Selector::parse("a").unwrap();
while let Some(current_url) = to_visit.pop() {
if visited.contains(¤t_url) {
continue;
}
visited.insert(current_url.clone());
println!("🔍 Exploration: {}", current_url);
let response = match client.get(¤t_url).send().await {
Ok(r) => r,
Err(e) => {
eprintln!("⚠️ Error while retrieving {}: {}", current_url, e);
continue;
}
};
let html = match response.text().await {
Ok(h) => h,
Err(e) => {
eprintln!("⚠️ Error while reading HTML: {}", e);
continue;
}
};
let document = Html::parse_document(&html);
for element in document.select(&link_selector) {
if let Some(href) = element.value().attr("href") {
if let Ok(link_url) = base.join(href) {
let link_str = link_url.to_string();
if link_url.domain() == base.domain()
&& !link_str.contains('#')
&& !link_str.ends_with(".pdf")
&& !link_str.ends_with(".zip")
&& !link_str.ends_with(".jpg")
&& !link_str.ends_with(".png")
{
let normalized = link_str.trim_end_matches('/').to_string();
all_links.insert(normalized.clone());
if !visited.contains(&normalized) && !to_visit.contains(&normalized) {
to_visit.push(normalized);
}
}
}
}
}
}
let mut result: Vec<String> = all_links.into_iter().collect();
result.sort();
println!("✅ {} page(s) trouvée(s)", result.len());
Ok(result)
}
pub async fn crawl_and_save(
base_url: &str,
output_file: &str,
) -> Result<(), Box<dyn std::error::Error>> {
println!("🔍 Checking that {} is a GitBook...", base_url);
if !is_gitbook(base_url).await? {
return Err(format!("⚠️ {} does not seem to be a GitBook site", base_url).into());
}
println!("✅ GitBook detected !");
println!("🕷️ Starting crawling...");
let links = extract_gitbook_links(base_url).await?;
let content = links.join("\n");
tokio::fs::write(output_file, content).await?;
println!("💾 {} saved links in {}", links.len(), output_file);
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_is_gitbook() {
let result = is_gitbook("https://docs.gitbook.com").await;
assert!(result.is_ok());
}
}