pyo3_stub_gen/generate/
docstring.rs

1use std::fmt;
2
3/// Normalize a docstring by trimming outer whitespace and dedenting.
4///
5/// Implements Python's inspect.cleandoc() behavior:
6/// 1. Trim leading/trailing whitespace from the entire string
7/// 2. Find minimum indentation of non-empty lines (skip first line)
8/// 3. Remove that indentation from all lines
9///
10/// # Examples
11/// ```
12/// # use pyo3_stub_gen::generate::normalize_docstring;
13/// let doc = r#"
14///     First line
15///     Second line
16///         Indented line
17/// "#;
18/// let normalized = normalize_docstring(doc);
19/// assert_eq!(normalized, "First line\nSecond line\n    Indented line");
20/// ```
21pub fn normalize_docstring(doc: &str) -> String {
22    let doc = doc.trim();
23    if doc.is_empty() {
24        return String::new();
25    }
26
27    let lines: Vec<&str> = doc.lines().collect();
28
29    // Find minimum indentation of non-empty lines (skip first line)
30    let min_indent = lines
31        .iter()
32        .skip(1)
33        .filter(|line| !line.trim().is_empty())
34        .map(|line| line.chars().take_while(|c| c.is_whitespace()).count())
35        .min()
36        .unwrap_or(0);
37
38    // Build normalized lines with dedenting applied
39    let normalized_lines: Vec<String> = lines
40        .iter()
41        .enumerate()
42        .map(|(i, line)| {
43            if i == 0 {
44                // First line: use as-is (already trimmed by outer trim())
45                line.to_string()
46            } else if line.trim().is_empty() {
47                // Empty line: keep it but remove whitespace
48                String::new()
49            } else {
50                // Other lines: remove common indentation
51                if line.len() >= min_indent {
52                    line[min_indent..].to_string()
53                } else {
54                    line.trim_start().to_string()
55                }
56            }
57        })
58        .collect();
59
60    normalized_lines.join("\n")
61}
62
63pub fn write_docstring(f: &mut fmt::Formatter, doc: &str, indent: &str) -> fmt::Result {
64    // Docstrings should already be normalized, but trim again for safety
65    let doc = doc.trim();
66    if !doc.is_empty() {
67        writeln!(f, r#"{indent}r""""#)?;
68        for line in doc.lines() {
69            writeln!(f, "{indent}{line}")?;
70        }
71        writeln!(f, r#"{indent}""""#)?;
72    }
73    Ok(())
74}