Skip to main content

scirs2_ndimage/
documentation.rs

1//! Documentation Generation System
2//!
3//! This module provides a comprehensive documentation generation system for SciRS2 NDImage,
4//! including HTML generation, tutorials, examples, API documentation, and styling.
5//!
6//! This module has been refactored into focused components for better maintainability.
7//! See the submodules for specific functionality.
8//!
9//! # Features
10//!
11//! - **Comprehensive API Documentation**: Auto-generated documentation for all modules and functions
12//! - **Interactive Tutorials**: Step-by-step guides with executable examples
13//! - **Code Examples**: Real-world examples for different domains (medical, satellite, scientific)
14//! - **Modern Web Interface**: Responsive design with search functionality
15//! - **Syntax Highlighting**: Code syntax highlighting with Prism.js
16//! - **Mobile-Friendly**: Responsive design that works on all devices
17//!
18//! # Usage
19//!
20//! ```rust
21//! use scirs2_ndimage::documentation::DocumentationSite;
22//!
23//! let mut site = DocumentationSite::new();
24//! site.build_comprehensive_documentation()?;
25//! site.generate_html_documentation("./docs")?;
26//! ```
27
28// Re-export all module components for backward compatibility
29pub use self::{html_generation::*, modules::*, styling::*, tutorials::*, types::*};
30
31// Module declarations
32pub mod html_generation;
33pub mod modules;
34pub mod styling;
35pub mod tutorials;
36pub mod types;
37
38// Import for conditional compilation
39#[cfg(feature = "serde")]
40use serde::{Deserialize, Serialize};
41
42/// Quick builder function for creating a complete documentation site
43pub fn create_documentation_site() -> types::Result<DocumentationSite> {
44    let mut site = DocumentationSite::new();
45    site.build_comprehensive_documentation()?;
46    Ok(site)
47}
48
49/// Generate complete HTML documentation to a directory
50pub fn generate_complete_documentation(output_dir: &str) -> types::Result<()> {
51    let site = create_documentation_site()?;
52    site.generate_html_documentation(output_dir)?;
53    Ok(())
54}
55
56/// Builder pattern for customizing documentation generation
57pub struct DocumentationBuilder {
58    site: DocumentationSite,
59    include_tutorials: bool,
60    include_examples: bool,
61    include_search: bool,
62    custom_css: Option<String>,
63    custom_js: Option<String>,
64}
65
66impl DocumentationBuilder {
67    /// Create a new documentation builder
68    pub fn new() -> Self {
69        Self {
70            site: DocumentationSite::new(),
71            include_tutorials: true,
72            include_examples: true,
73            include_search: true,
74            custom_css: None,
75            custom_js: None,
76        }
77    }
78
79    /// Set the site title
80    pub fn title(mut self, title: impl Into<String>) -> Self {
81        self.site.title = title.into();
82        self
83    }
84
85    /// Set the site description
86    pub fn description(mut self, description: impl Into<String>) -> Self {
87        self.site.description = description.into();
88        self
89    }
90
91    /// Set the site version
92    pub fn version(mut self, version: impl Into<String>) -> Self {
93        self.site.version = version.into();
94        self
95    }
96
97    /// Set the base URL
98    pub fn base_url(mut self, url: impl Into<String>) -> Self {
99        self.site.base_url = url.into();
100        self
101    }
102
103    /// Include or exclude tutorials
104    pub fn with_tutorials(mut self, include: bool) -> Self {
105        self.include_tutorials = include;
106        self
107    }
108
109    /// Include or exclude examples
110    pub fn with_examples(mut self, include: bool) -> Self {
111        self.include_examples = include;
112        self
113    }
114
115    /// Include or exclude search functionality
116    pub fn with_search(mut self, include: bool) -> Self {
117        self.include_search = include;
118        self
119    }
120
121    /// Add custom CSS
122    pub fn custom_css(mut self, css: impl Into<String>) -> Self {
123        self.custom_css = Some(css.into());
124        self
125    }
126
127    /// Add custom JavaScript
128    pub fn custom_js(mut self, js: impl Into<String>) -> Self {
129        self.custom_js = Some(js.into());
130        self
131    }
132
133    /// Build the documentation site
134    pub fn build(mut self) -> types::Result<DocumentationSite> {
135        // Build modules (always included)
136        self.site.build_module_documentation()?;
137
138        // Build tutorials if requested
139        if self.include_tutorials {
140            self.site.build_tutorials()?;
141        }
142
143        // Build examples if requested
144        if self.include_examples {
145            self.site.build_examples()?;
146        }
147
148        Ok(self.site)
149    }
150
151    /// Build and generate HTML documentation
152    pub fn generate(self, output_dir: &str) -> types::Result<()> {
153        let site = self.build()?;
154        site.generate_html_documentation(output_dir)?;
155        Ok(())
156    }
157}
158
159impl Default for DocumentationBuilder {
160    fn default() -> Self {
161        Self::new()
162    }
163}
164
165/// Utility functions for common documentation tasks
166pub mod utils {
167    use super::*;
168
169    /// Extract function signatures from Rust code
170    pub fn extract_function_signatures(rust_code: &str) -> Vec<String> {
171        let mut signatures = Vec::new();
172
173        // Simple regex-based extraction (in practice, you'd want a proper parser)
174        if let Ok(re) = regex::Regex::new(r"pub fn (\w+)[^{]*\{") {
175            for cap in re.captures_iter(rust_code) {
176                if let Some(sig) = cap.get(0) {
177                    signatures.push(sig.as_str().to_string());
178                }
179            }
180        }
181
182        signatures
183    }
184
185    /// Generate API documentation from source files
186    pub fn generate_api_from_source(source_dir: &str) -> types::Result<Vec<types::ModuleDoc>> {
187        use std::fs;
188        use std::path::Path;
189
190        let mut modules = Vec::new();
191        let source_path = Path::new(source_dir);
192
193        if source_path.exists() {
194            for entry in fs::read_dir(source_path)? {
195                let entry = entry?;
196                let path = entry.path();
197
198                if path.extension().and_then(|s| s.to_str()) == Some("rs") {
199                    let content = fs::read_to_string(&path)?;
200                    let module_name = path
201                        .file_stem()
202                        .and_then(|s| s.to_str())
203                        .unwrap_or("unknown")
204                        .to_string();
205
206                    let mut module =
207                        types::ModuleDoc::new(module_name, "Auto-generated documentation");
208
209                    // Extract functions (simplified)
210                    let signatures = extract_function_signatures(&content);
211                    for sig in signatures {
212                        let func = types::FunctionDoc::new(
213                            "extracted_function",
214                            sig,
215                            "Auto-extracted function",
216                            "Return type",
217                        );
218                        module.add_function(func);
219                    }
220
221                    modules.push(module);
222                }
223            }
224        }
225
226        Ok(modules)
227    }
228
229    /// Validate HTML output for common issues
230    pub fn validate_html_output(html: &str) -> Vec<String> {
231        let mut issues = Vec::new();
232
233        // Check for unclosed tags (simplified).
234        // `close_open` counts explicit closing tags (</foo>).
235        // `self_close` counts self-closing tags (<br/>).
236        // Opening tags = all `<` minus the ones that start a `</foo>` closer.
237        let close_open = html.matches("</").count();
238        let self_close = html.matches("/>").count();
239        let opening = html.matches('<').count() - close_open;
240        let closing = close_open + self_close;
241
242        if opening != closing {
243            issues.push("Potential unclosed HTML tags detected".to_string());
244        }
245
246        // Check for missing alt attributes on images
247        if html.contains("<img") && !html.contains("alt=") {
248            issues.push("Images missing alt attributes".to_string());
249        }
250
251        // Check for missing title
252        if !html.contains("<title>") {
253            issues.push("HTML missing title tag".to_string());
254        }
255
256        issues
257    }
258
259    /// Generate sitemap.xml for the documentation
260    pub fn generate_sitemap(base_url: &str, pages: &[&str]) -> String {
261        let mut sitemap = String::from(
262            r#"<?xml version="1.0" encoding="UTF-8"?>
263<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">"#,
264        );
265
266        for page in pages {
267            sitemap.push_str(&format!(
268                r#"
269  <url>
270    <loc>{}{}</loc>
271    <changefreq>weekly</changefreq>
272    <priority>0.8</priority>
273  </url>"#,
274                base_url.trim_end_matches('/'),
275                page
276            ));
277        }
278
279        sitemap.push_str("\n</urlset>");
280        sitemap
281    }
282}
283
284/// Configuration for documentation themes and styling
285#[derive(Debug, Clone)]
286pub struct DocumentationTheme {
287    /// Primary color (hex)
288    pub primary_color: String,
289    /// Secondary color (hex)
290    pub secondary_color: String,
291    /// Background color (hex)
292    pub background_color: String,
293    /// Text color (hex)
294    pub text_color: String,
295    /// Font family
296    pub font_family: String,
297    /// Code font family
298    pub code_font_family: String,
299}
300
301impl Default for DocumentationTheme {
302    fn default() -> Self {
303        Self {
304            primary_color: "#3498db".to_string(),
305            secondary_color: "#2c3e50".to_string(),
306            background_color: "#f8f9fa".to_string(),
307            text_color: "#333333".to_string(),
308            font_family: "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif"
309                .to_string(),
310            code_font_family: "Consolas, Monaco, 'Andale Mono', monospace".to_string(),
311        }
312    }
313}
314
315impl DocumentationTheme {
316    /// Create a dark theme variant
317    pub fn dark() -> Self {
318        Self {
319            primary_color: "#61dafb".to_string(),
320            secondary_color: "#282c34".to_string(),
321            background_color: "#20232a".to_string(),
322            text_color: "#ffffff".to_string(),
323            ..Default::default()
324        }
325    }
326
327    /// Create a minimal theme variant
328    pub fn minimal() -> Self {
329        Self {
330            primary_color: "#000000".to_string(),
331            secondary_color: "#666666".to_string(),
332            background_color: "#ffffff".to_string(),
333            text_color: "#333333".to_string(),
334            ..Default::default()
335        }
336    }
337
338    /// Generate CSS variables for this theme
339    pub fn to_css_variables(&self) -> String {
340        format!(
341            r#":root {{
342    --primary-color: {};
343    --secondary-color: {};
344    --background-color: {};
345    --text-color: {};
346    --font-family: {};
347    --code-font-family: {};
348}}"#,
349            self.primary_color,
350            self.secondary_color,
351            self.background_color,
352            self.text_color,
353            self.font_family,
354            self.code_font_family
355        )
356    }
357}
358
359#[cfg(test)]
360mod tests {
361    use super::*;
362
363    #[test]
364    fn test_create_documentation_site() {
365        let result = create_documentation_site();
366        assert!(result.is_ok());
367
368        let site = result.expect("Operation failed");
369        assert_eq!(site.title, "SciRS2 NDImage Documentation");
370        assert!(!site.modules.is_empty());
371        assert!(!site.tutorials.is_empty());
372        assert!(!site.examples.is_empty());
373    }
374
375    #[test]
376    fn test_documentation_builder() {
377        let site = DocumentationBuilder::new()
378            .title("Custom Documentation")
379            .description("Custom description")
380            .version("1.0.0")
381            .with_tutorials(true)
382            .with_examples(false)
383            .build()
384            .expect("Operation failed");
385
386        assert_eq!(site.title, "Custom Documentation");
387        assert_eq!(site.description, "Custom description");
388        assert_eq!(site.version, "1.0.0");
389        assert!(!site.tutorials.is_empty());
390        assert!(site.examples.is_empty()); // Examples disabled
391    }
392
393    #[test]
394    fn test_documentation_theme() {
395        let default_theme = DocumentationTheme::default();
396        assert_eq!(default_theme.primary_color, "#3498db");
397
398        let dark_theme = DocumentationTheme::dark();
399        assert_eq!(dark_theme.background_color, "#20232a");
400
401        let css_vars = default_theme.to_css_variables();
402        assert!(css_vars.contains("--primary-color"));
403        assert!(css_vars.contains("--text-color"));
404    }
405
406    #[test]
407    fn test_utils_extract_functions() {
408        let rust_code = r#"
409            pub fn example_function() {
410                // Some code
411            }
412
413            pub fn another_function(param: i32) -> String {
414                // More code
415            }
416        "#;
417
418        let signatures = utils::extract_function_signatures(rust_code);
419        assert_eq!(signatures.len(), 2);
420        assert!(signatures[0].contains("example_function"));
421        assert!(signatures[1].contains("another_function"));
422    }
423
424    #[test]
425    fn test_utils_validate_html() {
426        let good_html = "<html><head><title>Test</title></head><body><img src='test.png' alt='test'/></body></html>";
427        let issues = utils::validate_html_output(good_html);
428        assert!(issues.is_empty());
429
430        let bad_html = "<html><head></head><body><img src='test.png'/></body>";
431        let issues = utils::validate_html_output(bad_html);
432        assert!(!issues.is_empty());
433    }
434
435    #[test]
436    fn test_utils_generate_sitemap() {
437        let pages = vec![
438            "/index.html",
439            "/api/index.html",
440            "/tutorials/getting-started.html",
441        ];
442        let sitemap = utils::generate_sitemap("https://example.com", &pages);
443
444        assert!(sitemap.contains("<?xml version"));
445        assert!(sitemap.contains("https://example.com/index.html"));
446        assert!(sitemap.contains("https://example.com/api/index.html"));
447        assert!(sitemap.contains("https://example.com/tutorials/getting-started.html"));
448    }
449
450    #[test]
451    fn test_module_integration() {
452        let mut site = DocumentationSite::new();
453
454        // Test that all modules integrate properly
455        assert!(site.build_module_documentation().is_ok());
456        assert!(site.build_tutorials().is_ok());
457        assert!(site.build_examples().is_ok());
458
459        // Verify content was built
460        assert!(!site.modules.is_empty());
461        assert!(!site.tutorials.is_empty());
462        assert!(!site.examples.is_empty());
463
464        // Test HTML generation methods exist
465        let _ = site.generate_module_cards();
466        let _ = site.generate_api_module_list();
467        let _ = site.generate_tutorial_cards();
468        let _ = site.generate_default_css();
469        let _ = site.generate_default_js();
470    }
471}