spinne_html/
lib.rs

1use serde_json::Value;
2use std::fs;
3use std::path::Path;
4
5const HTML_TEMPLATE: &str = include_str!("./component-graph.html");
6
7/// Generates an HTML report from a component graph.
8/// Uses d3.js to render the graph.
9pub struct HtmlGenerator {
10    template: String,
11}
12
13impl HtmlGenerator {
14    pub fn new(workspace_data: Value) -> Self {
15        // Convert numeric IDs to strings in the JSON data
16        let workspace_data = convert_ids_to_strings(workspace_data);
17
18        let template = HTML_TEMPLATE.replace(
19            "[/* {{GRAPH_DATA}} */]",
20            &serde_json::to_string(&workspace_data).unwrap_or_default(),
21        );
22        Self { template }
23    }
24
25    pub fn save(&self, output_path: &Path) -> std::io::Result<()> {
26        fs::write(output_path, self.template.clone())
27    }
28}
29
30/// Recursively converts numeric IDs to strings in the JSON data
31fn convert_ids_to_strings(data: Value) -> Value {
32    match data {
33        Value::Array(arr) => Value::Array(arr.into_iter().map(convert_ids_to_strings).collect()),
34        Value::Object(obj) => {
35            let mut new_obj = serde_json::Map::new();
36            for (key, value) in obj {
37                let new_value = if key == "id" || key == "from" || key == "to" {
38                    // Convert numeric IDs to strings
39                    match value {
40                        Value::Number(n) => Value::String(n.to_string()),
41                        _ => convert_ids_to_strings(value),
42                    }
43                } else {
44                    convert_ids_to_strings(value)
45                };
46                new_obj.insert(key, new_value);
47            }
48            Value::Object(new_obj)
49        }
50        _ => data,
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57    use serde_json::json;
58
59    #[test]
60    fn test_html_generation() {
61        let project_data = json!([
62          {
63            "name": "consumer-app",
64            "graph": {
65              "components": [
66                {
67                  "id": 14300231078674835378u64,
68                  "name": "App",
69                  "path": "consumer-app/src/App.tsx",
70                  "props": {},
71                },
72              ],
73              "edges": [
74                {
75                  "from": 14300231078674835378u64,
76                  "to": 11611080489164640768u64,
77                  "project_context": "source-lib",
78                },
79              ],
80            },
81          },
82          {
83            "name": "source-lib",
84            "graph": {
85              "components": [
86                {
87                  "id": 11611080489164640768u64,
88                  "name": "Button",
89                  "path": "source-lib/src/components/Button.tsx",
90                  "props": {},
91                },
92              ],
93              "edges": [],
94            },
95          },
96        ]);
97
98        let generator = HtmlGenerator::new(project_data);
99
100        assert!(generator.template.contains("App"));
101        assert!(!generator.template.contains("{{GRAPH_DATA}}"));
102    }
103
104    #[test]
105    fn test_html() {
106        let graph_data = json!([
107        {
108          "name": "source-lib",
109          "graph": {
110            "components": [
111              {
112                "id": 11611080489164640768u64,
113                "name": "Button",
114                "path": "source-lib/src/components/Button.tsx",
115                "props": {
116                  "label": 1,
117                  "onClick": 1,
118                  "variant": 1,
119                  "disabled": 1
120                }
121              },
122              {
123                "id": 11611080489164640769u64,
124                "name": "Input",
125                "path": "source-lib/src/components/Input.tsx",
126                "props": {
127                  "value": 1,
128                  "onChange": 1,
129                  "placeholder": 1,
130                  "type": 1
131                }
132              },
133              {
134                "id": 11611080489164640770u64,
135                "name": "Card",
136                "path": "source-lib/src/components/Card.tsx",
137                "props": {
138                  "title": 1,
139                  "children": 1,
140                  "padding": 1
141                }
142              },
143              {
144                "id": 11611080489164640771u64,
145                "name": "Modal",
146                "path": "source-lib/src/components/Modal.tsx",
147                "props": {
148                  "isOpen": 1,
149                  "onClose": 1,
150                  "title": 1,
151                  "children": 1
152                }
153              }
154            ],
155            "edges": [
156              {
157                "from": 11611080489164640771u64,
158                "to": 11611080489164640770u64,
159                "project_context": "source-lib"
160              },
161              {
162                "from": 11611080489164640771u64,
163                "to": 11611080489164640768u64,
164                "project_context": "source-lib"
165              },
166              {
167                "from": 11611080489164640770u64,
168                "to": 11611080489164640768u64,
169                "project_context": "source-lib"
170              },
171              {
172                "from": 11611080489164640770u64,
173                "to": 11611080489164640769u64,
174                "project_context": "source-lib"
175              }
176            ]
177          }
178        },
179        {
180          "name": "consumer-app",
181          "graph": {
182            "components": [
183              {
184                "id": 14300231078674835378u64,
185                "name": "App",
186                "path": "consumer-app/src/App.tsx",
187                "props": {}
188              },
189              {
190                "id": 14300231078674835379u64,
191                "name": "LoginForm",
192                "path": "consumer-app/src/components/LoginForm.tsx",
193                "props": {
194                  "onSubmit": 1,
195                  "error": 1
196                }
197              },
198              {
199                "id": 14300231078674835380u64,
200                "name": "UserProfile",
201                "path": "consumer-app/src/components/UserProfile.tsx",
202                "props": {
203                  "user": 1,
204                  "onEdit": 1
205                }
206              },
207              {
208                "id": 14300231078674835381u64,
209                "name": "SettingsModal",
210                "path": "consumer-app/src/components/SettingsModal.tsx",
211                "props": {
212                  "isOpen": 1,
213                  "onClose": 1,
214                  "settings": 1
215                }
216              }
217            ],
218            "edges": [
219              {
220                "from": 14300231078674835378u64,
221                "to": 11611080489164640768u64,
222                "project_context": "source-lib"
223              },
224              {
225                "from": 14300231078674835378u64,
226                "to": 14300231078674835379u64,
227                "project_context": "consumer-app"
228              },
229              {
230                "from": 14300231078674835378u64,
231                "to": 14300231078674835380u64,
232                "project_context": "consumer-app"
233              },
234              {
235                "from": 14300231078674835379u64,
236                "to": 11611080489164640768u64,
237                "project_context": "source-lib"
238              },
239              {
240                "from": 14300231078674835379u64,
241                "to": 11611080489164640769u64,
242                "project_context": "source-lib"
243              },
244              {
245                "from": 14300231078674835380u64,
246                "to": 11611080489164640770u64,
247                "project_context": "source-lib"
248              },
249              {
250                "from": 14300231078674835380u64,
251                "to": 14300231078674835381u64,
252                "project_context": "consumer-app"
253              },
254              {
255                "from": 14300231078674835381u64,
256                "to": 11611080489164640771u64,
257                "project_context": "source-lib"
258              }
259            ]
260          }
261        }]);
262
263        let generator = HtmlGenerator::new(graph_data);
264        let output_path = Path::new("test.html");
265        generator.save(output_path).unwrap();
266        open::that(output_path).unwrap();
267    }
268}