pyo3_stub_gen/docgen/
render.rs

1//! JSON rendering and Sphinx extension embedding
2
3use crate::docgen::config::DocGenConfig;
4use crate::docgen::ir::{DocItem, DocPackage};
5use crate::Result;
6use std::path::Path;
7
8/// Render DocPackage to JSON string
9pub fn render_to_json(package: &DocPackage) -> Result<String> {
10    // Normalize for deterministic output
11    let mut normalized = package.clone();
12    normalized.normalize();
13    Ok(serde_json::to_string_pretty(&normalized)?)
14}
15
16/// Copy the embedded Sphinx extension to the output directory
17pub fn copy_sphinx_extension(output_dir: &Path) -> Result<()> {
18    let extension_code = include_str!("sphinx_ext.py");
19    let ext_path = output_dir.join("pyo3_stub_gen_ext.py");
20    std::fs::write(ext_path, extension_code)?;
21    Ok(())
22}
23
24/// Generate RST files for each module
25pub fn generate_module_pages(
26    package: &DocPackage,
27    output_dir: &Path,
28    config: &DocGenConfig,
29) -> Result<()> {
30    // Sort modules to ensure consistent ordering
31    let mut module_names: Vec<_> = package.modules.keys().collect();
32    module_names.sort();
33
34    for module_name in module_names {
35        let module = &package.modules[module_name];
36
37        let mut rst_content = format!("{}\n{}\n\n", module_name, "=".repeat(module_name.len()),);
38
39        if config.separate_items {
40            // Summary directive instead of full rendering
41            rst_content.push_str(&format!(".. pyo3-api-summary:: {}\n\n", module_name));
42
43            // Collect all items that get their own pages
44            let item_pages: Vec<String> = module
45                .items
46                .iter()
47                .filter_map(|item| {
48                    let name = match item {
49                        DocItem::Class(c) => &c.name,
50                        DocItem::Function(f) => &f.name,
51                        DocItem::TypeAlias(t) => &t.name,
52                        DocItem::Variable(v) => &v.name,
53                        DocItem::Module(_) => return None,
54                    };
55                    Some(format!("   _items/{}.{}", module_name, name))
56                })
57                .collect();
58
59            // Add hidden toctree referencing item pages
60            if !item_pages.is_empty() {
61                rst_content.push_str(".. toctree::\n");
62                rst_content.push_str("   :hidden:\n\n");
63                for page in &item_pages {
64                    rst_content.push_str(page);
65                    rst_content.push('\n');
66                }
67            }
68        } else {
69            rst_content.push_str(&format!(".. pyo3-api:: {}\n", module_name));
70        }
71
72        // Convert module name to filename: mixed.main_mod -> mixed.main_mod.rst
73        let filename = format!("{}.rst", module_name);
74        let file_path = output_dir.join(&filename);
75
76        std::fs::write(file_path, rst_content)?;
77    }
78
79    Ok(())
80}
81
82/// Generate individual RST pages for each item (class, function, type alias, variable)
83///
84/// Item pages are placed in `_items/` subdirectory to avoid filename collisions
85/// with module pages (e.g., a class `pkg.Foo` vs submodule `pkg.Foo`).
86///
87/// The `_items/` directory is cleared before generation to remove stale pages
88/// from renamed or deleted items.
89pub fn generate_item_pages(package: &DocPackage, output_dir: &Path) -> Result<()> {
90    let items_dir = output_dir.join("_items");
91    if items_dir.exists() {
92        std::fs::remove_dir_all(&items_dir)?;
93    }
94    std::fs::create_dir_all(&items_dir)?;
95
96    for (module_name, module) in &package.modules {
97        for item in &module.items {
98            let (item_name, directive) = match item {
99                DocItem::Class(c) => (c.name.as_str(), "pyo3-api-class"),
100                DocItem::Function(f) => (f.name.as_str(), "pyo3-api-function"),
101                DocItem::TypeAlias(t) => (t.name.as_str(), "pyo3-api-type-alias"),
102                DocItem::Variable(v) => (v.name.as_str(), "pyo3-api-variable"),
103                DocItem::Module(_) => continue,
104            };
105
106            let rst_content = format!(
107                "{}\n{}\n\n.. {}:: {} {}\n",
108                item_name,
109                "=".repeat(item_name.len()),
110                directive,
111                module_name,
112                item_name,
113            );
114
115            let filename = format!("{}.{}.rst", module_name, item_name);
116            std::fs::write(items_dir.join(&filename), rst_content)?;
117        }
118    }
119
120    Ok(())
121}
122
123/// Generate index.rst that references all module pages
124pub fn generate_index_rst(
125    package: &DocPackage,
126    output_dir: &Path,
127    config: &DocGenConfig,
128) -> Result<()> {
129    let mut content = String::new();
130
131    // Title - use configured title or default to "{package_name} API Reference"
132    let title = if let Some(custom_title) = &config.index_title {
133        if custom_title.is_empty() {
134            "API Reference".to_string()
135        } else {
136            custom_title.clone()
137        }
138    } else {
139        format!("{} API Reference", package.name)
140    };
141
142    content.push_str(&format!("{}\n{}\n\n", title, "=".repeat(title.len())));
143
144    // Add intro message (configurable or default)
145    if let Some(intro) = &config.intro_message {
146        if !intro.is_empty() {
147            content.push_str(intro);
148            content.push_str("\n\n");
149        }
150        // Empty string -> skip intro entirely
151    } else {
152        // Default message when not configured
153        content.push_str(
154            "This is the API reference documentation generated from Rust code using `pyo3-stub-gen <https://github.com/Jij-Inc/pyo3-stub-gen>`_.\n\n",
155        );
156    }
157
158    // Create toctree
159    content.push_str(".. toctree::\n");
160    content.push_str("   :maxdepth: 2\n");
161    content.push_str("   :caption: Modules:\n\n");
162
163    // Sort modules to ensure consistent ordering
164    let mut module_names: Vec<_> = package.modules.keys().collect();
165    module_names.sort();
166
167    for module_name in module_names {
168        content.push_str(&format!("   {}\n", module_name));
169    }
170
171    let index_path = output_dir.join("index.rst");
172    std::fs::write(index_path, content)?;
173
174    Ok(())
175}