use std::borrow::Cow;
use super::*;
use crate::markdown::MarkdownRenderer;
use crate::styled_string::{DocumentNode, LinkTarget, TruncationLevel};
use rustdoc_types::ItemKind;
#[derive(Debug, Clone, Default)]
pub(crate) struct DocInfo {
pub(crate) text: String,
pub(crate) total_lines: usize,
pub(crate) displayed_lines: usize,
pub(crate) is_truncated: bool,
}
impl DocInfo {
pub(crate) fn elided_lines(&self) -> usize {
self.total_lines.saturating_sub(self.displayed_lines)
}
pub(crate) fn elided_indicator(&self) -> Option<String> {
if self.is_truncated {
Some(format!("[+{} lines elided]", self.elided_lines()))
} else {
None
}
}
}
impl Request {
pub(crate) fn render_docs<'a>(
&'a self,
item: DocRef<'a, Item>,
markdown: &str,
) -> Vec<DocumentNode<'a>> {
MarkdownRenderer::render_with_resolver(markdown, |url| -> Option<LinkTarget<'a>> {
self.extract_link_target(item, url)
})
}
fn extract_link_target<'a>(
&'a self,
origin: DocRef<'a, Item>,
url: &str,
) -> Option<LinkTarget<'a>> {
if url.starts_with('#') {
return None; }
if url.starts_with("http://") || url.starts_with("https://") {
return None; }
let (path, _fragment) = url.split_once('#').unwrap_or((url, ""));
if path.ends_with(".html") || path.contains("/") {
log::trace!("extract_link_target: parsing relative URL '{}'", path);
if let Some(item_path) = self.parse_html_path_to_item_path(origin, path) {
log::trace!(" → Extracted item path: '{}'", item_path);
return Some(LinkTarget::Path(Cow::Owned(item_path)));
} else {
log::trace!(" → Could not extract item path, keeping as external URL");
return None;
}
}
log::trace!("extract_link_target: processing link '{}'", path);
let link_id = origin
.links
.get(path)
.or_else(|| origin.links.get(&format!("`{}`", path)));
if let Some(link_id) = link_id {
log::trace!(" ✓ Found in origin.links with ID {:?}", link_id);
if let Some(item) = origin.get(link_id) {
log::trace!(
" → Same-crate item: path='{}', kind={:?}",
self.get_item_full_path(item),
item.kind()
);
return Some(LinkTarget::Resolved(item));
}
log::trace!(" → Not in same crate index, checking external paths");
if let Some(item_summary) = origin.crate_docs().paths.get(link_id) {
log::trace!(
" ✓ Found in paths map: {:?}, kind: {:?}",
item_summary.path,
item_summary.kind
);
let full_path = item_summary.path.join("::");
return Some(LinkTarget::Path(Cow::Owned(full_path)));
}
}
log::trace!(" ✗ Not found in links map, using fallback for '{}'", path);
let qualified_path = if let Some(rest) = path.strip_prefix("crate::") {
format!("{}::{}", origin.crate_docs().name(), rest)
} else if let Some(rest) = path.strip_prefix("self::") {
format!("{}::{}", origin.crate_docs().name(), rest)
} else if path.contains("::") {
path.to_string()
} else {
format!("{}::{}", origin.crate_docs().name(), path)
};
log::trace!(" → Qualified path: '{}'", qualified_path);
Some(LinkTarget::Path(Cow::Owned(qualified_path)))
}
fn parse_html_path_to_item_path(
&self,
origin: DocRef<'_, Item>,
html_path: &str,
) -> Option<String> {
let crate_name = origin.crate_docs().name();
let path = html_path.strip_prefix("./").unwrap_or(html_path);
if !path.ends_with(".html") {
return None;
}
if path.ends_with("/index.html") {
let module_path = path.strip_suffix("/index.html")?;
let module_parts: Vec<&str> = module_path.split('/').collect();
return Some(format!("{}::{}", crate_name, module_parts.join("::")));
}
let without_html = path.strip_suffix(".html")?;
if let Some((_kind, name)) = without_html.split_once('.') {
return Some(format!("{}::{}", crate_name, name));
}
Some(format!("{}::{}", crate_name, without_html))
}
fn make_relative_url_absolute(&self, origin: DocRef<'_, Item>, relative_url: &str) -> String {
let crate_docs = origin.crate_docs();
let crate_name = crate_docs.name();
let version = crate_docs
.version()
.map(|v| v.to_string())
.unwrap_or_else(|| "latest".to_string());
let is_std = crate_docs.provenance().is_std();
let base = if is_std {
format!("https://doc.rust-lang.org/nightly/{}", crate_name)
} else {
format!("https://docs.rs/{}/{}/{}", crate_name, version, crate_name)
};
let relative = relative_url.strip_prefix("./").unwrap_or(relative_url);
if relative.starts_with("../") {
return format!("{}/{}", base, relative.trim_start_matches("../"));
}
format!("{}/{}", base, relative)
}
fn get_item_full_path(&self, item: DocRef<'_, Item>) -> String {
if let Some(path) = item.path() {
path.to_string()
} else if let Some(name) = item.name() {
format!("{}::{}", item.crate_docs().name(), name)
} else {
item.crate_docs().name().to_string()
}
}
fn generate_url_from_path_and_kind(&self, path: &str, kind: rustdoc_types::ItemKind) -> String {
let parts: Vec<&str> = path.split("::").collect();
if parts.is_empty() {
return String::new();
}
let crate_name = parts[0];
let is_std = matches!(crate_name, "std" | "core" | "alloc" | "proc_macro");
let base = if is_std {
"https://doc.rust-lang.org/nightly".to_string()
} else {
format!("https://docs.rs/{}/latest", crate_name)
};
if parts.len() == 1 {
return format!("{}/{}/index.html", base, crate_name);
}
let module_parts = &parts[1..parts.len() - 1];
let item_name = parts[parts.len() - 1];
let module_path = if module_parts.is_empty() {
crate_name.to_string()
} else {
format!("{}/{}", crate_name, module_parts.join("/"))
};
match kind {
ItemKind::Module => {
let full_module_path = format!("{}/{}", module_path, item_name);
format!("{}/{}/index.html", base, full_module_path)
}
ItemKind::Struct => format!("{}/{}/struct.{}.html", base, module_path, item_name),
ItemKind::Enum => format!("{}/{}/enum.{}.html", base, module_path, item_name),
ItemKind::Trait => format!("{}/{}/trait.{}.html", base, module_path, item_name),
ItemKind::Function => format!("{}/{}/fn.{}.html", base, module_path, item_name),
ItemKind::TypeAlias => format!("{}/{}/type.{}.html", base, module_path, item_name),
ItemKind::Constant => format!("{}/{}/constant.{}.html", base, module_path, item_name),
ItemKind::Static => format!("{}/{}/static.{}.html", base, module_path, item_name),
ItemKind::Union => format!("{}/{}/union.{}.html", base, module_path, item_name),
ItemKind::Macro | ItemKind::ProcAttribute | ItemKind::ProcDerive => {
format!("{}/{}/macro.{}.html", base, module_path, item_name)
}
ItemKind::Primitive => format!("{}/{}/primitive.{}.html", base, crate_name, item_name),
_ => {
format!("{}/{}/struct.{}.html", base, module_path, item_name)
}
}
}
fn generate_heuristic_url(&self, path: &str) -> String {
self.generate_url_from_path_and_kind(path, rustdoc_types::ItemKind::Struct)
}
fn generate_search_url(&self, path: &str) -> String {
let parts: Vec<&str> = path.split("::").collect();
if parts.is_empty() {
return String::new();
}
let crate_name = parts[0];
let is_std = matches!(crate_name, "std" | "core" | "alloc" | "proc_macro");
let base = if is_std {
"https://doc.rust-lang.org/nightly".to_string()
} else {
format!("https://docs.rs/{}/latest", crate_name)
};
let module_path = if parts.len() > 2 {
parts[1..parts.len() - 1].join("/")
} else if parts.len() == 2 {
String::new()
} else {
String::new()
};
let index_path = if module_path.is_empty() {
format!("{}/{}/index.html", base, crate_name)
} else {
format!("{}/{}/{}/index.html", base, crate_name, module_path)
};
use percent_encoding::{NON_ALPHANUMERIC, utf8_percent_encode};
format!(
"{}?search={}",
index_path,
utf8_percent_encode(path, NON_ALPHANUMERIC)
)
}
fn generate_url_from_html_root(
&self,
html_root: &str,
path: &[String],
kind: rustdoc_types::ItemKind,
) -> String {
let html_root = html_root.trim_end_matches('/');
if path.is_empty() {
return html_root.to_string();
}
let crate_name = &path[0];
let remaining_parts = &path[1..];
if remaining_parts.is_empty() {
return format!("{}/{}/index.html", html_root, crate_name);
}
let item_name = &remaining_parts[remaining_parts.len() - 1];
let module_parts = &remaining_parts[..remaining_parts.len() - 1];
let module_path = if module_parts.is_empty() {
crate_name.to_string()
} else {
format!("{}/{}", crate_name, module_parts.join("/"))
};
use rustdoc_types::ItemKind;
match kind {
ItemKind::Module => {
let full_module_path = format!("{}/{}", module_path, item_name);
format!("{}/{}/index.html", html_root, full_module_path)
}
ItemKind::Struct => format!("{}/{}/struct.{}.html", html_root, module_path, item_name),
ItemKind::Enum => format!("{}/{}/enum.{}.html", html_root, module_path, item_name),
ItemKind::Trait => format!("{}/{}/trait.{}.html", html_root, module_path, item_name),
ItemKind::Function => format!("{}/{}/fn.{}.html", html_root, module_path, item_name),
ItemKind::TypeAlias => format!("{}/{}/type.{}.html", html_root, module_path, item_name),
ItemKind::Constant => {
format!("{}/{}/constant.{}.html", html_root, module_path, item_name)
}
ItemKind::Static => format!("{}/{}/static.{}.html", html_root, module_path, item_name),
ItemKind::Union => format!("{}/{}/union.{}.html", html_root, module_path, item_name),
ItemKind::Macro | ItemKind::ProcAttribute | ItemKind::ProcDerive => {
format!("{}/{}/macro.{}.html", html_root, module_path, item_name)
}
ItemKind::Primitive => {
format!("{}/{}/primitive.{}.html", html_root, crate_name, item_name)
}
_ => {
format!("{}/{}/struct.{}.html", html_root, module_path, item_name)
}
}
}
pub(crate) fn docs_to_show<'a>(
&'a self,
item: DocRef<'a, Item>,
truncation_level: TruncationLevel,
) -> Option<Vec<DocumentNode<'a>>> {
let docs = item.docs.as_deref()?;
if docs.is_empty() {
return None;
}
let nodes = self.render_docs(item, docs);
Some(vec![DocumentNode::truncated_block(nodes, truncation_level)])
}
pub(crate) fn count_lines(&self, text: &str) -> usize {
if text.is_empty() {
0
} else {
text.lines().count()
}
}
pub(crate) fn truncate_to_paragraph_or_lines(&self, text: &str, max_lines: usize) -> String {
if let Some(first_break) = text.find("\n\n") {
let after_first_break = &text[first_break + 2..];
if let Some(second_break_offset) = after_first_break.find("\n\n") {
let second_break_pos = first_break + 2 + second_break_offset;
let first_section = &text[..second_break_pos];
let first_section_lines = self.count_lines(first_section);
if first_section_lines <= max_lines {
return first_section.to_string();
}
}
}
let lines: Vec<&str> = text.lines().collect();
let cutoff = max_lines.min(lines.len());
lines[..cutoff].join("\n")
}
}