sphinx_ultra/
python_config.rs

1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::config::BuildConfig;
7
8/// Python configuration parser that can execute conf.py files
9pub struct PythonConfigParser {
10    conf_namespace: HashMap<String, serde_json::Value>,
11}
12
13/// Represents a parsed conf.py configuration
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ConfPyConfig {
16    // Project information
17    pub project: Option<String>,
18    pub version: Option<String>,
19    pub release: Option<String>,
20    pub copyright: Option<String>,
21    pub author: Option<String>,
22
23    // General configuration
24    pub extensions: Vec<String>,
25    pub templates_path: Vec<String>,
26    pub exclude_patterns: Vec<String>,
27    pub include_patterns: Vec<String>,
28    pub source_suffix: HashMap<String, String>,
29    pub root_doc: Option<String>,
30    pub language: Option<String>,
31    pub locale_dirs: Vec<String>,
32    pub gettext_compact: Option<bool>,
33
34    // HTML output options
35    pub html_theme: Option<String>,
36    pub html_theme_options: HashMap<String, serde_json::Value>,
37    pub html_title: Option<String>,
38    pub html_short_title: Option<String>,
39    pub html_logo: Option<String>,
40    pub html_favicon: Option<String>,
41    pub html_css_files: Vec<String>,
42    pub html_js_files: Vec<String>,
43    pub html_static_path: Vec<String>,
44    pub html_extra_path: Vec<String>,
45    pub html_use_index: Option<bool>,
46    pub html_split_index: Option<bool>,
47    pub html_copy_source: Option<bool>,
48    pub html_show_sourcelink: Option<bool>,
49    pub html_sourcelink_suffix: Option<String>,
50    pub html_use_opensearch: Option<String>,
51    pub html_file_suffix: Option<String>,
52    pub html_link_suffix: Option<String>,
53    pub html_show_copyright: Option<bool>,
54    pub html_show_sphinx: Option<bool>,
55    pub html_context: HashMap<String, serde_json::Value>,
56    pub html_output_encoding: Option<String>,
57    pub html_compact_lists: Option<bool>,
58    pub html_secnumber_suffix: Option<String>,
59    pub html_search_language: Option<String>,
60    pub html_search_options: HashMap<String, serde_json::Value>,
61    pub html_search_scorer: Option<String>,
62    pub html_scaled_image_link: Option<bool>,
63    pub html_baseurl: Option<String>,
64    pub html_codeblock_linenos_style: Option<String>,
65    pub html_math_renderer: Option<String>,
66    pub html_math_renderer_options: HashMap<String, serde_json::Value>,
67
68    // LaTeX output options
69    pub latex_engine: Option<String>,
70    pub latex_documents: Vec<(String, String, String, String, String)>,
71    pub latex_logo: Option<String>,
72    pub latex_appendices: Vec<String>,
73    pub latex_domain_indices: Option<bool>,
74    pub latex_show_pagerefs: Option<bool>,
75    pub latex_show_urls: Option<String>,
76    pub latex_use_latex_multicolumn: Option<bool>,
77    pub latex_use_xindy: Option<bool>,
78    pub latex_toplevel_sectioning: Option<String>,
79    pub latex_docclass: HashMap<String, String>,
80    pub latex_additional_files: Vec<String>,
81    pub latex_elements: HashMap<String, String>,
82
83    // ePub output options
84    pub epub_title: Option<String>,
85    pub epub_author: Option<String>,
86    pub epub_language: Option<String>,
87    pub epub_publisher: Option<String>,
88    pub epub_copyright: Option<String>,
89    pub epub_identifier: Option<String>,
90    pub epub_scheme: Option<String>,
91    pub epub_uid: Option<String>,
92    pub epub_cover: Option<(String, String)>,
93    pub epub_css_files: Vec<String>,
94    pub epub_pre_files: Vec<(String, String)>,
95    pub epub_post_files: Vec<(String, String)>,
96    pub epub_exclude_files: Vec<String>,
97    pub epub_tocdepth: Option<i32>,
98    pub epub_tocdup: Option<bool>,
99    pub epub_tocscope: Option<String>,
100    pub epub_fix_images: Option<bool>,
101    pub epub_max_image_width: Option<i32>,
102    pub epub_show_urls: Option<String>,
103    pub epub_use_index: Option<bool>,
104    pub epub_description: Option<String>,
105    pub epub_contributor: Option<String>,
106    pub epub_writing_mode: Option<String>,
107
108    // Extension-specific configurations
109    pub extension_configs: HashMap<String, HashMap<String, serde_json::Value>>,
110
111    // Build options
112    pub needs_sphinx: Option<String>,
113    pub needs_extensions: HashMap<String, String>,
114    pub manpages_url: Option<String>,
115    pub nitpicky: Option<bool>,
116    pub nitpick_ignore: Vec<(String, String)>,
117    pub nitpick_ignore_regex: Vec<(String, String)>,
118    pub numfig: Option<bool>,
119    pub numfig_format: HashMap<String, String>,
120    pub numfig_secnum_depth: Option<i32>,
121    pub math_number_all: Option<bool>,
122    pub math_eqref_format: Option<String>,
123    pub math_numfig: Option<bool>,
124    pub tls_verify: Option<bool>,
125    pub tls_cacerts: Option<String>,
126    pub user_agent: Option<String>,
127
128    // Internationalization
129    pub gettext_uuid: Option<bool>,
130    pub gettext_location: Option<bool>,
131    pub gettext_auto_build: Option<bool>,
132    pub gettext_additional_targets: Vec<String>,
133
134    // Custom configurations (catch-all for extension-specific or custom settings)
135    pub custom_configs: HashMap<String, serde_json::Value>,
136}
137
138impl PythonConfigParser {
139    /// Create a new Python configuration parser
140    pub fn new() -> Result<Self> {
141        let conf_namespace = HashMap::new();
142
143        Ok(Self { conf_namespace })
144    }
145
146    /// Parse a conf.py file and extract configuration
147    pub fn parse_conf_py<P: AsRef<Path>>(&mut self, conf_py_path: P) -> Result<ConfPyConfig> {
148        let conf_py_path = conf_py_path.as_ref();
149        let _conf_dir = conf_py_path
150            .parent()
151            .ok_or_else(|| anyhow!("Invalid conf.py path"))?;
152
153        // Read the conf.py file
154        let conf_py_content = std::fs::read_to_string(conf_py_path)?;
155
156        // For now, implement a simple parser that extracts basic configuration
157        // In a full implementation, this would execute the Python code
158        self.simple_parse_conf_py(&conf_py_content)?;
159
160        // Extract configuration values
161        self.extract_configuration()
162    }
163
164    /// Simple parser for basic conf.py configurations (stub implementation)
165    fn simple_parse_conf_py(&mut self, content: &str) -> Result<()> {
166        // Parse simple assignment statements like: variable = "value"
167        for line in content.lines() {
168            let line = line.trim();
169            if line.is_empty() || line.starts_with('#') {
170                continue;
171            }
172
173            // Parse simple assignments
174            if let Some((key, value)) = self.parse_simple_assignment(line) {
175                self.conf_namespace.insert(key, value);
176            }
177        }
178
179        Ok(())
180    }
181
182    /// Parse simple Python assignments
183    fn parse_simple_assignment(&self, line: &str) -> Option<(String, serde_json::Value)> {
184        if let Some(eq_pos) = line.find('=') {
185            let key = line[..eq_pos].trim().to_string();
186            let value_str = line[eq_pos + 1..].trim();
187
188            // Parse common value types
189            if value_str.starts_with('"') && value_str.ends_with('"') {
190                // String value
191                let value = value_str[1..value_str.len() - 1].to_string();
192                return Some((key, serde_json::Value::String(value)));
193            } else if value_str.starts_with('\'') && value_str.ends_with('\'') {
194                // String value with single quotes
195                let value = value_str[1..value_str.len() - 1].to_string();
196                return Some((key, serde_json::Value::String(value)));
197            } else if value_str == "True" {
198                return Some((key, serde_json::Value::Bool(true)));
199            } else if value_str == "False" {
200                return Some((key, serde_json::Value::Bool(false)));
201            } else if let Ok(num) = value_str.parse::<i64>() {
202                return Some((key, serde_json::Value::Number(num.into())));
203            } else if value_str.starts_with('[') && value_str.ends_with(']') {
204                // Simple list parsing
205                let list_content = &value_str[1..value_str.len() - 1];
206                let items: Vec<serde_json::Value> = list_content
207                    .split(',')
208                    .map(|item| {
209                        let item = item.trim();
210                        if (item.starts_with('"') && item.ends_with('"'))
211                            || (item.starts_with('\'') && item.ends_with('\''))
212                        {
213                            serde_json::Value::String(item[1..item.len() - 1].to_string())
214                        } else {
215                            serde_json::Value::String(item.to_string())
216                        }
217                    })
218                    .collect();
219                return Some((key, serde_json::Value::Array(items)));
220            }
221        }
222        None
223    }
224
225    /// Extract configuration values from the parsed Python namespace
226    fn extract_configuration(&self) -> Result<ConfPyConfig> {
227        let mut config = ConfPyConfig::default();
228
229        // Helper function to extract optional string values
230        let extract_string = |key: &str| -> Option<String> {
231            self.conf_namespace
232                .get(key)
233                .and_then(|val| val.as_str().map(|s| s.to_string()))
234        };
235
236        // Helper function to extract optional bool values
237        let extract_bool = |key: &str| -> Option<bool> {
238            self.conf_namespace.get(key).and_then(|val| val.as_bool())
239        };
240
241        // Helper function to extract optional int values
242        let extract_int = |key: &str| -> Option<i32> {
243            self.conf_namespace
244                .get(key)
245                .and_then(|val| val.as_i64().map(|i| i as i32))
246        };
247
248        // Helper function to extract list of strings
249        let extract_string_list = |key: &str| -> Vec<String> {
250            self.conf_namespace
251                .get(key)
252                .and_then(|val| val.as_array())
253                .map(|arr| {
254                    arr.iter()
255                        .filter_map(|v| v.as_str().map(|s| s.to_string()))
256                        .collect()
257                })
258                .unwrap_or_default()
259        };
260
261        // Helper function to extract dictionary
262        let extract_dict = |key: &str| -> HashMap<String, serde_json::Value> {
263            self.conf_namespace
264                .get(key)
265                .and_then(|val| val.as_object())
266                .map(|obj| obj.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
267                .unwrap_or_default()
268        };
269
270        // Extract project information
271        config.project = extract_string("project");
272        config.version = extract_string("version");
273        config.release = extract_string("release");
274        config.copyright = extract_string("copyright");
275        config.author = extract_string("author");
276
277        // Extract general configuration
278        config.extensions = extract_string_list("extensions");
279        config.templates_path = extract_string_list("templates_path");
280        config.exclude_patterns = extract_string_list("exclude_patterns");
281        config.include_patterns = extract_string_list("include_patterns");
282        config.root_doc = extract_string("root_doc").or_else(|| extract_string("master_doc"));
283        config.language = extract_string("language");
284        config.locale_dirs = extract_string_list("locale_dirs");
285        config.gettext_compact = extract_bool("gettext_compact");
286
287        // Extract HTML output options
288        config.html_theme = extract_string("html_theme");
289        config.html_theme_options = extract_dict("html_theme_options");
290        config.html_title = extract_string("html_title");
291        config.html_short_title = extract_string("html_short_title");
292        config.html_logo = extract_string("html_logo");
293        config.html_favicon = extract_string("html_favicon");
294        config.html_css_files = extract_string_list("html_css_files");
295        config.html_js_files = extract_string_list("html_js_files");
296        config.html_static_path = extract_string_list("html_static_path");
297        config.html_extra_path = extract_string_list("html_extra_path");
298        config.html_use_index = extract_bool("html_use_index");
299        config.html_split_index = extract_bool("html_split_index");
300        config.html_copy_source = extract_bool("html_copy_source");
301        config.html_show_sourcelink = extract_bool("html_show_sourcelink");
302        config.html_sourcelink_suffix = extract_string("html_sourcelink_suffix");
303        config.html_use_opensearch = extract_string("html_use_opensearch");
304        config.html_file_suffix = extract_string("html_file_suffix");
305        config.html_link_suffix = extract_string("html_link_suffix");
306        config.html_show_copyright = extract_bool("html_show_copyright");
307        config.html_show_sphinx = extract_bool("html_show_sphinx");
308        config.html_context = extract_dict("html_context");
309        config.html_output_encoding = extract_string("html_output_encoding");
310        config.html_compact_lists = extract_bool("html_compact_lists");
311        config.html_secnumber_suffix = extract_string("html_secnumber_suffix");
312        config.html_search_language = extract_string("html_search_language");
313        config.html_search_options = extract_dict("html_search_options");
314        config.html_search_scorer = extract_string("html_search_scorer");
315        config.html_scaled_image_link = extract_bool("html_scaled_image_link");
316        config.html_baseurl = extract_string("html_baseurl");
317        config.html_codeblock_linenos_style = extract_string("html_codeblock_linenos_style");
318        config.html_math_renderer = extract_string("html_math_renderer");
319        config.html_math_renderer_options = extract_dict("html_math_renderer_options");
320
321        // Extract build options
322        config.needs_sphinx = extract_string("needs_sphinx");
323        config.nitpicky = extract_bool("nitpicky");
324        config.numfig = extract_bool("numfig");
325        config.numfig_secnum_depth = extract_int("numfig_secnum_depth");
326        config.math_number_all = extract_bool("math_number_all");
327        config.math_eqref_format = extract_string("math_eqref_format");
328        config.math_numfig = extract_bool("math_numfig");
329        config.tls_verify = extract_bool("tls_verify");
330        config.tls_cacerts = extract_string("tls_cacerts");
331        config.user_agent = extract_string("user_agent");
332
333        // Extract internationalization
334        config.gettext_uuid = extract_bool("gettext_uuid");
335        config.gettext_location = extract_bool("gettext_location");
336        config.gettext_auto_build = extract_bool("gettext_auto_build");
337        config.gettext_additional_targets = extract_string_list("gettext_additional_targets");
338
339        // Extract custom configurations
340        for (key, value) in &self.conf_namespace {
341            if !Self::is_standard_config_key(key) {
342                config.custom_configs.insert(key.clone(), value.clone());
343            }
344        }
345
346        Ok(config)
347    }
348
349    /// Check if a configuration key is a standard Sphinx configuration
350    fn is_standard_config_key(key: &str) -> bool {
351        matches!(
352            key,
353            "project"
354                | "version"
355                | "release"
356                | "copyright"
357                | "author"
358                | "extensions"
359                | "templates_path"
360                | "exclude_patterns"
361                | "include_patterns"
362                | "source_suffix"
363                | "root_doc"
364                | "master_doc"
365                | "language"
366                | "locale_dirs"
367                | "gettext_compact"
368                | "html_theme"
369                | "html_theme_options"
370                | "html_title"
371                | "html_short_title"
372                | "html_logo"
373                | "html_favicon"
374                | "html_css_files"
375                | "html_js_files"
376                | "html_static_path"
377                | "html_extra_path"
378                | "html_use_index"
379                | "html_split_index"
380                | "html_copy_source"
381                | "html_show_sourcelink"
382                | "html_sourcelink_suffix"
383                | "html_use_opensearch"
384                | "html_file_suffix"
385                | "html_link_suffix"
386                | "html_show_copyright"
387                | "html_show_sphinx"
388                | "html_context"
389                | "html_output_encoding"
390                | "html_compact_lists"
391                | "html_secnumber_suffix"
392                | "html_search_language"
393                | "html_search_options"
394                | "html_search_scorer"
395                | "html_scaled_image_link"
396                | "html_baseurl"
397                | "html_codeblock_linenos_style"
398                | "html_math_renderer"
399                | "html_math_renderer_options"
400                | "needs_sphinx"
401                | "nitpicky"
402                | "numfig"
403                | "numfig_secnum_depth"
404                | "math_number_all"
405                | "math_eqref_format"
406                | "math_numfig"
407                | "tls_verify"
408                | "tls_cacerts"
409                | "user_agent"
410                | "gettext_uuid"
411                | "gettext_location"
412                | "gettext_auto_build"
413                | "gettext_additional_targets"
414        )
415    }
416}
417
418impl Default for ConfPyConfig {
419    fn default() -> Self {
420        Self {
421            project: None,
422            version: None,
423            release: None,
424            copyright: None,
425            author: None,
426            extensions: Vec::new(),
427            templates_path: vec!["_templates".to_string()],
428            exclude_patterns: Vec::new(),
429            include_patterns: vec!["**".to_string()], // Sphinx default
430            source_suffix: HashMap::new(),
431            root_doc: Some("index".to_string()),
432            language: None,
433            locale_dirs: vec!["locales".to_string()],
434            gettext_compact: Some(true),
435            html_theme: Some("alabaster".to_string()),
436            html_theme_options: HashMap::new(),
437            html_title: None,
438            html_short_title: None,
439            html_logo: None,
440            html_favicon: None,
441            html_css_files: Vec::new(),
442            html_js_files: Vec::new(),
443            html_static_path: vec!["_static".to_string()],
444            html_extra_path: Vec::new(),
445            html_use_index: Some(true),
446            html_split_index: Some(false),
447            html_copy_source: Some(true),
448            html_show_sourcelink: Some(true),
449            html_sourcelink_suffix: Some(".txt".to_string()),
450            html_use_opensearch: None,
451            html_file_suffix: Some(".html".to_string()),
452            html_link_suffix: Some(".html".to_string()),
453            html_show_copyright: Some(true),
454            html_show_sphinx: Some(true),
455            html_context: HashMap::new(),
456            html_output_encoding: Some("utf-8".to_string()),
457            html_compact_lists: Some(true),
458            html_secnumber_suffix: Some(". ".to_string()),
459            html_search_language: None,
460            html_search_options: HashMap::new(),
461            html_search_scorer: None,
462            html_scaled_image_link: Some(true),
463            html_baseurl: None,
464            html_codeblock_linenos_style: Some("table".to_string()),
465            html_math_renderer: Some("mathjax".to_string()),
466            html_math_renderer_options: HashMap::new(),
467            latex_engine: Some("pdflatex".to_string()),
468            latex_documents: Vec::new(),
469            latex_logo: None,
470            latex_appendices: Vec::new(),
471            latex_domain_indices: Some(true),
472            latex_show_pagerefs: Some(false),
473            latex_show_urls: Some("no".to_string()),
474            latex_use_latex_multicolumn: Some(false),
475            latex_use_xindy: Some(false),
476            latex_toplevel_sectioning: None,
477            latex_docclass: HashMap::new(),
478            latex_additional_files: Vec::new(),
479            latex_elements: HashMap::new(),
480            epub_title: None,
481            epub_author: None,
482            epub_language: None,
483            epub_publisher: None,
484            epub_copyright: None,
485            epub_identifier: None,
486            epub_scheme: None,
487            epub_uid: None,
488            epub_cover: None,
489            epub_css_files: Vec::new(),
490            epub_pre_files: Vec::new(),
491            epub_post_files: Vec::new(),
492            epub_exclude_files: Vec::new(),
493            epub_tocdepth: Some(3),
494            epub_tocdup: Some(true),
495            epub_tocscope: Some("default".to_string()),
496            epub_fix_images: Some(false),
497            epub_max_image_width: Some(0),
498            epub_show_urls: Some("inline".to_string()),
499            epub_use_index: Some(true),
500            epub_description: None,
501            epub_contributor: None,
502            epub_writing_mode: Some("horizontal".to_string()),
503            extension_configs: HashMap::new(),
504            needs_sphinx: None,
505            needs_extensions: HashMap::new(),
506            manpages_url: None,
507            nitpicky: Some(false),
508            nitpick_ignore: Vec::new(),
509            nitpick_ignore_regex: Vec::new(),
510            numfig: Some(false),
511            numfig_format: HashMap::new(),
512            numfig_secnum_depth: Some(1),
513            math_number_all: Some(false),
514            math_eqref_format: None,
515            math_numfig: Some(true),
516            tls_verify: Some(true),
517            tls_cacerts: None,
518            user_agent: None,
519            gettext_uuid: Some(false),
520            gettext_location: Some(true),
521            gettext_auto_build: Some(true),
522            gettext_additional_targets: Vec::new(),
523            custom_configs: HashMap::new(),
524        }
525    }
526}
527
528impl ConfPyConfig {
529    /// Convert conf.py configuration to BuildConfig
530    pub fn to_build_config(&self) -> BuildConfig {
531        let mut config = BuildConfig::default();
532
533        // Map basic project information
534        if let Some(project) = &self.project {
535            config.project = project.clone();
536        }
537        if let Some(version) = &self.version {
538            config.version = Some(version.clone());
539        }
540        if let Some(release) = &self.release {
541            config.release = Some(release.clone());
542        }
543        if let Some(copyright) = &self.copyright {
544            config.copyright = Some(copyright.clone());
545        }
546        if let Some(language) = &self.language {
547            config.language = Some(language.clone());
548        }
549        if let Some(root_doc) = &self.root_doc {
550            config.root_doc = Some(root_doc.clone());
551        }
552
553        // Map extensions
554        config.extensions = self.extensions.clone();
555
556        // Map template paths
557        config.template_dirs = self.templates_path.iter().map(PathBuf::from).collect();
558
559        // Map static paths
560        config.static_dirs = self.html_static_path.iter().map(PathBuf::from).collect();
561        config.html_static_path = self.html_static_path.iter().map(PathBuf::from).collect();
562
563        // Map HTML configuration
564        if let Some(html_theme) = &self.html_theme {
565            config.output.html_theme = html_theme.clone();
566            config.theme.name = html_theme.clone();
567        }
568        if let Some(html_title) = &self.html_title {
569            config.html_title = Some(html_title.clone());
570        }
571        if let Some(html_short_title) = &self.html_short_title {
572            config.html_short_title = Some(html_short_title.clone());
573        }
574        if let Some(html_logo) = &self.html_logo {
575            config.html_logo = Some(html_logo.clone());
576        }
577        if let Some(html_favicon) = &self.html_favicon {
578            config.html_favicon = Some(html_favicon.clone());
579        }
580        config.html_css_files = self.html_css_files.clone();
581        config.html_js_files = self.html_js_files.clone();
582        if let Some(html_show_copyright) = self.html_show_copyright {
583            config.html_show_copyright = Some(html_show_copyright);
584        }
585        if let Some(html_show_sphinx) = self.html_show_sphinx {
586            config.html_show_sphinx = Some(html_show_sphinx);
587        }
588        if let Some(html_copy_source) = self.html_copy_source {
589            config.html_copy_source = Some(html_copy_source);
590        }
591        if let Some(html_show_sourcelink) = self.html_show_sourcelink {
592            config.html_show_sourcelink = Some(html_show_sourcelink);
593        }
594        if let Some(html_sourcelink_suffix) = &self.html_sourcelink_suffix {
595            config.html_sourcelink_suffix = Some(html_sourcelink_suffix.clone());
596        }
597        if let Some(html_use_index) = self.html_use_index {
598            config.html_use_index = Some(html_use_index);
599        }
600        if let Some(html_use_opensearch) = &self.html_use_opensearch {
601            config.html_use_opensearch = Some(!html_use_opensearch.is_empty());
602        }
603        if let Some(html_last_updated_fmt) = &self.html_context.get("last_updated") {
604            if let Some(fmt_str) = html_last_updated_fmt.as_str() {
605                config.html_last_updated_fmt = Some(fmt_str.to_string());
606            }
607        }
608
609        // Map templates path
610        config.templates_path = self.templates_path.iter().map(PathBuf::from).collect();
611
612        // Map file patterns (Sphinx compatibility)
613        config.include_patterns = if self.include_patterns.is_empty() {
614            vec!["**".to_string()] // Sphinx default
615        } else {
616            self.include_patterns.clone()
617        };
618        config.exclude_patterns = self.exclude_patterns.clone();
619
620        config
621    }
622}