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(§ions, &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}