1use crate::tree::{Graphviz, Group, ItemExt, SkillTree, Status};
2use fehler::throws;
3use std::io::Write;
4
5impl SkillTree {
6 #[throws(anyhow::Error)]
8 pub fn write_graphviz(&self, output: &mut dyn Write) {
9 write_graphviz(self, output)?
10 }
11
12 #[throws(anyhow::Error)]
14 pub fn to_graphviz(&self) -> String {
15 let mut output = Vec::new();
16 write_graphviz(self, &mut output)?;
17 String::from_utf8(output)?
18 }
19}
20
21#[throws(anyhow::Error)]
22fn write_graphviz(tree: &SkillTree, output: &mut dyn Write) {
23 let rankdir = match &tree.graphviz {
24 Some(Graphviz {
25 rankdir: Some(rankdir),
26 ..
27 }) => &rankdir[..],
28 _ => "LR",
29 };
30 writeln!(output, r#"digraph g {{"#)?;
31 writeln!(
32 output,
33 r#"graph [ rankdir = "{rankdir}" ];"#,
34 rankdir = rankdir
35 )?;
36 writeln!(output, r#"node [ fontsize="16", shape = "ellipse" ];"#)?;
37 writeln!(output, r#"edge [ ];"#)?;
38
39 if let Some(clusters) = &tree.cluster {
40 for cluster in clusters {
41 let cluster_name = format!("cluster_{}", cluster.name);
42 writeln!(
43 output,
44 r#"subgraph {cluster_name} {{"#,
45 cluster_name = cluster_name
46 )?;
47 writeln!(output, r#" label="{}";"#, cluster.label)?;
48 write_cluster(tree, output, Some(&cluster.name))?;
49 writeln!(output, r#"}}"#)?;
50 }
51 }
52 write_cluster(tree, output, None)?;
53
54 for group in tree.groups() {
55 if let Some(requires) = &group.requires {
56 for requirement in requires {
57 writeln!(output, r#""{}" -> "{}";"#, requirement, &group.name)?;
58 }
59 }
60 }
61
62 writeln!(output, r#"}}"#)?;
63}
64
65#[throws(anyhow::Error)]
66fn write_cluster(tree: &SkillTree, output: &mut dyn Write, cluster: Option<&String>) {
67 for group in tree.groups() {
68 match (&group.cluster, cluster) {
71 (None, None) => {}
72 (Some(c1), Some(c2)) if c1 == c2 => {}
73 _ => continue,
74 }
75 writeln!(output, r#""{}" ["#, group.name)?;
76 write_group_label(tree, group, output)?;
77 writeln!(output, r#" shape = "none""#)?;
78 writeln!(output, r#" margin = 0"#)?;
79 writeln!(output, r#"]"#)?;
80 }
81}
82
83const WATCH_EMOJI: &str = "⌚";
84const HAMMER_WRENCH_EMOJI: &str = "🛠️";
85const CHECKED_BOX_EMOJI: &str = "☑️";
86const RAISED_HAND_EMOJI: &str = "🙋";
87
88fn escape(s: &str) -> String {
89 htmlescape::encode_minimal(s).replace('\n', "<br/>")
90}
91
92#[throws(anyhow::Error)]
93fn write_group_label(tree: &SkillTree, group: &Group, output: &mut dyn Write) {
94 writeln!(output, r#" label = <<table>"#)?;
95
96 let label = group.label.as_ref().unwrap_or(&group.name);
97 let label = escape(label);
98 let group_href = attribute_str("href", &group.href, "");
99 let header_color = group
100 .header_color
101 .as_ref()
102 .map(String::as_str)
103 .unwrap_or("darkgoldenrod");
104 let description_color = group
105 .description_color
106 .as_ref()
107 .map(String::as_str)
108 .unwrap_or("darkgoldenrod1");
109
110 let columns = tree.columns().len() + 1;
112
113 writeln!(
114 output,
115 r#" <tr><td bgcolor="{header_color}" colspan="{columns}"{group_href}>{label}</td></tr>"#,
116 group_href = group_href,
117 label = label,
118 header_color = header_color,
119 columns = columns,
120 )?;
121
122 for label in group.description.iter().flatten() {
123 writeln!(
124 output,
125 r#" <tr><td bgcolor="{description_color}" colspan="{columns}"{group_href}>{label}</td></tr>"#,
126 group_href = group_href,
127 label = label,
128 description_color = description_color,
129 columns = columns,
130 )?;
131 }
132
133 for item in &group.items {
134 let item_status = Status::Unassigned; let (_emoji, _fontcolor, mut start_tag, mut end_tag) = match item_status {
136 Status::Blocked => (
137 WATCH_EMOJI,
138 None,
139 "<i><font color=\"lightgrey\">",
140 "</font></i>",
141 ),
142 Status::Unassigned => (RAISED_HAND_EMOJI, Some("red"), "", ""),
143 Status::Assigned => (HAMMER_WRENCH_EMOJI, None, "", ""),
144 Status::Complete => (CHECKED_BOX_EMOJI, None, "<s>", "</s>"),
145 };
146
147 let bgcolor = attribute_str("bgcolor", &Some("cornsilk"), "");
148 let href = attribute_str("href", &item.href(), "");
149 if item.href().is_some() && start_tag == "" {
150 start_tag = "<u>";
151 end_tag = "</u>";
152 }
153 write!(output, " <tr>")?;
154
155 for column in tree.columns() {
156 let item_value = item.column_value(tree, column);
157 let emoji = tree.emoji(column, item_value);
158 write!(
159 output,
160 "<td{bgcolor}>{emoji}</td>",
161 bgcolor = bgcolor,
162 emoji = emoji
163 )?;
164 }
165
166 write!(
167 output,
168 "<td{bgcolor}{href}>{start_tag}{label}{end_tag}</td>",
169 bgcolor = bgcolor,
170 href = href,
171 label = item.label(),
172 start_tag = start_tag,
173 end_tag = end_tag,
174 )?;
175
176 writeln!(output, "</tr>")?;
177 }
178
179 writeln!(output, r#" </table>>"#)?;
180}
181
182fn attribute_str(label: &str, text: &Option<impl AsRef<str>>, suffix: &str) -> String {
183 match text {
184 None => format!(""),
185 Some(t) => format!(" {}=\"{}{}\"", label, t.as_ref(), suffix),
186 }
187}