skill_tree/
graphviz.rs

1use crate::tree::{Graphviz, Group, ItemExt, SkillTree, Status};
2use fehler::throws;
3use std::io::Write;
4
5impl SkillTree {
6    /// Writes graphviz representing this skill-tree to the given output.
7    #[throws(anyhow::Error)]
8    pub fn write_graphviz(&self, output: &mut dyn Write) {
9        write_graphviz(self, output)?
10    }
11
12    /// Generates a string containing graphviz content for this skill-tree.
13    #[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        // If we are doing a cluster, the group must be in it;
69        // otherwise, the group must not be in any cluster.
70        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    // We have one column for each thing specified by user, plus the label.
111    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; // XXX
135        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}