1use std::{
2 env,
3 fmt::Display,
4 fs::File,
5 io::Write,
6 path::{Path, PathBuf},
7 process::{Command, Stdio},
8};
9
10use svg::{
11 node::{
12 element::tag::{self, Type},
13 Attributes,
14 },
15 parser::Event,
16 Parser,
17};
18
19use crate::graph::Graph;
20
21const HEAD: &str = r#"
22<head>
23 <meta charset="UTF-8">
24 <meta name="viewport" content="width=device-width, initial-scale=1.0">
25 <title>GRAPH</title>
26 <style>
27 body {
28 background-color: #3d3d3d;
29 }
30
31 .svg-container {
32 display: flex;
33 justify-content: space-around;
34 margin-top: 20px;
35 }
36
37 .info-container {
38 display: flex;
39 justify-content: space-around;
40 font-family: monospace
41 }
42
43 svg {
44 width: auto;
45 /* Adjust the width as needed */
46 height: auto;
47 }
48
49 .hidden {
50 display: none;
51 }
52
53 .highlighted {
54 stroke: hsla(348, 83%, 47%, 0.5);
55 /* Set the stroke color to yellow */
56 /* stroke-width: 2; */
57 /* Set the stroke width */
58 }
59 </style>
60</head>
61"#;
62
63#[derive(Debug, thiserror::Error)]
64pub enum RenderError {
65 #[error("failed to write flowchart")]
66 IO(#[from] std::io::Error),
67 #[error("failed to convert to string")]
68 Utf(#[from] std::string::FromUtf8Error),
69 #[error("flowchart layout is empty")]
70 Layout,
71}
72type Result<T> = std::result::Result<T, RenderError>;
73
74#[derive(Debug, Clone)]
75pub struct Render {
76 name: String,
77 render: String,
78 pub(crate) child: Option<Vec<Box<Render>>>,
79}
80impl From<&Graph> for Render {
81 fn from(graph: &Graph) -> Self {
82 Self {
83 name: graph.name.clone(),
84 render: graph.to_string(),
85 child: None,
86 }
87 }
88}
89
90#[derive(Debug, Clone, Default, PartialEq)]
91#[allow(dead_code)]
92enum GraphLayout {
93 Dot,
94 #[default]
95 Neato,
96 Fdp,
97}
98impl Display for GraphLayout {
99 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100 match self {
101 GraphLayout::Dot => write!(f, "dot"),
102 GraphLayout::Neato => write!(f, "neato"),
103 GraphLayout::Fdp => write!(f, "fdp"),
104 }
105 }
106}
107impl GraphLayout {
108 pub fn new() -> Option<Self> {
109 match env::var("FLOWCHART") {
110 Ok(var) => match var.to_lowercase().as_str() {
111 "dot" => Some(Self::Dot),
112 "neato" => Some(Self::Neato),
113 "fdp" => Some(Self::Fdp),
114 _ => None,
115 },
116 Err(_) => Some(Self::default()),
117 }
118 }
119}
120impl Render {
121 fn id(&self) -> String {
122 use std::hash::{DefaultHasher, Hash, Hasher};
123 let mut hasher = DefaultHasher::new();
124 self.name.hash(&mut hasher);
125 let sh = hasher.finish();
126 format!("id{sh:x}")
127 }
128 pub fn into_svg(&mut self) -> Result<&mut Self> {
130 let mut graph_layout = GraphLayout::new().ok_or(RenderError::Layout)?;
131 let result = loop {
132 let graph = Command::new("echo")
133 .arg(&self.render)
134 .stdout(Stdio::piped())
135 .spawn()?;
136 let svg = Command::new(graph_layout.to_string())
137 .arg("-Tsvg")
138 .stdin(Stdio::from(graph.stdout.unwrap()))
139 .stdout(Stdio::piped())
140 .spawn()?;
141 let output = svg.wait_with_output()?;
142 if output.status.success() {
143 break String::from_utf8(output.stdout)?;
144 } else if graph_layout == GraphLayout::Dot {
145 println!("failed to convert model `{:}` to SVG diagram", self.name);
146 return Ok(self);
147 } else {
148 graph_layout = GraphLayout::Dot;
149 }
150 };
151 log::debug!("{:}", &result[..result.len().min(64)]);
152 self.render = result
153 .lines()
154 .skip(6)
155 .collect::<Vec<_>>()
156 .join("\n")
157 .replace(r#"g id="node"#, &format!(r#"g id="{}_node"#, self.id()));
158 self.child
159 .as_mut()
160 .map(|child| {
161 child
162 .iter_mut()
163 .map(|child| child.into_svg())
164 .collect::<Result<Vec<_>>>()
165 })
166 .transpose()?;
167 log::debug!("{:}", self);
168 Ok(self)
169 }
170 fn hover(&self, child: &str, element: &str) -> String {
171 format!(
172 r#"
173const {0} = document.getElementById('{0}');
174{0}.addEventListener('mouseenter', function () {{
175 {0}.classList.add('highlighted');
176}});
177{0}.addEventListener('mouseleave', function () {{
178 {0}.classList.remove('highlighted');
179}});
180{0}.addEventListener('click', function () {{
181 // Hide graph1
182 {1}.classList.add('hidden');
183 // Show graph2
184 {2}.classList.remove('hidden');
185}});
186{2}.addEventListener('keydown', function (event) {{
187 if (event.key === 'Escape') {{
188 // Show graph1
189 {1}.classList.remove('hidden');
190 // Hide graph2
191 {2}.classList.add('hidden');
192 }}
193}});
194 "#,
195 element,
196 self.id(),
197 child
198 )
199 }
200 fn parse(&self) -> Option<String> {
202 let Some(child) = &self.child else {
203 return None;
204 };
205 let parser = Parser::new(&self.render);
206 let mut h = vec![];
207 let mut attributes = Attributes::new();
208 for event in parser {
209 match event {
210 Event::Tag(tag::Group, Type::Start, a) => {
211 attributes = a;
212 }
213 Event::Text(text) => {
214 for child in child {
215 if html_escape::decode_html_entities(text) == child.name {
216 log::debug!("{:?}", (&child.name, child.id()));
217 h.push(attributes.get("id").map(|id| self.hover(&child.id(), id)));
218 }
219 }
220 }
221 _ => {}
222 }
223 }
224 if h.is_empty() {
225 None
226 } else {
227 h.into_iter()
228 .collect::<Option<Vec<String>>>()
229 .map(|h| h.join("\n"))
230 }
231 }
232 fn script_child_hover(&self, file: &mut File) -> Result<()> {
234 log::debug!("{:?}", (&self.name, self.id()));
235 if let Some(h) = self.parse() {
236 writeln!(file, "{}", h)?;
237 }
238 let Some(child) = &self.child else {
239 return Ok(());
240 };
241 for child in child {
242 child.script_child_hover(file)?;
243 }
244 Ok(())
245 }
246 fn child_svg(&self, file: &mut File, class: Option<&str>) -> Result<()> {
248 match class {
249 Some(class) => writeln!(
250 file,
251 "{}",
252 self.render.replace(
253 "<svg",
254 &format!(r#"<svg id="{}" tabindex="0" class="{}""#, self.id(), class)
255 )
256 )?,
257 None => writeln!(
258 file,
259 "{}",
260 self.render
261 .replace("<svg", &format!(r#"<svg id="{}" tabindex="0""#, self.id()))
262 )?,
263 }
264 let Some(child) = &self.child else {
265 return Ok(());
266 };
267 for child in child {
268 child.child_svg(file, Some("hidden"))?;
269 }
270 Ok(())
271 }
272 fn script_child_const(&self, file: &mut File) -> Result<()> {
274 let Some(child) = &self.child else {
275 return Ok(());
276 };
277 for child in child {
278 writeln!(
279 file,
280 "const {0} = document.getElementById('{0}');",
281 child.id()
282 )?;
283 child.script_child_const(file)?;
284 }
285 Ok(())
286 }
287 fn get_children_id(&self, ids: &mut Vec<String>) {
299 let Some(child) = &self.child else {
300 return;
301 };
302 for child in child {
303 ids.push(child.id());
304 child.get_children_id(ids);
305 }
306 }
307 fn script_home(&self) -> String {
309 let mut ids = vec![];
310 self.get_children_id(&mut ids);
311 format!(
312 r#"
313document.addEventListener('keydown', function (event) {{
314 if (event.key === 'Home') {{
315// Show graph1
316{0}.classList.remove('hidden');
317// Hide other graphs
318{1}
319 }}
320}});
321 "#,
322 self.id(),
323 ids.into_iter()
324 .map(|id| format!("{0}.classList.add('hidden');", id))
325 .collect::<Vec<String>>()
326 .join("\n")
327 )
328 }
329 pub fn to_html(&self) -> Result<PathBuf> {
331 log::debug!("{:}", self);
332 let data_repo = env::var("DATA_REPO").unwrap_or(".".into());
333 let path = Path::new(&data_repo).join(format!("{}_flowchart.html", self.name));
334 let mut file = File::create(&path).unwrap();
335 writeln!(file, "<!DOCTYPE html>")?;
336 writeln!(file, r#"<html lang="en">"#)?;
337 writeln!(
338 file,
339 "{}",
340 HEAD.replace("GRAPH", &format!("{} Flowchart", self.name.to_uppercase()))
341 )?;
342 writeln!(file, "<body>")?;
343 writeln!(
344 file,
345 r#"<div class="info-container">Left Click on System: show ; Left Click followed by Escape key: back-up ; Home key: back to root</div>"#
346 )?;
347
348 writeln!(file, r#" <div class="svg-container">"#)?;
349 self.child_svg(&mut file, None)?;
350 writeln!(file, " </div>")?;
351 writeln!(file, "<script>")?;
352 writeln!(
353 file,
354 "const {0} = document.getElementById('{0}');",
355 self.id()
356 )?;
357 write!(file, "{}", self.script_home())?;
358 self.script_child_const(&mut file)?;
359 self.script_child_hover(&mut file)?;
360 writeln!(file, "</script>")?;
361 writeln!(file, "</body>")?;
362 writeln!(file, "</html>")?;
363 Ok(path)
364 }
365}
366
367impl Display for Render {
368 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
369 writeln!(f, "==>> {}", self.name)?;
370 if let Some(child) = &self.child {
372 for (i, child) in child.iter().enumerate() {
373 writeln!(f, "{} child #{i}", self.name)?;
374 writeln!(f, "{}", child)?;
375 }
376 }
377 writeln!(f, " <<== {}", self.name)?;
378 Ok(())
379 }
380}
381
382#[cfg(test)]
383mod tests {
384 #[test]
385 fn hash() {
386 use std::hash::{DefaultHasher, Hash, Hasher};
387 let mut hasher = DefaultHasher::new();
388 let s = String::from("M1@80");
389 s.hash(&mut hasher);
390 let sh = hasher.finish();
391 println!("{s} -> {sh:x}");
392 let s = String::from("GMT Servo-Mechanisms (M1@80)");
393 s.hash(&mut hasher);
394 let sh = hasher.finish();
395 println!("{s} -> {sh:x}");
396 }
397}