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