use std::fmt::Write;
use crate::types::{DisplayItem, DisplayLimits, GroupedItems, IndexItem, TraitImplInfo};
use super::{
feature_gate_suffix, strip_markdown, summary_with_gate, trim_trailing_newlines, truncate_doc,
};
pub(crate) fn render_text(
display: &DisplayItem<'_>,
limits: &DisplayLimits,
reexport_info: Option<(&str, &str)>,
) -> String {
match display {
DisplayItem::Crate { item, children } => render_crate(item, children, limits),
DisplayItem::Module { item, children } => render_module(item, children, limits),
DisplayItem::Type {
item,
methods,
variants,
trait_impls,
} => render_type(item, methods, variants, trait_impls, limits, reexport_info),
DisplayItem::Trait {
item,
required_methods,
provided_methods,
} => render_trait(
item,
required_methods,
provided_methods,
limits,
reexport_info,
),
DisplayItem::Leaf { item } => render_leaf(item, limits, reexport_info),
}
}
fn render_crate(item: &IndexItem, children: &GroupedItems<'_>, limits: &DisplayLimits) -> String {
let mut out = String::new();
let gate = feature_gate_suffix(item.feature_gate.as_ref());
let _ = writeln!(out, "crate {}{gate}", item.name);
if !item.docs.is_empty() {
let stripped = strip_markdown(&item.docs);
let truncated = truncate_doc(&stripped, limits);
out.push('\n');
out.push_str(&truncated);
out.push('\n');
}
render_grouped_children(&mut out, children);
trim_trailing_newlines(&mut out);
out
}
fn render_module(item: &IndexItem, children: &GroupedItems<'_>, limits: &DisplayLimits) -> String {
let mut out = String::new();
let gate = feature_gate_suffix(item.feature_gate.as_ref());
let _ = writeln!(out, "mod {}{gate}", item.path);
if !item.docs.is_empty() {
let stripped = strip_markdown(&item.docs);
let truncated = truncate_doc(&stripped, limits);
out.push('\n');
out.push_str(&truncated);
out.push('\n');
}
render_grouped_children(&mut out, children);
trim_trailing_newlines(&mut out);
out
}
fn render_type(
item: &IndexItem,
methods: &[&IndexItem],
variants: &[&IndexItem],
trait_impls: &[TraitImplInfo],
limits: &DisplayLimits,
reexport_info: Option<(&str, &str)>,
) -> String {
let mut out = String::new();
let gate = feature_gate_suffix(item.feature_gate.as_ref());
let display_path = reexport_info.map_or(item.path.as_str(), |(stub, _)| stub);
let _ = writeln!(out, "{} {}{gate}", item.kind.short_name(), display_path);
out.push('\n');
out.push_str(&item.signature);
out.push('\n');
if let Some((_, source_path)) = reexport_info {
let _ = write!(out, "\nRe-exported from {source_path}.\n");
}
if !item.docs.is_empty() {
let stripped = strip_markdown(&item.docs);
let truncated = truncate_doc(&stripped, limits);
out.push('\n');
out.push_str(&truncated);
out.push('\n');
}
if !variants.is_empty() {
out.push('\n');
out.push_str("variants:\n");
for v in variants {
render_signature_line(&mut out, v);
}
}
if !methods.is_empty() {
out.push('\n');
out.push_str("methods:\n");
for m in methods {
render_signature_line(&mut out, m);
}
}
if !trait_impls.is_empty() {
let mut sorted_impls: Vec<&TraitImplInfo> = trait_impls.iter().collect();
sorted_impls.sort_by(|a, b| {
a.is_synthetic
.cmp(&b.is_synthetic)
.then_with(|| a.trait_path.cmp(&b.trait_path))
});
out.push('\n');
out.push_str("trait implementations:\n");
for ti in &sorted_impls {
let _ = writeln!(out, " impl {}", ti.trait_path);
}
}
trim_trailing_newlines(&mut out);
out
}
fn render_trait(
item: &IndexItem,
required_methods: &[&IndexItem],
provided_methods: &[&IndexItem],
limits: &DisplayLimits,
reexport_info: Option<(&str, &str)>,
) -> String {
let mut out = String::new();
let gate = feature_gate_suffix(item.feature_gate.as_ref());
let display_path = reexport_info.map_or(item.path.as_str(), |(stub, _)| stub);
let _ = writeln!(out, "trait {display_path}{gate}");
out.push('\n');
out.push_str(&item.signature);
out.push('\n');
if let Some((_, source_path)) = reexport_info {
let _ = write!(out, "\nRe-exported from {source_path}.\n");
}
if !item.docs.is_empty() {
let stripped = strip_markdown(&item.docs);
let truncated = truncate_doc(&stripped, limits);
out.push('\n');
out.push_str(&truncated);
out.push('\n');
}
let has_required = !required_methods.is_empty();
let has_provided = !provided_methods.is_empty();
if !has_required && !has_provided {
out.push('\n');
out.push_str("(no methods)");
} else if has_required && has_provided {
out.push('\n');
out.push_str("required methods:\n");
for m in required_methods {
render_signature_line(&mut out, m);
}
out.push('\n');
out.push_str("provided methods:\n");
for m in provided_methods {
render_signature_line(&mut out, m);
}
} else {
let methods = if has_required {
required_methods
} else {
provided_methods
};
out.push('\n');
out.push_str("methods:\n");
for m in methods {
render_signature_line(&mut out, m);
}
}
trim_trailing_newlines(&mut out);
out
}
fn render_leaf(
item: &IndexItem,
limits: &DisplayLimits,
reexport_info: Option<(&str, &str)>,
) -> String {
let mut out = String::new();
let gate = feature_gate_suffix(item.feature_gate.as_ref());
let display_path = reexport_info.map_or(item.path.as_str(), |(stub, _)| stub);
let _ = writeln!(out, "{} {}{gate}", item.kind.short_name(), display_path);
out.push('\n');
out.push_str(&item.signature);
out.push('\n');
if let Some((_, source_path)) = reexport_info {
let _ = write!(out, "\nRe-exported from {source_path}.\n");
}
if !item.docs.is_empty() {
let stripped = strip_markdown(&item.docs);
let truncated = truncate_doc(&stripped, limits);
out.push('\n');
out.push_str(&truncated);
}
trim_trailing_newlines(&mut out);
out
}
fn render_grouped_children(out: &mut String, children: &GroupedItems<'_>) {
for (category, items) in children {
out.push('\n');
let _ = writeln!(out, "{}", category.header());
if category.uses_signature_display() {
for item in items {
render_signature_line(out, item);
}
} else {
for item in items {
render_name_line(out, item);
}
}
}
}
fn render_name_line(out: &mut String, item: &IndexItem) {
let name = &item.name;
match summary_with_gate(&item.summary, item.feature_gate.as_ref()) {
None => {
let _ = writeln!(out, " {name}");
}
Some(display_summary) => {
let _ = writeln!(out, " {name:<30} {display_summary}");
}
}
}
fn render_signature_line(out: &mut String, item: &IndexItem) {
let sig = &item.signature;
match summary_with_gate(&item.summary, item.feature_gate.as_ref()) {
None => {
let _ = writeln!(out, " {sig}");
}
Some(display_summary) => {
let _ = writeln!(out, " {sig:<58} {display_summary}");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::make_item_full;
use crate::types::{ChildRef, DisplayLimits, DocIndex, ItemKind, TraitImplInfo};
#[test]
fn render_crate_root_output_format() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut crate_item = make_item_full(
"mycrate",
"mycrate",
ItemKind::Module,
"",
"A framework for doing things.",
"A framework for doing things.",
);
let struct_item = make_item_full(
"Widget",
"mycrate::Widget",
ItemKind::Struct,
"pub struct Widget",
"A widget.",
"A widget.",
);
let fn_item = make_item_full(
"process",
"mycrate::process",
ItemKind::Function,
"pub fn process(x: u32) -> u32",
"Processes a value.",
"Processes a value.",
);
let mod_item = make_item_full(
"utils",
"mycrate::utils",
ItemKind::Module,
"",
"Utility helpers.",
"Utility helpers.",
);
index.add_item(struct_item);
index.add_item(fn_item);
index.add_item(mod_item);
crate_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Struct,
name: "Widget".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Function,
name: "process".to_string(),
},
ChildRef {
index: 2,
kind: ItemKind::Module,
name: "utils".to_string(),
},
];
index.add_item(crate_item);
let di = crate::render::build_display_item(&index, 3, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_module_output_format() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut mod_item = make_item_full(
"sync",
"mycrate::sync",
ItemKind::Module,
"",
"Synchronization primitives.",
"Synchronization primitives.",
);
let struct_item = make_item_full(
"Mutex",
"mycrate::sync::Mutex",
ItemKind::Struct,
"pub struct Mutex<T>",
"An asynchronous Mutex-like type.",
"An asynchronous Mutex-like type.",
);
let enum_item = make_item_full(
"TryLockError",
"mycrate::sync::TryLockError",
ItemKind::Enum,
"pub enum TryLockError",
"Error returned from try_lock.",
"Error returned from try_lock.",
);
index.add_item(struct_item);
index.add_item(enum_item);
mod_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Struct,
name: "Mutex".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Enum,
name: "TryLockError".to_string(),
},
];
index.add_item(mod_item);
let di = crate::render::build_display_item(&index, 2, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_struct_with_methods_truncated_at_15() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut children = Vec::new();
for i in 0..20 {
let name = format!("method_{i:02}");
let path = format!("mycrate::Big::method_{i:02}");
let sig = format!("pub fn method_{i:02}(&self)");
let summary = format!("Method {i}.");
let method = make_item_full(&name, &path, ItemKind::Function, &sig, "", &summary);
let idx = index.items.len();
index.add_item(method);
children.push(ChildRef {
index: idx,
kind: ItemKind::Function,
name,
});
}
let mut struct_item = make_item_full(
"Big",
"mycrate::Big",
ItemKind::Struct,
"pub struct Big",
"A struct with many methods.",
"A struct with many methods.",
);
struct_item.children = children;
index.add_item(struct_item);
let di = crate::render::build_display_item(&index, 20, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_enum_with_variants() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let v1 = make_item_full(
"Red",
"mycrate::Color::Red",
ItemKind::Variant,
"Red",
"",
"The color red.",
);
let v2 = make_item_full(
"Green",
"mycrate::Color::Green",
ItemKind::Variant,
"Green",
"",
"The color green.",
);
let v3 = make_item_full(
"Blue",
"mycrate::Color::Blue",
ItemKind::Variant,
"Blue(u8)",
"",
"The color blue with intensity.",
);
let method = make_item_full(
"name",
"mycrate::Color::name",
ItemKind::Function,
"pub fn name(&self) -> &str",
"Returns the color name.",
"Returns the color name.",
);
index.add_item(v1);
index.add_item(v2);
index.add_item(v3);
index.add_item(method);
let mut enum_item = make_item_full(
"Color",
"mycrate::Color",
ItemKind::Enum,
"pub enum Color",
"Represents a color.",
"Represents a color.",
);
enum_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Variant,
name: "Red".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Variant,
name: "Green".to_string(),
},
ChildRef {
index: 2,
kind: ItemKind::Variant,
name: "Blue".to_string(),
},
ChildRef {
index: 3,
kind: ItemKind::Function,
name: "name".to_string(),
},
];
index.add_item(enum_item);
let di = crate::render::build_display_item(&index, 4, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_trait_with_required_and_provided_methods() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut req_method = make_item_full(
"poll_read",
"mycrate::AsyncRead::poll_read",
ItemKind::Function,
"fn poll_read(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<()>>",
"Attempts to read from the source.",
"Attempts to read from the source.",
);
req_method.has_body = false;
let mut prov_method1 = make_item_full(
"chain",
"mycrate::AsyncRead::chain",
ItemKind::Function,
"fn chain<R>(self, next: R) -> Chain<Self, R>",
"Creates an adaptor which chains this stream.",
"Creates an adaptor which chains this stream.",
);
prov_method1.has_body = true;
let mut prov_method2 = make_item_full(
"take",
"mycrate::AsyncRead::take",
ItemKind::Function,
"fn take(self, limit: u64) -> Take<Self>",
"Creates an adaptor which reads at most limit bytes.",
"Creates an adaptor which reads at most limit bytes.",
);
prov_method2.has_body = true;
index.add_item(req_method);
index.add_item(prov_method1);
index.add_item(prov_method2);
let mut trait_item = make_item_full(
"AsyncRead",
"mycrate::AsyncRead",
ItemKind::Trait,
"pub trait AsyncRead",
"Read bytes from a source asynchronously.",
"Read bytes from a source asynchronously.",
);
trait_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Function,
name: "poll_read".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Function,
name: "chain".to_string(),
},
ChildRef {
index: 2,
kind: ItemKind::Function,
name: "take".to_string(),
},
];
index.add_item(trait_item);
let di = crate::render::build_display_item(&index, 3, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_function_leaf_with_full_docs() {
let item = make_item_full(
"spawn",
"mycrate::spawn",
ItemKind::Function,
"pub fn spawn<F>(future: F) -> JoinHandle<F::Output>",
"Spawns a new asynchronous task, returning a JoinHandle for it.\n\nThe provided future will start running in the background immediately\nwhen spawn is called, even if you don't await the returned JoinHandle.",
"Spawns a new asynchronous task, returning a JoinHandle for it.",
);
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_constant_leaf() {
let item = make_item_full(
"PI",
"std::f64::consts::PI",
ItemKind::Constant,
"pub const PI: f64",
"Archimedes' constant (pi = 3.14159...).",
"Archimedes' constant (pi = 3.14159...).",
);
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_feature_gate_annotation_in_listings() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut mod_item = make_item_full(
"mycrate",
"mycrate",
ItemKind::Module,
"",
"A crate.",
"A crate.",
);
let mut gated_mod = make_item_full(
"fs",
"mycrate::fs",
ItemKind::Module,
"",
"Filesystem utilities.",
"Filesystem utilities.",
);
gated_mod.feature_gate = Some("fs".to_string());
let normal_mod = make_item_full(
"io",
"mycrate::io",
ItemKind::Module,
"",
"I/O utilities.",
"I/O utilities.",
);
let mut gated_fn = make_item_full(
"read",
"mycrate::read",
ItemKind::Function,
"pub fn read(path: &str) -> Vec<u8>",
"Reads a file.",
"Reads a file.",
);
gated_fn.feature_gate = Some("fs".to_string());
index.add_item(gated_mod);
index.add_item(normal_mod);
index.add_item(gated_fn);
mod_item.children = vec![
ChildRef {
index: 0,
kind: ItemKind::Module,
name: "fs".to_string(),
},
ChildRef {
index: 1,
kind: ItemKind::Module,
name: "io".to_string(),
},
ChildRef {
index: 2,
kind: ItemKind::Function,
name: "read".to_string(),
},
];
index.add_item(mod_item);
let di = crate::render::build_display_item(&index, 3, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_truncation_at_1500_chars() {
let long_docs = format!(
"First paragraph of documentation.\n\n{}",
"This is a very long sentence that repeats. ".repeat(50)
);
let item = make_item_full(
"big_fn",
"mycrate::big_fn",
ItemKind::Function,
"pub fn big_fn()",
&long_docs,
"First paragraph of documentation.",
);
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits {
expand_all: false,
..DisplayLimits::default()
};
let output = render_text(&di, &limits, None);
assert!(
output.contains("..."),
"output should contain ... for truncation"
);
assert!(
output.len() < 2000,
"output should be truncated, got {} bytes",
output.len()
);
insta::assert_snapshot!(output);
}
#[test]
fn render_truncation_respects_utf8_boundaries() {
let long_docs = "日本語テスト。".repeat(300);
let item = make_item_full(
"unicode_fn",
"mycrate::unicode_fn",
ItemKind::Function,
"pub fn unicode_fn()",
&long_docs,
"Japanese test text.",
);
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits {
expand_all: false,
..DisplayLimits::default()
};
let output = render_text(&di, &limits, None);
assert!(output.contains("..."), "output should be truncated");
assert!(std::str::from_utf8(output.as_bytes()).is_ok());
}
#[test]
fn render_trait_only_required_uses_methods_header() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let mut method = make_item_full(
"next",
"mycrate::Iterator::next",
ItemKind::Function,
"fn next(&mut self) -> Option<Self::Item>",
"Advances the iterator.",
"Advances the iterator.",
);
method.has_body = false;
index.add_item(method);
let mut trait_item = make_item_full(
"Iterator",
"mycrate::Iterator",
ItemKind::Trait,
"pub trait Iterator",
"An interface for dealing with iterators.",
"An interface for dealing with iterators.",
);
trait_item.children = vec![ChildRef {
index: 0,
kind: ItemKind::Function,
name: "next".to_string(),
}];
index.add_item(trait_item);
let di = crate::render::build_display_item(&index, 1, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
assert!(output.contains("methods:"));
assert!(!output.contains("required methods:"));
assert!(!output.contains("provided methods:"));
}
#[test]
fn render_marker_trait_shows_no_methods() {
let item = make_item_full(
"Send",
"mycrate::Send",
ItemKind::Trait,
"pub unsafe auto trait Send",
"Types that can be transferred across thread boundaries.",
"Types that can be transferred across thread boundaries.",
);
let di = DisplayItem::Trait {
item: &item,
required_methods: vec![],
provided_methods: vec![],
};
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_leaf_no_docs() {
let item = make_item_full(
"internal_helper",
"mycrate::internal_helper",
ItemKind::Function,
"pub fn internal_helper(x: u32) -> u32",
"",
"",
);
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_struct_with_trait_impls() {
let mut index = DocIndex::new("mycrate".to_string(), "0.1.0".to_string());
let struct_item = make_item_full(
"Foo",
"mycrate::Foo",
ItemKind::Struct,
"pub struct Foo",
"A foo struct.",
"A foo struct.",
);
index.add_item(struct_item);
index.trait_impls.insert(
0,
vec![
TraitImplInfo {
trait_path: "Debug".to_string(),
is_synthetic: false,
},
TraitImplInfo {
trait_path: "Clone".to_string(),
is_synthetic: false,
},
TraitImplInfo {
trait_path: "Send".to_string(),
is_synthetic: true,
},
TraitImplInfo {
trait_path: "Sync".to_string(),
is_synthetic: true,
},
],
);
let di = crate::render::build_display_item(&index, 0, false, None);
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
#[test]
fn render_feature_gate_on_leaf_header() {
let mut item = make_item_full(
"read",
"mycrate::fs::read",
ItemKind::Function,
"pub async fn read(path: impl AsRef<Path>) -> io::Result<Vec<u8>>",
"Reads the entire contents of a file.",
"Reads the entire contents of a file.",
);
item.feature_gate = Some("fs".to_string());
let di = DisplayItem::Leaf { item: &item };
let limits = DisplayLimits::default();
let output = render_text(&di, &limits, None);
insta::assert_snapshot!(output);
}
}