1use anyhow::{anyhow, Result};
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4use std::path::{Path, PathBuf};
5
6use crate::config::BuildConfig;
7
8pub struct PythonConfigParser {
10 conf_namespace: HashMap<String, serde_json::Value>,
11}
12
13#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct ConfPyConfig {
16 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 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 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 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 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 pub extension_configs: HashMap<String, HashMap<String, serde_json::Value>>,
109
110 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 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 pub custom_configs: HashMap<String, serde_json::Value>,
135}
136
137impl PythonConfigParser {
138 pub fn new() -> Result<Self> {
140 let conf_namespace = HashMap::new();
141
142 Ok(Self { conf_namespace })
143 }
144
145 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 let conf_py_content = std::fs::read_to_string(conf_py_path)?;
154
155 self.simple_parse_conf_py(&conf_py_content)?;
158
159 self.extract_configuration()
161 }
162
163 fn simple_parse_conf_py(&mut self, content: &str) -> Result<()> {
165 for line in content.lines() {
167 let line = line.trim();
168 if line.is_empty() || line.starts_with('#') {
169 continue;
170 }
171
172 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 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 if value_str.starts_with('"') && value_str.ends_with('"') {
189 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 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 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 fn extract_configuration(&self) -> Result<ConfPyConfig> {
226 let mut config = ConfPyConfig::default();
227
228 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 let extract_bool = |key: &str| -> Option<bool> {
237 self.conf_namespace.get(key).and_then(|val| val.as_bool())
238 };
239
240 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 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 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 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 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 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 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 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 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 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 pub fn to_build_config(&self) -> BuildConfig {
527 let mut config = BuildConfig::default();
528
529 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 config.extensions = self.extensions.clone();
551
552 config.template_dirs = self.templates_path.iter().map(PathBuf::from).collect();
554
555 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 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 config.templates_path = self.templates_path.iter().map(PathBuf::from).collect();
607
608 config
609 }
610}