sphinx_ultra/
environment.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3use std::path::PathBuf;
4
5/// Type alias for document relations: (parent, previous, next)
6type DocumentRelations = HashMap<String, (Option<String>, Option<String>, Option<String>)>;
7
8/// Build environment that mirrors Sphinx's BuildEnvironment
9#[derive(Debug, Clone)]
10pub struct BuildEnvironment {
11    pub config: crate::config::BuildConfig,
12    pub domains: HashMap<String, Domain>,
13    pub found_docs: Vec<String>,
14    pub all_docs: HashMap<String, f64>, // docname -> mtime
15    pub dependencies: HashMap<String, HashSet<PathBuf>>,
16    pub included: HashMap<PathBuf, HashSet<String>>,
17    pub temp_data: HashMap<String, serde_json::Value>,
18    pub ref_context: HashMap<String, serde_json::Value>,
19    pub toctree_includes: HashMap<String, Vec<String>>,
20    pub files_to_rebuild: HashMap<String, HashSet<String>>,
21    pub glob_toctrees: HashSet<String>,
22    pub reread_always: HashSet<String>,
23    pub metadata: HashMap<String, HashMap<String, String>>,
24    pub titles: HashMap<String, String>,
25    pub longtitles: HashMap<String, String>,
26    pub tocs: HashMap<String, String>,
27    pub toc_secnumbers: HashMap<String, HashMap<String, Vec<u32>>>,
28    pub toc_fignumbers: HashMap<String, HashMap<String, HashMap<String, Vec<u32>>>>,
29    pub toc_num_entries: HashMap<String, usize>,
30    pub dlfiles: HashMap<String, (Option<String>, String)>,
31    pub images: HashMap<String, String>,
32}
33
34use std::collections::HashSet;
35
36impl BuildEnvironment {
37    pub fn new(config: crate::config::BuildConfig) -> Self {
38        Self {
39            config,
40            domains: HashMap::new(),
41            found_docs: Vec::new(),
42            all_docs: HashMap::new(),
43            dependencies: HashMap::new(),
44            included: HashMap::new(),
45            temp_data: HashMap::new(),
46            ref_context: HashMap::new(),
47            toctree_includes: HashMap::new(),
48            files_to_rebuild: HashMap::new(),
49            glob_toctrees: HashSet::new(),
50            reread_always: HashSet::new(),
51            metadata: HashMap::new(),
52            titles: HashMap::new(),
53            longtitles: HashMap::new(),
54            tocs: HashMap::new(),
55            toc_secnumbers: HashMap::new(),
56            toc_fignumbers: HashMap::new(),
57            toc_num_entries: HashMap::new(),
58            dlfiles: HashMap::new(),
59            images: HashMap::new(),
60        }
61    }
62
63    /// Add a document to the environment
64    pub fn add_document(&mut self, docname: String, mtime: f64) {
65        self.found_docs.push(docname.clone());
66        self.all_docs.insert(docname, mtime);
67    }
68
69    /// Get document path from docname
70    pub fn doc2path(&self, docname: &str) -> PathBuf {
71        PathBuf::from(format!("{}.rst", docname))
72    }
73
74    /// Collect relations between documents
75    pub fn collect_relations(&self) -> DocumentRelations {
76        // TODO: Implement relation collection from toctree
77        HashMap::new()
78    }
79
80    /// Check if document needs to be updated
81    pub fn doc_needs_update(&self, docname: &str, source_path: &PathBuf) -> bool {
82        // Check if document exists in environment
83        if !self.all_docs.contains_key(docname) {
84            return true;
85        }
86
87        // Check modification time
88        if let Ok(metadata) = std::fs::metadata(source_path) {
89            if let Ok(mtime) = metadata.modified() {
90                let file_mtime = mtime
91                    .duration_since(std::time::UNIX_EPOCH)
92                    .unwrap()
93                    .as_secs_f64();
94                if let Some(&env_mtime) = self.all_docs.get(docname) {
95                    return file_mtime > env_mtime;
96                }
97            }
98        }
99
100        true
101    }
102
103    /// Update domain object
104    pub fn update_domain_object(
105        &mut self,
106        domain_name: &str,
107        obj_type: &str,
108        object: DomainObject,
109    ) {
110        let domain = self
111            .domains
112            .entry(domain_name.to_string())
113            .or_insert_with(|| Domain::new(domain_name));
114        domain.add_object(obj_type, object);
115    }
116
117    /// Get all objects from all domains
118    pub fn get_all_objects(&self) -> Vec<&DomainObject> {
119        let mut objects = Vec::new();
120        for domain in self.domains.values() {
121            objects.extend(domain.get_all_objects());
122        }
123        objects
124    }
125}
126
127/// Domain represents a Sphinx domain (py, cpp, js, std, etc.)
128#[derive(Debug, Clone)]
129pub struct Domain {
130    pub name: String,
131    pub label: String,
132    pub object_types: HashMap<String, ObjectType>,
133    pub objects: HashMap<String, Vec<DomainObject>>,
134    pub indices: Vec<DomainIndex>,
135}
136
137impl Domain {
138    pub fn new(name: &str) -> Self {
139        Self {
140            name: name.to_string(),
141            label: name.to_string(),
142            object_types: HashMap::new(),
143            objects: HashMap::new(),
144            indices: Vec::new(),
145        }
146    }
147
148    pub fn add_object(&mut self, obj_type: &str, object: DomainObject) {
149        self.objects
150            .entry(obj_type.to_string())
151            .or_default()
152            .push(object);
153    }
154
155    pub fn get_objects(&self) -> Vec<&DomainObject> {
156        let mut all_objects = Vec::new();
157        for objects in self.objects.values() {
158            all_objects.extend(objects);
159        }
160        all_objects
161    }
162
163    pub fn get_all_objects(&self) -> Vec<&DomainObject> {
164        self.get_objects()
165    }
166
167    pub fn get_objects_by_type(&self, obj_type: &str) -> Option<&Vec<DomainObject>> {
168        self.objects.get(obj_type)
169    }
170}
171
172/// Object type definition in a domain
173#[derive(Debug, Clone, Serialize, Deserialize)]
174pub struct ObjectType {
175    pub lname: String,
176    pub roles: Vec<String>,
177    pub attrs: HashMap<String, bool>,
178}
179
180/// Domain object (function, class, module, etc.)
181#[derive(Debug, Clone, Serialize, Deserialize)]
182pub struct DomainObject {
183    pub name: String,
184    pub display_name: Option<String>,
185    pub object_type: String,
186    pub docname: String,
187    pub anchor: Option<String>,
188    pub priority: i32,
189    pub description: Option<String>,
190    pub signature: Option<String>,
191    pub deprecated: bool,
192}
193
194impl DomainObject {
195    pub fn new(
196        name: String,
197        object_type: String,
198        docname: String,
199        anchor: Option<String>,
200        priority: i32,
201    ) -> Self {
202        Self {
203            name,
204            display_name: None,
205            object_type,
206            docname,
207            anchor,
208            priority,
209            description: None,
210            signature: None,
211            deprecated: false,
212        }
213    }
214
215    pub fn with_display_name(mut self, display_name: String) -> Self {
216        self.display_name = Some(display_name);
217        self
218    }
219
220    pub fn with_description(mut self, description: String) -> Self {
221        self.description = Some(description);
222        self
223    }
224
225    pub fn with_signature(mut self, signature: String) -> Self {
226        self.signature = Some(signature);
227        self
228    }
229
230    pub fn with_deprecated(mut self, deprecated: bool) -> Self {
231        self.deprecated = deprecated;
232        self
233    }
234}
235
236/// Domain index for generating index pages
237#[derive(Debug, Clone)]
238pub struct DomainIndex {
239    pub name: String,
240    pub localname: String,
241    pub shortname: Option<String>,
242    pub entries: Vec<IndexEntry>,
243}
244
245#[derive(Debug, Clone, Serialize, Deserialize)]
246pub struct IndexEntry {
247    pub name: String,
248    pub subentries: Vec<IndexEntry>,
249    pub uri: String,
250    pub display_name: String,
251    pub extra: Option<String>,
252}
253
254/// Create standard domains that mirror Sphinx's built-in domains
255pub fn create_standard_domains() -> HashMap<String, Domain> {
256    let mut domains = HashMap::new();
257
258    // Python domain
259    let mut py_domain = Domain::new("py");
260    py_domain.object_types.insert(
261        "module".to_string(),
262        ObjectType {
263            lname: "module".to_string(),
264            roles: vec!["mod".to_string(), "obj".to_string()],
265            attrs: HashMap::new(),
266        },
267    );
268    py_domain.object_types.insert(
269        "function".to_string(),
270        ObjectType {
271            lname: "function".to_string(),
272            roles: vec!["func".to_string(), "obj".to_string()],
273            attrs: HashMap::new(),
274        },
275    );
276    py_domain.object_types.insert(
277        "class".to_string(),
278        ObjectType {
279            lname: "class".to_string(),
280            roles: vec!["class".to_string(), "obj".to_string()],
281            attrs: HashMap::new(),
282        },
283    );
284    py_domain.object_types.insert(
285        "method".to_string(),
286        ObjectType {
287            lname: "method".to_string(),
288            roles: vec!["meth".to_string(), "obj".to_string()],
289            attrs: HashMap::new(),
290        },
291    );
292    py_domain.object_types.insert(
293        "attribute".to_string(),
294        ObjectType {
295            lname: "attribute".to_string(),
296            roles: vec!["attr".to_string(), "obj".to_string()],
297            attrs: HashMap::new(),
298        },
299    );
300    py_domain.object_types.insert(
301        "exception".to_string(),
302        ObjectType {
303            lname: "exception".to_string(),
304            roles: vec!["exc".to_string(), "obj".to_string()],
305            attrs: HashMap::new(),
306        },
307    );
308    py_domain.object_types.insert(
309        "data".to_string(),
310        ObjectType {
311            lname: "data".to_string(),
312            roles: vec!["data".to_string(), "obj".to_string()],
313            attrs: HashMap::new(),
314        },
315    );
316    domains.insert("py".to_string(), py_domain);
317
318    // C++ domain
319    let mut cpp_domain = Domain::new("cpp");
320    cpp_domain.object_types.insert(
321        "class".to_string(),
322        ObjectType {
323            lname: "class".to_string(),
324            roles: vec!["class".to_string()],
325            attrs: HashMap::new(),
326        },
327    );
328    cpp_domain.object_types.insert(
329        "function".to_string(),
330        ObjectType {
331            lname: "function".to_string(),
332            roles: vec!["func".to_string()],
333            attrs: HashMap::new(),
334        },
335    );
336    cpp_domain.object_types.insert(
337        "type".to_string(),
338        ObjectType {
339            lname: "type".to_string(),
340            roles: vec!["type".to_string()],
341            attrs: HashMap::new(),
342        },
343    );
344    domains.insert("cpp".to_string(), cpp_domain);
345
346    // JavaScript domain
347    let mut js_domain = Domain::new("js");
348    js_domain.object_types.insert(
349        "module".to_string(),
350        ObjectType {
351            lname: "module".to_string(),
352            roles: vec!["mod".to_string()],
353            attrs: HashMap::new(),
354        },
355    );
356    js_domain.object_types.insert(
357        "class".to_string(),
358        ObjectType {
359            lname: "class".to_string(),
360            roles: vec!["class".to_string()],
361            attrs: HashMap::new(),
362        },
363    );
364    js_domain.object_types.insert(
365        "function".to_string(),
366        ObjectType {
367            lname: "function".to_string(),
368            roles: vec!["func".to_string()],
369            attrs: HashMap::new(),
370        },
371    );
372    js_domain.object_types.insert(
373        "method".to_string(),
374        ObjectType {
375            lname: "method".to_string(),
376            roles: vec!["meth".to_string()],
377            attrs: HashMap::new(),
378        },
379    );
380    js_domain.object_types.insert(
381        "data".to_string(),
382        ObjectType {
383            lname: "data".to_string(),
384            roles: vec!["data".to_string()],
385            attrs: HashMap::new(),
386        },
387    );
388    domains.insert("js".to_string(), js_domain);
389
390    // Standard domain
391    let mut std_domain = Domain::new("std");
392    std_domain.object_types.insert(
393        "doc".to_string(),
394        ObjectType {
395            lname: "document".to_string(),
396            roles: vec!["doc".to_string()],
397            attrs: HashMap::new(),
398        },
399    );
400    std_domain.object_types.insert(
401        "label".to_string(),
402        ObjectType {
403            lname: "label".to_string(),
404            roles: vec!["ref".to_string()],
405            attrs: HashMap::new(),
406        },
407    );
408    std_domain.object_types.insert(
409        "term".to_string(),
410        ObjectType {
411            lname: "term".to_string(),
412            roles: vec!["term".to_string()],
413            attrs: HashMap::new(),
414        },
415    );
416    std_domain.object_types.insert(
417        "cmdoption".to_string(),
418        ObjectType {
419            lname: "command line option".to_string(),
420            roles: vec!["option".to_string()],
421            attrs: HashMap::new(),
422        },
423    );
424    std_domain.object_types.insert(
425        "envvar".to_string(),
426        ObjectType {
427            lname: "environment variable".to_string(),
428            roles: vec!["envvar".to_string()],
429            attrs: HashMap::new(),
430        },
431    );
432    domains.insert("std".to_string(), std_domain);
433
434    domains
435}
436
437#[cfg(test)]
438mod tests {
439    use super::*;
440
441    #[test]
442    fn test_build_environment_creation() {
443        let config = crate::config::BuildConfig::default();
444        let env = BuildEnvironment::new(config);
445
446        assert_eq!(env.found_docs.len(), 0);
447        assert_eq!(env.all_docs.len(), 0);
448        assert_eq!(env.domains.len(), 0);
449    }
450
451    #[test]
452    fn test_domain_object_creation() {
453        let obj = DomainObject::new(
454            "test_function".to_string(),
455            "function".to_string(),
456            "test_module".to_string(),
457            Some("test_function".to_string()),
458            1,
459        );
460
461        assert_eq!(obj.name, "test_function");
462        assert_eq!(obj.object_type, "function");
463        assert_eq!(obj.docname, "test_module");
464        assert_eq!(obj.anchor, Some("test_function".to_string()));
465        assert_eq!(obj.priority, 1);
466    }
467
468    #[test]
469    fn test_domain_creation() {
470        let mut domain = Domain::new("py");
471
472        let obj = DomainObject::new(
473            "test_func".to_string(),
474            "function".to_string(),
475            "test".to_string(),
476            None,
477            1,
478        );
479
480        domain.add_object("function", obj);
481
482        assert_eq!(domain.objects.len(), 1);
483        assert_eq!(domain.get_objects().len(), 1);
484    }
485
486    #[test]
487    fn test_standard_domains() {
488        let domains = create_standard_domains();
489
490        assert!(domains.contains_key("py"));
491        assert!(domains.contains_key("cpp"));
492        assert!(domains.contains_key("js"));
493        assert!(domains.contains_key("std"));
494
495        let py_domain = &domains["py"];
496        assert!(py_domain.object_types.contains_key("module"));
497        assert!(py_domain.object_types.contains_key("function"));
498        assert!(py_domain.object_types.contains_key("class"));
499    }
500}