1use serde_json::Value;
2use std::fs;
3use std::path::Path;
4
5const HTML_TEMPLATE: &str = include_str!("./component-graph.html");
6
7pub struct HtmlGenerator {
10 template: String,
11}
12
13impl HtmlGenerator {
14 pub fn new(workspace_data: Value) -> Self {
15 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
30fn 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 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}