sphinx_ultra/
extensions.rs

1use anyhow::Result;
2use serde_json::Value;
3use std::collections::HashMap;
4use std::path::PathBuf;
5
6use crate::config::BuildConfig;
7
8/// Represents a Sphinx extension
9#[derive(Debug, Clone)]
10pub struct SphinxExtension {
11    pub name: String,
12    pub module_path: String,
13    pub setup_function: Option<String>,
14    pub metadata: ExtensionMetadata,
15    pub config: HashMap<String, Value>,
16}
17
18/// Extension metadata
19#[derive(Debug, Clone)]
20pub struct ExtensionMetadata {
21    pub version: String,
22    pub parallel_read_safe: bool,
23    pub parallel_write_safe: bool,
24    pub env_version: Option<i32>,
25}
26
27/// Sphinx application context for extensions
28pub struct SphinxApp {
29    pub config: BuildConfig,
30    pub extensions: HashMap<String, SphinxExtension>,
31    pub env: SphinxEnvironment,
32}
33
34/// Sphinx build environment
35#[derive(Debug)]
36pub struct SphinxEnvironment {
37    pub docname_to_path: HashMap<String, PathBuf>,
38    pub path_to_docname: HashMap<PathBuf, String>,
39    pub dependencies: HashMap<String, Vec<String>>,
40    pub included: HashMap<String, Vec<String>>,
41    pub toctree_includes: HashMap<String, Vec<String>>,
42    pub files_to_rebuild: HashMap<String, Vec<String>>,
43    pub glob_toctrees: Vec<String>,
44    pub numbered_toctrees: Vec<String>,
45    pub metadata: HashMap<String, HashMap<String, String>>,
46}
47
48/// Extension loader and manager
49pub struct ExtensionLoader {
50    loaded_extensions: HashMap<String, SphinxExtension>,
51}
52
53impl ExtensionLoader {
54    /// Create a new extension loader
55    pub fn new() -> Result<Self> {
56        Ok(Self {
57            loaded_extensions: HashMap::new(),
58        })
59    }
60
61    /// Load a Sphinx extension by name
62    pub fn load_extension(&mut self, extension_name: &str) -> Result<SphinxExtension> {
63        if let Some(extension) = self.loaded_extensions.get(extension_name) {
64            return Ok(extension.clone());
65        }
66
67        let extension = self.import_and_setup_extension(extension_name)?;
68        self.loaded_extensions
69            .insert(extension_name.to_string(), extension.clone());
70
71        Ok(extension)
72    }
73
74    /// Import and set up a Python extension
75    fn import_and_setup_extension(&self, extension_name: &str) -> Result<SphinxExtension> {
76        // For now, create a stub extension for built-in extensions
77        // In a full implementation, this would use PyO3 to import Python modules
78
79        let metadata = ExtensionMetadata {
80            version: "1.0.0".to_string(),
81            parallel_read_safe: true,
82            parallel_write_safe: true,
83            env_version: Some(1),
84        };
85
86        Ok(SphinxExtension {
87            name: extension_name.to_string(),
88            module_path: extension_name.to_string(),
89            setup_function: Some("setup".to_string()),
90            metadata,
91            config: HashMap::new(),
92        })
93    }
94
95    /// Extract metadata from extension module
96    #[allow(dead_code)]
97    fn extract_extension_metadata(&self, _extension_name: &str) -> Result<ExtensionMetadata> {
98        // Stub implementation - in a real version this would introspect the Python module
99        Ok(ExtensionMetadata {
100            version: "1.0.0".to_string(),
101            parallel_read_safe: true,
102            parallel_write_safe: true,
103            env_version: Some(1),
104        })
105    }
106
107    /// Get all loaded extensions
108    pub fn get_loaded_extensions(&self) -> &HashMap<String, SphinxExtension> {
109        &self.loaded_extensions
110    }
111}
112
113impl SphinxApp {
114    /// Create a new Sphinx application
115    pub fn new(config: BuildConfig) -> Result<Self> {
116        let env = SphinxEnvironment::new();
117
118        Ok(Self {
119            config,
120            extensions: HashMap::new(),
121            env,
122        })
123    }
124
125    /// Add an extension to the application
126    pub fn add_extension(&mut self, extension: SphinxExtension) -> Result<()> {
127        // Call the extension's setup function if it exists
128        if let Some(setup_fn) = &extension.setup_function {
129            self.call_extension_setup(&extension, setup_fn)?;
130        }
131
132        self.extensions.insert(extension.name.clone(), extension);
133        Ok(())
134    }
135
136    /// Call an extension's setup function
137    fn call_extension_setup(&self, extension: &SphinxExtension, _setup_fn: &str) -> Result<()> {
138        // Stub implementation - in a real version this would call the Python setup function
139        println!("Setting up extension: {}", extension.name);
140        Ok(())
141    }
142
143    /// Create a configuration dictionary for Python (stub)
144    #[allow(dead_code)]
145    fn create_config_dict(&self) -> Result<HashMap<String, String>> {
146        // Stub implementation
147        let mut config_dict = HashMap::new();
148        config_dict.insert("project".to_string(), self.config.project.clone());
149        Ok(config_dict)
150    }
151
152    /// Get extension by name
153    pub fn get_extension(&self, name: &str) -> Option<&SphinxExtension> {
154        self.extensions.get(name)
155    }
156
157    /// Check if extension is loaded
158    pub fn has_extension(&self, name: &str) -> bool {
159        self.extensions.contains_key(name)
160    }
161}
162
163impl Default for SphinxEnvironment {
164    fn default() -> Self {
165        Self::new()
166    }
167}
168
169impl SphinxEnvironment {
170    /// Create a new Sphinx environment
171    pub fn new() -> Self {
172        Self {
173            docname_to_path: HashMap::new(),
174            path_to_docname: HashMap::new(),
175            dependencies: HashMap::new(),
176            included: HashMap::new(),
177            toctree_includes: HashMap::new(),
178            files_to_rebuild: HashMap::new(),
179            glob_toctrees: Vec::new(),
180            numbered_toctrees: Vec::new(),
181            metadata: HashMap::new(),
182        }
183    }
184
185    /// Add a document to the environment
186    pub fn add_document(&mut self, docname: String, path: PathBuf) {
187        self.path_to_docname.insert(path.clone(), docname.clone());
188        self.docname_to_path.insert(docname, path);
189    }
190
191    /// Get document path by name
192    pub fn get_doc_path(&self, docname: &str) -> Option<&PathBuf> {
193        self.docname_to_path.get(docname)
194    }
195
196    /// Get document name by path
197    pub fn get_docname(&self, path: &PathBuf) -> Option<&String> {
198        self.path_to_docname.get(path)
199    }
200
201    /// Add dependency between documents
202    pub fn add_dependency(&mut self, docname: String, dependency: String) {
203        self.dependencies
204            .entry(docname)
205            .or_default()
206            .push(dependency);
207    }
208
209    /// Get dependencies for a document
210    pub fn get_dependencies(&self, docname: &str) -> Option<&Vec<String>> {
211        self.dependencies.get(docname)
212    }
213
214    /// Add metadata for a document
215    pub fn add_metadata(&mut self, docname: String, key: String, value: String) {
216        self.metadata.entry(docname).or_default().insert(key, value);
217    }
218
219    /// Get metadata for a document
220    pub fn get_metadata(&self, docname: &str) -> Option<&HashMap<String, String>> {
221        self.metadata.get(docname)
222    }
223}
224
225/// Built-in Sphinx extensions that we need to handle specially
226pub struct BuiltinExtensions;
227
228impl BuiltinExtensions {
229    /// Get list of built-in Sphinx extensions
230    pub fn get_builtin_extensions() -> Vec<&'static str> {
231        vec![
232            "sphinx.ext.autodoc",
233            "sphinx.ext.autosummary",
234            "sphinx.ext.doctest",
235            "sphinx.ext.intersphinx",
236            "sphinx.ext.todo",
237            "sphinx.ext.coverage",
238            "sphinx.ext.imgmath",
239            "sphinx.ext.mathjax",
240            "sphinx.ext.ifconfig",
241            "sphinx.ext.viewcode",
242            "sphinx.ext.githubpages",
243            "sphinx.ext.napoleon",
244            "sphinx.ext.extlinks",
245            "sphinx.ext.linkcode",
246            "sphinx.ext.graphviz",
247            "sphinx.ext.inheritance_diagram",
248        ]
249    }
250
251    /// Check if an extension is built-in
252    pub fn is_builtin_extension(name: &str) -> bool {
253        Self::get_builtin_extensions().contains(&name)
254    }
255
256    /// Get default configuration for built-in extensions
257    pub fn get_default_config(extension_name: &str) -> HashMap<String, Value> {
258        let mut config = HashMap::new();
259
260        match extension_name {
261            "sphinx.ext.autodoc" => {
262                config.insert(
263                    "autodoc_default_options".to_string(),
264                    serde_json::json!({
265                        "members": true,
266                        "undoc-members": true,
267                        "show-inheritance": true
268                    }),
269                );
270                config.insert(
271                    "autodoc_member_order".to_string(),
272                    Value::String("alphabetical".to_string()),
273                );
274                config.insert(
275                    "autodoc_typehints".to_string(),
276                    Value::String("description".to_string()),
277                );
278            }
279            "sphinx.ext.autosummary" => {
280                config.insert("autosummary_generate".to_string(), Value::Bool(true));
281                config.insert(
282                    "autosummary_imported_members".to_string(),
283                    Value::Bool(false),
284                );
285            }
286            "sphinx.ext.intersphinx" => {
287                config.insert(
288                    "intersphinx_mapping".to_string(),
289                    serde_json::json!({
290                        "python": ["https://docs.python.org/3", null]
291                    }),
292                );
293                config.insert(
294                    "intersphinx_timeout".to_string(),
295                    Value::Number(serde_json::Number::from(5)),
296                );
297            }
298            "sphinx.ext.todo" => {
299                config.insert("todo_include_todos".to_string(), Value::Bool(true));
300                config.insert("todo_emit_warnings".to_string(), Value::Bool(false));
301            }
302            "sphinx.ext.napoleon" => {
303                config.insert("napoleon_google_docstring".to_string(), Value::Bool(true));
304                config.insert("napoleon_numpy_docstring".to_string(), Value::Bool(true));
305                config.insert(
306                    "napoleon_include_init_with_doc".to_string(),
307                    Value::Bool(false),
308                );
309                config.insert(
310                    "napoleon_include_private_with_doc".to_string(),
311                    Value::Bool(false),
312                );
313                config.insert(
314                    "napoleon_include_special_with_doc".to_string(),
315                    Value::Bool(true),
316                );
317                config.insert(
318                    "napoleon_use_admonition_for_examples".to_string(),
319                    Value::Bool(false),
320                );
321                config.insert(
322                    "napoleon_use_admonition_for_notes".to_string(),
323                    Value::Bool(false),
324                );
325                config.insert(
326                    "napoleon_use_admonition_for_references".to_string(),
327                    Value::Bool(false),
328                );
329                config.insert("napoleon_use_ivar".to_string(), Value::Bool(false));
330                config.insert("napoleon_use_param".to_string(), Value::Bool(true));
331                config.insert("napoleon_use_rtype".to_string(), Value::Bool(true));
332                config.insert("napoleon_use_keyword".to_string(), Value::Bool(true));
333                config.insert("napoleon_custom_sections".to_string(), Value::Array(vec![]));
334            }
335            "sphinx.ext.viewcode" => {
336                config.insert("viewcode_import".to_string(), Value::Bool(false));
337                config.insert("viewcode_enable_epub".to_string(), Value::Bool(false));
338            }
339            "sphinx.ext.imgmath" => {
340                config.insert(
341                    "imgmath_image_format".to_string(),
342                    Value::String("png".to_string()),
343                );
344                config.insert("imgmath_use_preview".to_string(), Value::Bool(false));
345                config.insert("imgmath_add_tooltips".to_string(), Value::Bool(true));
346                config.insert(
347                    "imgmath_font_size".to_string(),
348                    Value::Number(serde_json::Number::from(12)),
349                );
350            }
351            "sphinx.ext.mathjax" => {
352                config.insert("mathjax_path".to_string(), 
353                    Value::String("https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-AMS-MML_HTMLorMML".to_string()));
354                config.insert(
355                    "mathjax_config".to_string(),
356                    serde_json::json!({
357                        "tex2jax": {
358                            "inlineMath": [["$", "$"], ["\\(", "\\)"]],
359                            "displayMath": [["$$", "$$"], ["\\[", "\\]"]],
360                            "processEscapes": true,
361                            "processEnvironments": true
362                        }
363                    }),
364                );
365            }
366            _ => {}
367        }
368
369        config
370    }
371}