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 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 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 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 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 pub extension_configs: HashMap<String, HashMap<String, serde_json::Value>>,
110
111 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 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 pub custom_configs: HashMap<String, serde_json::Value>,
136}
137
138impl PythonConfigParser {
139 pub fn new() -> Result<Self> {
141 let conf_namespace = HashMap::new();
142
143 Ok(Self { conf_namespace })
144 }
145
146 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 let conf_py_content = std::fs::read_to_string(conf_py_path)?;
155
156 self.simple_parse_conf_py(&conf_py_content)?;
159
160 self.extract_configuration()
162 }
163
164 fn simple_parse_conf_py(&mut self, content: &str) -> Result<()> {
166 for line in content.lines() {
168 let line = line.trim();
169 if line.is_empty() || line.starts_with('#') {
170 continue;
171 }
172
173 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 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 if value_str.starts_with('"') && value_str.ends_with('"') {
190 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 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 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 fn extract_configuration(&self) -> Result<ConfPyConfig> {
227 let mut config = ConfPyConfig::default();
228
229 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 let extract_bool = |key: &str| -> Option<bool> {
238 self.conf_namespace.get(key).and_then(|val| val.as_bool())
239 };
240
241 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 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 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 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 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 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 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 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 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 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()], 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 pub fn to_build_config(&self) -> BuildConfig {
531 let mut config = BuildConfig::default();
532
533 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 config.extensions = self.extensions.clone();
555
556 config.template_dirs = self.templates_path.iter().map(PathBuf::from).collect();
558
559 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 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 config.templates_path = self.templates_path.iter().map(PathBuf::from).collect();
611
612 config.include_patterns = if self.include_patterns.is_empty() {
614 vec!["**".to_string()] } else {
616 self.include_patterns.clone()
617 };
618 config.exclude_patterns = self.exclude_patterns.clone();
619
620 config
621 }
622}