use std::fmt::Write;
#[derive(Debug, Clone)]
pub struct QuickRefEntry {
pub name: String,
pub kind: &'static str,
pub anchor: String,
pub summary: String,
}
impl QuickRefEntry {
#[must_use]
pub fn new(
name: impl Into<String>,
kind: &'static str,
anchor: impl Into<String>,
summary: impl Into<String>,
) -> Self {
Self {
name: name.into(),
kind,
anchor: anchor.into(),
summary: summary.into(),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct QuickRefGenerator;
impl QuickRefGenerator {
#[must_use]
pub const fn new() -> Self {
Self
}
#[must_use]
pub fn generate(&self, entries: &[QuickRefEntry]) -> String {
if entries.is_empty() {
return String::new();
}
let mut md = String::new();
_ = write!(
md,
"## Quick Reference\n\n\
| Item | Kind | Description |\n\
|------|------|-------------|\n"
);
for entry in entries {
let escaped_summary = entry.summary.replace('|', "\\|");
_ = writeln!(
md,
"| [`{}`](#{}) | {} | {} |",
entry.name, entry.anchor, entry.kind, escaped_summary
);
}
md.push('\n');
md
}
}
#[must_use]
pub fn extract_summary(docs: Option<&str>) -> String {
let Some(docs) = docs else {
return String::new();
};
let mut collected = String::new();
let mut found_content = false;
for line in docs.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
if found_content {
break;
}
continue;
}
found_content = true;
if !collected.is_empty() {
collected.push(' ');
}
_ = write!(collected, "{trimmed}");
if let Some(sentence) = try_extract_sentence(&collected) {
return sentence.trim_end_matches([',', ';', ':']).to_string();
}
}
if collected.is_empty() {
return String::new();
}
collected.trim_end_matches([',', ';', ':']).to_string()
}
const ABBREVIATIONS: &[&str] = &[
"e.g.", "i.e.", "etc.", "vs.", "cf.", "Dr.", "Mr.", "Mrs.", "Ms.", "Jr.", "Sr.", "Inc.",
"Ltd.", "Corp.", "viz.", "approx.", "dept.", "est.", "fig.", "no.", "vol.",
];
fn try_extract_sentence(text: &str) -> Option<String> {
let mut search_start = 0;
loop {
let pos = text[search_start..].find(". ")?;
let absolute_pos = search_start + pos;
let prefix = &text[..=absolute_pos];
let is_abbreviation = ABBREVIATIONS.iter().any(|abbr| prefix.ends_with(abbr));
let is_version = absolute_pos > 0
&& text[..absolute_pos]
.chars()
.last()
.is_some_and(|c| c.is_ascii_digit())
&& text[absolute_pos + 2..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit());
if is_abbreviation || is_version {
search_start = absolute_pos + 2;
continue;
}
return Some(text[..=absolute_pos].to_string());
}
}
#[cfg(test)]
fn extract_first_sentence(text: &str) -> &str {
let mut search_start = 0;
loop {
let Some(pos) = text[search_start..].find(". ") else {
return text;
};
let absolute_pos = search_start + pos;
let prefix = &text[..=absolute_pos];
let is_abbreviation = ABBREVIATIONS.iter().any(|abbr| prefix.ends_with(abbr));
let is_version = absolute_pos > 0
&& text[..absolute_pos]
.chars()
.last()
.is_some_and(|c| c.is_ascii_digit())
&& text[absolute_pos + 2..]
.chars()
.next()
.is_some_and(|c| c.is_ascii_digit());
if is_abbreviation || is_version {
search_start = absolute_pos + 2;
continue;
}
return &text[..=absolute_pos]; }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn quick_ref_entry_new() {
let entry = QuickRefEntry::new("Parser", "struct", "parser", "A JSON parser");
assert_eq!(entry.name, "Parser");
assert_eq!(entry.kind, "struct");
assert_eq!(entry.anchor, "parser");
assert_eq!(entry.summary, "A JSON parser");
}
#[test]
fn quick_ref_empty_entries() {
let generator = QuickRefGenerator::new();
let result = generator.generate(&[]);
assert!(result.is_empty());
}
#[test]
fn quick_ref_single_entry() {
let generator = QuickRefGenerator::new();
let entries = vec![QuickRefEntry::new("Parser", "struct", "parser", "A parser")];
let result = generator.generate(&entries);
assert!(result.contains("## Quick Reference"));
assert!(result.contains("| Item | Kind | Description |"));
assert!(result.contains("| [`Parser`](#parser) | struct | A parser |"));
}
#[test]
fn quick_ref_multiple_entries() {
let generator = QuickRefGenerator::new();
let entries = vec![
QuickRefEntry::new("Parser", "struct", "parser", "A parser"),
QuickRefEntry::new("Value", "enum", "value", "A value type"),
QuickRefEntry::new("parse", "fn", "parse", "Parse a string"),
];
let result = generator.generate(&entries);
assert!(result.contains("| [`Parser`](#parser) | struct | A parser |"));
assert!(result.contains("| [`Value`](#value) | enum | A value type |"));
assert!(result.contains("| [`parse`](#parse) | fn | Parse a string |"));
}
#[test]
fn quick_ref_escapes_pipe_in_summary() {
let generator = QuickRefGenerator::new();
let entries = vec![QuickRefEntry::new(
"Choice",
"enum",
"choice",
"Either A | B",
)];
let result = generator.generate(&entries);
assert!(result.contains(r"Either A \| B"));
}
#[test]
fn extract_summary_none() {
assert_eq!(extract_summary(None), "");
}
#[test]
fn extract_summary_empty() {
assert_eq!(extract_summary(Some("")), "");
assert_eq!(extract_summary(Some(" ")), "");
assert_eq!(extract_summary(Some("\n\n")), "");
}
#[test]
fn extract_summary_single_line() {
assert_eq!(extract_summary(Some("A simple parser")), "A simple parser");
}
#[test]
fn extract_summary_first_sentence() {
assert_eq!(
extract_summary(Some("A parser. With more details.")),
"A parser."
);
}
#[test]
fn extract_summary_multiline() {
assert_eq!(
extract_summary(Some("First line.\nSecond line.")),
"First line."
);
}
#[test]
fn extract_summary_leading_whitespace() {
assert_eq!(extract_summary(Some("\n First line")), "First line");
}
#[test]
fn extract_summary_preserves_eg_abbreviation() {
assert_eq!(
extract_summary(Some("Use e.g. this method. Then do more.")),
"Use e.g. this method."
);
}
#[test]
fn extract_summary_preserves_version_numbers() {
assert_eq!(
extract_summary(Some("Version 1.0 is here. More info.")),
"Version 1.0 is here."
);
}
#[test]
fn extract_summary_strips_trailing_punctuation() {
assert_eq!(extract_summary(Some("A value,")), "A value");
assert_eq!(extract_summary(Some("A value;")), "A value");
assert_eq!(extract_summary(Some("A value:")), "A value");
}
#[test]
fn extract_summary_keeps_trailing_period() {
assert_eq!(
extract_summary(Some("A complete sentence.")),
"A complete sentence."
);
}
#[test]
fn first_sentence_no_period() {
assert_eq!(extract_first_sentence("No period here"), "No period here");
}
#[test]
fn first_sentence_single_sentence() {
assert_eq!(
extract_first_sentence("One sentence. Two sentences."),
"One sentence."
);
}
#[test]
fn first_sentence_ie_abbreviation() {
assert_eq!(
extract_first_sentence("That is i.e. an example. More text."),
"That is i.e. an example."
);
}
#[test]
fn first_sentence_version_number() {
assert_eq!(
extract_first_sentence("Supports version 2.0 and up. Details follow."),
"Supports version 2.0 and up."
);
}
#[test]
fn extract_summary_wrapped_sentence() {
assert_eq!(
extract_summary(Some(
"A long sentence that\nspans multiple lines. More text."
)),
"A long sentence that spans multiple lines."
);
}
#[test]
fn extract_summary_wrapped_no_sentence_end() {
assert_eq!(
extract_summary(Some("A sentence that\nspans lines")),
"A sentence that spans lines"
);
}
#[test]
fn extract_summary_blank_line_terminates() {
assert_eq!(
extract_summary(Some("First paragraph\n\nSecond paragraph")),
"First paragraph"
);
}
#[test]
fn extract_summary_wrapped_with_abbreviation() {
assert_eq!(
extract_summary(Some("This is e.g. an\nexample sentence. More.")),
"This is e.g. an example sentence."
);
}
#[test]
fn extract_summary_three_lines() {
assert_eq!(
extract_summary(Some("Line one\nline two\nline three. Done.")),
"Line one line two line three."
);
}
#[test]
fn extract_summary_preserves_single_sentence_behavior() {
assert_eq!(extract_summary(Some("Short. More.")), "Short.");
}
}