report_leptos/
lib.rs

1//! # report-leptos
2//!
3//! Leptos SSR renderer for generating static HTML reports.
4//!
5//! This crate provides a type-safe, component-based approach to generating
6//! beautiful HTML reports using [Leptos](https://leptos.dev/) server-side rendering.
7//! Originally built for [loctree](https://github.com/Loctree/Loctree) codebase
8//! analysis, it can be used independently for any static report generation needs.
9//!
10//! ## Features
11//!
12//! - **Zero JavaScript Runtime** - Pure SSR, no hydration needed
13//! - **Component-Based** - Modular, reusable UI components
14//! - **Type-Safe** - Full Rust type safety from data to HTML
15//! - **Interactive Graphs** - Cytoscape.js integration for dependency visualization
16//!
17//! ## Quick Start
18//!
19//! ```rust
20//! use report_leptos::{render_report, JsAssets, types::ReportSection};
21//!
22//! // Create report data
23//! let section = ReportSection {
24//!     root: "my-project".into(),
25//!     files_analyzed: 42,
26//!     ..Default::default()
27//! };
28//!
29//! // Configure JS assets (optional, for graph visualization)
30//! let js_assets = JsAssets::default();
31//!
32//! // Render to HTML string
33//! let html = render_report(&[section], &js_assets, false);
34//!
35//! // Write to file
36//! std::fs::write("report.html", html).unwrap();
37//! ```
38//!
39//! ## Architecture
40//!
41//! The crate is organized into modules:
42//!
43//! - [`types`] - Data structures for report content
44//! - [`components`] - Leptos UI components
45//! - [`styles`] - CSS constants
46//!
47//! ## Leptos 0.8 SSR
48//!
49//! This library uses Leptos 0.8's `RenderHtml` trait:
50//!
51//! ```rust,ignore
52//! use leptos::tachys::view::RenderHtml;
53//!
54//! let view = view! { <MyComponent /> };
55//! let html: String = view.to_html();
56//! ```
57//!
58//! No reactive runtime or hydration is needed - pure static HTML generation.
59//!
60//! ---
61//!
62//! Developed with 💀 by The Loctree Team (c)2025
63
64#![doc(html_root_url = "https://docs.rs/report-leptos/0.1.0")]
65#![warn(missing_docs)]
66#![warn(rustdoc::missing_crate_level_docs)]
67
68pub mod components;
69pub mod styles;
70pub mod types;
71
72use components::ReportDocument;
73use leptos::prelude::*;
74use leptos::tachys::view::RenderHtml;
75use types::ReportSection;
76
77/// Render a complete HTML report from analyzed sections.
78///
79/// This is the main entry point for generating reports. It takes a slice of
80/// [`ReportSection`] data and produces a complete HTML document as a string.
81///
82/// # Arguments
83///
84/// * `sections` - Slice of report sections to render
85/// * `js_assets` - Paths to JavaScript assets for graph visualization
86/// * `has_tauri` - Whether to show Tauri coverage tab (only for Tauri projects)
87///
88/// # Returns
89///
90/// A complete HTML document as a `String`, including `<!DOCTYPE html>`.
91///
92/// # Example
93///
94/// ```rust
95/// use report_leptos::{render_report, JsAssets, types::ReportSection};
96///
97/// let section = ReportSection {
98///     root: "src".into(),
99///     files_analyzed: 100,
100///     ..Default::default()
101/// };
102///
103/// let html = render_report(&[section], &JsAssets::default(), false);
104/// assert!(html.starts_with("<!DOCTYPE html>"));
105/// ```
106pub fn render_report(sections: &[ReportSection], js_assets: &JsAssets, has_tauri: bool) -> String {
107    let doc = view! {
108        <ReportDocument sections=sections.to_vec() js_assets=js_assets.clone() has_tauri=has_tauri />
109    };
110
111    let html = doc.to_html();
112
113    // Leptos doesn't include DOCTYPE, so we add it
114    format!("<!DOCTYPE html>\n{}", html)
115}
116
117/// JavaScript asset paths for graph visualization.
118///
119/// The report uses [Cytoscape.js](https://js.cytoscape.org/) with layout plugins
120/// for interactive dependency graph visualization. You can provide paths to:
121///
122/// - CDN URLs (e.g., unpkg.com)
123/// - Local bundled files (for offline use)
124/// - Empty strings (graph will show placeholder)
125///
126/// # Example
127///
128/// ```rust
129/// use report_leptos::JsAssets;
130///
131/// // CDN paths (with Cytoscape fallback, no WASM)
132/// let assets = JsAssets {
133///     cytoscape_path: "https://unpkg.com/cytoscape@3/dist/cytoscape.min.js".into(),
134///     dagre_path: "https://unpkg.com/dagre@0.8/dist/dagre.min.js".into(),
135///     cytoscape_dagre_path: "https://unpkg.com/cytoscape-dagre@2/cytoscape-dagre.js".into(),
136///     layout_base_path: "https://unpkg.com/layout-base@2/layout-base.js".into(),
137///     cose_base_path: "https://unpkg.com/cose-base@2/cose-base.js".into(),
138///     cytoscape_cose_bilkent_path: "https://unpkg.com/cytoscape-cose-bilkent@4/cytoscape-cose-bilkent.js".into(),
139///     ..Default::default() // wasm_base64, wasm_js_glue = None
140/// };
141///
142/// // Or use defaults (empty paths - graph shows placeholder)
143/// let assets = JsAssets::default();
144/// ```
145#[derive(Clone, Default, Debug)]
146pub struct JsAssets {
147    /// Path to cytoscape.min.js
148    pub cytoscape_path: String,
149    /// Path to dagre.min.js (for hierarchical layouts)
150    pub dagre_path: String,
151    /// Path to cytoscape-dagre.js plugin
152    pub cytoscape_dagre_path: String,
153    /// Path to layout-base.js (required by cose-base)
154    pub layout_base_path: String,
155    /// Path to cose-base.js (required by cytoscape-cose-bilkent)
156    pub cose_base_path: String,
157    /// Path to cytoscape-cose-bilkent.js plugin (for force-directed layouts)
158    pub cytoscape_cose_bilkent_path: String,
159    /// Inline WASM module (base64 encoded) for native graph rendering
160    pub wasm_base64: Option<String>,
161    /// Inline JS glue code for WASM module
162    pub wasm_js_glue: Option<String>,
163}
164
165#[cfg(test)]
166mod tests {
167    use super::*;
168
169    #[test]
170    fn renders_empty_report() {
171        let sections: Vec<ReportSection> = vec![];
172        let assets = JsAssets::default();
173        let html = render_report(&sections, &assets, false);
174
175        assert!(html.starts_with("<!DOCTYPE html>"));
176        assert!(html.contains("<html"));
177        assert!(html.contains("loctree"));
178    }
179
180    #[test]
181    fn renders_section_with_data() {
182        let section = ReportSection {
183            root: "test-root".into(),
184            files_analyzed: 42,
185            ..Default::default()
186        };
187        let assets = JsAssets::default();
188        let html = render_report(&[section], &assets, false);
189
190        assert!(html.contains("test-root"));
191        assert!(html.contains("42"));
192    }
193
194    #[test]
195    fn graph_data_to_dot_format() {
196        use types::{GraphData, GraphNode};
197
198        let graph = GraphData {
199            nodes: vec![
200                GraphNode {
201                    id: "src/main.ts".into(),
202                    label: "main.ts".into(),
203                    loc: 150,
204                    x: 0.5,
205                    y: 0.5,
206                    component: 0,
207                    degree: 2,
208                    detached: false,
209                },
210                GraphNode {
211                    id: "src/utils.ts".into(),
212                    label: "utils.ts".into(),
213                    loc: 50,
214                    x: 0.3,
215                    y: 0.7,
216                    component: 0,
217                    degree: 1,
218                    detached: false,
219                },
220            ],
221            edges: vec![("src/main.ts".into(), "src/utils.ts".into(), "import".into())],
222            components: vec![],
223            main_component_id: 0,
224            ..Default::default()
225        };
226
227        let dot = graph.to_dot();
228
229        // Verify DOT structure
230        assert!(dot.starts_with("digraph loctree"));
231        assert!(dot.contains("src/main.ts"));
232        assert!(dot.contains("src/utils.ts"));
233        assert!(dot.contains("->"));
234        assert!(dot.contains("fillcolor"));
235    }
236
237    #[test]
238    fn graph_data_to_dot_escapes_special_chars() {
239        use types::{GraphData, GraphNode};
240
241        let graph = GraphData {
242            nodes: vec![GraphNode {
243                id: "src/file\"with\"quotes.ts".into(),
244                label: "file\"quotes".into(),
245                loc: 10,
246                x: 0.0,
247                y: 0.0,
248                component: 0,
249                degree: 0,
250                detached: false,
251            }],
252            edges: vec![],
253            components: vec![],
254            main_component_id: 0,
255            ..Default::default()
256        };
257
258        let dot = graph.to_dot();
259
260        // Quotes should be escaped
261        assert!(dot.contains("\\\""));
262        // Raw unescaped quote should not appear in node definitions
263        assert!(!dot.contains("file\"with\"quotes"));
264    }
265}