sphinx_ultra/domains/
rst.rs

1use crate::domains::{
2    CrossReference, DomainObject, DomainValidator, ReferenceType, ReferenceValidationResult,
3};
4use crate::error::BuildError;
5/// RST Domain Implementation
6///
7/// Handles RST-specific objects and references like :doc:, :ref:, etc.
8use std::collections::HashMap;
9
10/// RST domain validator for document and section references
11pub struct RstDomain {
12    /// Documents registered in this domain
13    documents: HashMap<String, DomainObject>,
14    /// Sections registered in this domain
15    sections: HashMap<String, DomainObject>,
16}
17
18impl Default for RstDomain {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl RstDomain {
25    /// Create a new RST domain
26    pub fn new() -> Self {
27        Self {
28            documents: HashMap::new(),
29            sections: HashMap::new(),
30        }
31    }
32
33    /// Register a document
34    pub fn register_document(
35        &mut self,
36        docname: String,
37        title: String,
38        location: crate::domains::ReferenceLocation,
39    ) -> Result<(), BuildError> {
40        let object = DomainObject {
41            id: format!("doc:{}", docname),
42            name: docname.clone(),
43            object_type: "document".to_string(),
44            domain: "rst".to_string(),
45            definition_location: location,
46            qualified_name: docname.clone(),
47            metadata: {
48                let mut meta = HashMap::new();
49                meta.insert("title".to_string(), title);
50                meta
51            },
52            signature: None,
53            docstring: None,
54        };
55
56        self.documents.insert(docname, object);
57        Ok(())
58    }
59
60    /// Register a section (for :ref: targets)
61    pub fn register_section(
62        &mut self,
63        label: String,
64        title: String,
65        docname: String,
66        location: crate::domains::ReferenceLocation,
67    ) -> Result<(), BuildError> {
68        let qualified_name = format!("{}#{}", docname, label);
69        let object = DomainObject {
70            id: format!("ref:{}", label),
71            name: label.clone(),
72            object_type: "section".to_string(),
73            domain: "rst".to_string(),
74            definition_location: location,
75            qualified_name: qualified_name.clone(),
76            metadata: {
77                let mut meta = HashMap::new();
78                meta.insert("title".to_string(), title);
79                meta.insert("docname".to_string(), docname);
80                meta
81            },
82            signature: None,
83            docstring: None,
84        };
85
86        self.sections.insert(label, object);
87        Ok(())
88    }
89
90    /// Register a figure or table label
91    pub fn register_label(
92        &mut self,
93        label: String,
94        label_type: String, // "figure", "table", "code-block", etc.
95        title: Option<String>,
96        docname: String,
97        location: crate::domains::ReferenceLocation,
98    ) -> Result<(), BuildError> {
99        let qualified_name = format!("{}#{}", docname, label);
100        let object = DomainObject {
101            id: format!("{}:{}", label_type, label),
102            name: label.clone(),
103            object_type: label_type,
104            domain: "rst".to_string(),
105            definition_location: location,
106            qualified_name: qualified_name.clone(),
107            metadata: {
108                let mut meta = HashMap::new();
109                if let Some(title) = title {
110                    meta.insert("title".to_string(), title);
111                }
112                meta.insert("docname".to_string(), docname);
113                meta
114            },
115            signature: None,
116            docstring: None,
117        };
118
119        self.sections.insert(label, object);
120        Ok(())
121    }
122
123    /// Find suggestions for a broken reference
124    fn find_suggestions(&self, target: &str, ref_type: &ReferenceType) -> Vec<String> {
125        let mut suggestions = Vec::new();
126
127        let objects = match ref_type {
128            ReferenceType::Document => &self.documents,
129            ReferenceType::Section => &self.sections,
130            _ => &self.sections, // Default to sections for other types
131        };
132
133        // Look for similar names
134        for (key, obj) in objects {
135            // Exact match on key
136            if key.contains(target) || target.contains(key) {
137                suggestions.push(key.clone());
138            }
139            // Title match
140            if let Some(title) = obj.metadata.get("title") {
141                if title.to_lowercase().contains(&target.to_lowercase()) {
142                    suggestions.push(key.clone());
143                }
144            }
145        }
146
147        suggestions.sort();
148        suggestions.dedup();
149        suggestions.truncate(5); // Limit to 5 suggestions
150
151        suggestions
152    }
153}
154
155impl DomainValidator for RstDomain {
156    fn domain_name(&self) -> &str {
157        "rst"
158    }
159
160    fn supported_reference_types(&self) -> Vec<ReferenceType> {
161        vec![
162            ReferenceType::Document,
163            ReferenceType::Section,
164            ReferenceType::Custom("numref".to_string()),
165        ]
166    }
167
168    fn register_object(&mut self, object: DomainObject) -> Result<(), BuildError> {
169        match object.object_type.as_str() {
170            "document" => {
171                self.documents.insert(object.name.clone(), object);
172            }
173            "section" | "figure" | "table" | "code-block" => {
174                self.sections.insert(object.name.clone(), object);
175            }
176            _ => {
177                return Err(BuildError::ValidationError(format!(
178                    "Unknown RST object type: {}",
179                    object.object_type
180                )));
181            }
182        }
183        Ok(())
184    }
185
186    fn resolve_reference(&self, reference: &CrossReference) -> Option<DomainObject> {
187        match reference.ref_type {
188            ReferenceType::Document => {
189                // For documents, target might include .rst extension or not
190                let target = reference.target.trim_end_matches(".rst");
191                self.documents
192                    .get(target)
193                    .cloned()
194                    .or_else(|| self.documents.get(&reference.target).cloned())
195            }
196            ReferenceType::Section => self.sections.get(&reference.target).cloned(),
197            _ => {
198                // Try sections first, then documents
199                self.sections
200                    .get(&reference.target)
201                    .cloned()
202                    .or_else(|| self.documents.get(&reference.target).cloned())
203            }
204        }
205    }
206
207    fn validate_reference(&self, reference: &CrossReference) -> ReferenceValidationResult {
208        if let Some(target_object) = self.resolve_reference(reference) {
209            ReferenceValidationResult {
210                reference: reference.clone(),
211                is_valid: true,
212                target_object: Some(target_object),
213                error_message: None,
214                suggestions: Vec::new(),
215            }
216        } else {
217            let suggestions = self.find_suggestions(&reference.target, &reference.ref_type);
218            let error_message = match reference.ref_type {
219                ReferenceType::Document => {
220                    if suggestions.is_empty() {
221                        format!("Document '{}' not found", reference.target)
222                    } else {
223                        format!(
224                            "Document '{}' not found. Did you mean: {}?",
225                            reference.target,
226                            suggestions.join(", ")
227                        )
228                    }
229                }
230                ReferenceType::Section => {
231                    if suggestions.is_empty() {
232                        format!("Section label '{}' not found", reference.target)
233                    } else {
234                        format!(
235                            "Section label '{}' not found. Did you mean: {}?",
236                            reference.target,
237                            suggestions.join(", ")
238                        )
239                    }
240                }
241                _ => {
242                    if suggestions.is_empty() {
243                        format!("Reference target '{}' not found", reference.target)
244                    } else {
245                        format!(
246                            "Reference target '{}' not found. Did you mean: {}?",
247                            reference.target,
248                            suggestions.join(", ")
249                        )
250                    }
251                }
252            };
253
254            ReferenceValidationResult {
255                reference: reference.clone(),
256                is_valid: false,
257                target_object: None,
258                error_message: Some(error_message),
259                suggestions,
260            }
261        }
262    }
263
264    fn get_all_objects(&self) -> Vec<&DomainObject> {
265        let mut objects = Vec::new();
266        objects.extend(self.documents.values());
267        objects.extend(self.sections.values());
268        objects
269    }
270
271    fn clear_objects(&mut self) {
272        self.documents.clear();
273        self.sections.clear();
274    }
275}
276
277#[cfg(test)]
278mod tests {
279    use super::*;
280    use crate::domains::ReferenceLocation;
281
282    fn create_test_location() -> ReferenceLocation {
283        ReferenceLocation {
284            docname: "test.rst".to_string(),
285            lineno: Some(10),
286            column: Some(5),
287            source_path: Some("test.rst".to_string()),
288        }
289    }
290
291    #[test]
292    fn test_rst_domain_creation() {
293        let domain = RstDomain::new();
294        assert_eq!(domain.domain_name(), "rst");
295        assert!(domain
296            .supported_reference_types()
297            .contains(&ReferenceType::Document));
298        assert!(domain
299            .supported_reference_types()
300            .contains(&ReferenceType::Section));
301    }
302
303    #[test]
304    fn test_register_document() {
305        let mut domain = RstDomain::new();
306
307        let result = domain.register_document(
308            "index".to_string(),
309            "Home Page".to_string(),
310            create_test_location(),
311        );
312
313        assert!(result.is_ok());
314        assert_eq!(domain.documents.len(), 1);
315
316        let doc = domain.documents.get("index").unwrap();
317        assert_eq!(doc.name, "index");
318        assert_eq!(doc.object_type, "document");
319        assert_eq!(doc.metadata.get("title"), Some(&"Home Page".to_string()));
320    }
321
322    #[test]
323    fn test_register_section() {
324        let mut domain = RstDomain::new();
325
326        let result = domain.register_section(
327            "introduction".to_string(),
328            "Introduction".to_string(),
329            "index".to_string(),
330            create_test_location(),
331        );
332
333        assert!(result.is_ok());
334        assert_eq!(domain.sections.len(), 1);
335
336        let section = domain.sections.get("introduction").unwrap();
337        assert_eq!(section.name, "introduction");
338        assert_eq!(section.object_type, "section");
339        assert_eq!(
340            section.metadata.get("title"),
341            Some(&"Introduction".to_string())
342        );
343        assert_eq!(section.metadata.get("docname"), Some(&"index".to_string()));
344    }
345
346    #[test]
347    fn test_document_reference_validation() {
348        let mut domain = RstDomain::new();
349
350        domain
351            .register_document(
352                "getting-started".to_string(),
353                "Getting Started".to_string(),
354                create_test_location(),
355            )
356            .unwrap();
357
358        // Valid reference
359        let valid_ref = CrossReference {
360            ref_type: ReferenceType::Document,
361            target: "getting-started".to_string(),
362            display_text: None,
363            source_location: create_test_location(),
364            is_external: false,
365        };
366
367        let result = domain.validate_reference(&valid_ref);
368        assert!(result.is_valid);
369        assert!(result.target_object.is_some());
370
371        // Valid reference with .rst extension
372        let valid_ref_ext = CrossReference {
373            ref_type: ReferenceType::Document,
374            target: "getting-started.rst".to_string(),
375            display_text: None,
376            source_location: create_test_location(),
377            is_external: false,
378        };
379
380        let result = domain.validate_reference(&valid_ref_ext);
381        assert!(result.is_valid);
382
383        // Invalid reference
384        let invalid_ref = CrossReference {
385            ref_type: ReferenceType::Document,
386            target: "nonexistent".to_string(),
387            display_text: None,
388            source_location: create_test_location(),
389            is_external: false,
390        };
391
392        let result = domain.validate_reference(&invalid_ref);
393        assert!(!result.is_valid);
394        assert!(result.error_message.is_some());
395    }
396
397    #[test]
398    fn test_section_reference_validation() {
399        let mut domain = RstDomain::new();
400
401        domain
402            .register_section(
403                "api-reference".to_string(),
404                "API Reference".to_string(),
405                "api".to_string(),
406                create_test_location(),
407            )
408            .unwrap();
409
410        // Valid reference
411        let valid_ref = CrossReference {
412            ref_type: ReferenceType::Section,
413            target: "api-reference".to_string(),
414            display_text: None,
415            source_location: create_test_location(),
416            is_external: false,
417        };
418
419        let result = domain.validate_reference(&valid_ref);
420        assert!(result.is_valid);
421        assert!(result.target_object.is_some());
422
423        // Invalid reference with suggestions
424        let invalid_ref = CrossReference {
425            ref_type: ReferenceType::Section,
426            target: "api".to_string(), // Close but not exact
427            display_text: None,
428            source_location: create_test_location(),
429            is_external: false,
430        };
431
432        let result = domain.validate_reference(&invalid_ref);
433        assert!(!result.is_valid);
434        assert!(!result.suggestions.is_empty());
435    }
436}