use ferritin_common::{DocRef, doc_ref::Path};
use rustdoc_types::{Item, ItemEnum};
pub(crate) fn generate_docsrs_url(item: DocRef<'_, Item>) -> String {
let docs = item.crate_docs();
let crate_name = docs.name();
let version = docs.crate_version.as_deref().unwrap_or("latest");
let is_std = docs.provenance().is_std();
if let Some(path) = item.path() {
generate_url_for_item_with_path(crate_name, version, is_std, &path, &item)
} else {
generate_url_for_associated_item(item, crate_name, version, is_std)
}
}
fn generate_url_for_item_with_path(
crate_name: &str,
version: &str,
is_std: bool,
path: &Path<'_>,
item: &DocRef<'_, Item>,
) -> String {
let segments = path.to_string();
let parts: Vec<&str> = segments.split("::").collect();
let item_name = item.name().unwrap_or("unknown");
let kind = item.kind();
let base = if is_std {
String::from("http://docs.rust-lang.org/nightly")
} else {
format!("https://docs.rs/{crate_name}/{version}",)
};
match kind {
rustdoc_types::ItemKind::Module => {
if parts.len() <= 1 {
format!("{}/{}/index.html", base, crate_name)
} else {
let module_path = parts[1..].join("/");
format!("{}/{}/{}/index.html", base, crate_name, module_path)
}
}
_ => {
let module_path = if parts.len() > 2 {
parts[1..parts.len() - 1].join("/")
} else {
String::new()
};
let path_prefix = if module_path.is_empty() {
crate_name.to_string()
} else {
format!("{}/{}", crate_name, module_path)
};
match kind {
rustdoc_types::ItemKind::Struct => {
format!("{}/{}/struct.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Enum => {
format!("{}/{}/enum.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Trait => {
format!("{}/{}/trait.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Function => {
format!("{}/{}/fn.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::TypeAlias => {
format!("{}/{}/type.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Constant => {
format!("{}/{}/constant.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Static => {
format!("{}/{}/static.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Union => {
format!("{}/{}/union.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Macro
| rustdoc_types::ItemKind::ProcAttribute
| rustdoc_types::ItemKind::ProcDerive => {
format!("{}/{}/macro.{}.html", base, path_prefix, item_name)
}
rustdoc_types::ItemKind::Primitive => {
format!("{}/{}/primitive.{}.html", base, crate_name, item_name)
}
_ => {
format!("{}/{}/", base, crate_name)
}
}
}
}
}
fn generate_url_for_associated_item(
item: DocRef<'_, Item>,
crate_name: &str,
version: &str,
is_std: bool,
) -> String {
let docs = item.crate_docs();
let item_id = &item.id;
let item_name = item.name().unwrap_or("unknown");
let kind = item.kind();
for impl_item in docs.index.values() {
if let ItemEnum::Impl(impl_block) = &impl_item.inner
&& impl_block.items.contains(item_id)
{
if let rustdoc_types::Type::ResolvedPath(path) = &impl_block.for_
&& let Some(parent) = item.get(&path.id)
{
let parent_url = generate_docsrs_url(parent);
let fragment = match kind {
rustdoc_types::ItemKind::Function => {
if impl_block.trait_.is_some() {
format!("#method.{}", item_name)
} else {
format!("#method.{}", item_name)
}
}
rustdoc_types::ItemKind::AssocConst => {
format!("#associatedconstant.{}", item_name)
}
rustdoc_types::ItemKind::AssocType => {
format!("#associatedtype.{}", item_name)
}
_ => String::new(),
};
return format!("{}{}", parent_url, fragment);
}
}
}
if matches!(kind, rustdoc_types::ItemKind::Variant) {
for enum_item in docs.index.values() {
if let ItemEnum::Enum(enum_data) = &enum_item.inner
&& enum_data.variants.contains(item_id)
{
let parent = item.build_ref(enum_item);
let parent_url = generate_docsrs_url(parent);
return format!("{}#variant.{}", parent_url, item_name);
}
}
}
if matches!(kind, rustdoc_types::ItemKind::StructField) {
for struct_item in docs.index.values() {
if let ItemEnum::Struct(struct_data) = &struct_item.inner
&& matches!(&struct_data.kind, rustdoc_types::StructKind::Plain { fields, .. } if fields.contains(item_id))
{
let parent = item.build_ref(struct_item);
let parent_url = generate_docsrs_url(parent);
return format!("{}#structfield.{}", parent_url, item_name);
}
}
}
if is_std {
format!("https://doc.rust-lang.org/nightly/{}/", crate_name)
} else {
format!("https://docs.rs/{}/{}/{}/", crate_name, version, crate_name)
}
}