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