pugio_lib/
template.rs

1use std::collections::BTreeMap;
2
3use serde::Serialize;
4use tinytemplate::TinyTemplate;
5
6use crate::{coloring::NodeColoringScheme, error::TemplateError, graph::Graph};
7
8/// Trait for templating node and edge labels and tooltips.
9pub trait Templating {
10    /// The type of context shared across all nodes and edges.
11    type Context;
12    /// The type of value for each node.
13    type Value;
14
15    /// Get the label and tooltip for a given node.
16    fn node(
17        &self,
18        graph: &Graph,
19        index: usize,
20        value: Self::Value,
21        context: Self::Context,
22    ) -> (String, String);
23
24    /// Get the label and tooltip for a given edge.
25    fn edge(&self, graph: &Graph, source: usize, target: usize) -> (String, String);
26}
27
28/// Options for [`Template`] creation.
29///
30/// Internally, [`Template`] uses [`TinyTemplate`](https://docs.rs/tinytemplate/latest/tinytemplate/)
31/// for templating, which allows formatting using the `{...}` syntax.
32///
33/// # Node template values
34///
35/// - `short`: Short name of the node.
36/// - `extra`: Extra information of the node.
37/// - `full`: Full name of the node.
38/// - `size`: Size of the node in bytes.
39/// - `size_binary`: Size of the node in binary format (e.g., "1.0 KiB").
40/// - `size_decimal`: Size of the node in decimal format (e.g., "1.0 kB").
41/// - `scheme`: Coloring scheme used (if any).
42/// - `value`: Value used for coloring (if any).
43/// - `value_binary`: Value used for coloring in binary format (if any).
44/// - `value_decimal`: Value used for coloring in decimal format (if any).
45/// - `features`: Features of the node.
46///
47/// # Edge template values
48/// - `source`: Short name of the source node.
49/// - `target`: Short name of the target node.
50/// - `features`: Features of the edge.
51#[derive(Debug, Default)]
52pub struct TemplateOptions {
53    pub node_label_template: Option<String>,
54    pub node_tooltip_template: Option<String>,
55    pub edge_label_template: Option<String>,
56    pub edge_tooltip_template: Option<String>,
57}
58
59/// Templating system for node and edge labels and tooltips.
60///
61/// This implements the [`Templating`] trait to be used in conjunction with `Option<NodeColoringValues>`.
62pub struct Template<'a>(pub(crate) TinyTemplate<'a>);
63
64impl<'a> Template<'a> {
65    /// Create a new [`Template`] from the given [`TemplateOptions`].
66    ///
67    /// # Defaults
68    ///
69    /// If a template option field is `None`, its corresponding default is used:
70    /// - Node label: `"{short}"`
71    /// - Node tooltip: `"{full}\n{size_binary}\n{features}"`
72    /// - Edge label: `"{features}"`
73    /// - Edge tooltip: `"{source} -> {target}"`
74    pub fn new(template_options: &'a TemplateOptions) -> Result<Self, TemplateError> {
75        let mut template = TinyTemplate::new();
76        template.add_template(
77            "node_label",
78            template_options
79                .node_label_template
80                .as_deref()
81                .unwrap_or("{short}"),
82        )?;
83        template.add_template(
84            "node_tooltip",
85            template_options
86                .node_tooltip_template
87                .as_deref()
88                .unwrap_or("{full}\n{size_binary}\n{features}"),
89        )?;
90        template.add_template(
91            "edge_label",
92            template_options
93                .edge_label_template
94                .as_deref()
95                .unwrap_or("{features}"),
96        )?;
97        template.add_template(
98            "edge_tooltip",
99            template_options
100                .edge_tooltip_template
101                .as_deref()
102                .unwrap_or("{source} -> {target}"),
103        )?;
104        Ok(Template(template))
105    }
106}
107
108impl<'a> Templating for Template<'a> {
109    type Context = Option<NodeColoringScheme>;
110    type Value = Option<usize>;
111
112    fn node(
113        &self,
114        graph: &Graph,
115        index: usize,
116        value: Self::Value,
117        context: Self::Context,
118    ) -> (String, String) {
119        #[derive(Serialize)]
120        struct NodeContext<'a> {
121            short: &'a str,
122            extra: &'a str,
123            full: &'a str,
124            size: usize,
125            size_binary: String,
126            size_decimal: String,
127            scheme: Option<&'static str>,
128            value: Option<usize>,
129            value_binary: Option<String>,
130            value_decimal: Option<String>,
131            features: String,
132        }
133
134        let node = graph.node_weight(index);
135        let size = graph.size(index).unwrap_or_default();
136
137        let context = NodeContext {
138            short: node.short(),
139            extra: node.extra(),
140            full: node.full(),
141            size,
142            size_binary: humansize::format_size(size, humansize::BINARY),
143            size_decimal: humansize::format_size(size, humansize::DECIMAL),
144            scheme: context.map(Into::into),
145            value,
146            value_binary: value.map(|v| humansize::format_size(v, humansize::BINARY)),
147            value_decimal: value.map(|v| humansize::format_size(v, humansize::DECIMAL)),
148            features: features(&node.features),
149        };
150
151        let label = self
152            .0
153            .render("node_label", &context)
154            .unwrap_or_else(|e| e.to_string());
155        let tooltip = self
156            .0
157            .render("node_tooltip", &context)
158            .unwrap_or_else(|e| e.to_string());
159        (label, tooltip)
160    }
161
162    fn edge(&self, graph: &Graph, source: usize, target: usize) -> (String, String) {
163        #[derive(Serialize)]
164        struct EdgeContext<'a> {
165            source: &'a str,
166            target: &'a str,
167            features: String,
168        }
169
170        let edge = graph.edge_weight(source, target);
171        let source = graph.node_weight(source);
172        let target = graph.node_weight(target);
173
174        let context = EdgeContext {
175            source: source.short(),
176            target: target.short(),
177            features: features(&edge.features),
178        };
179
180        let label = self
181            .0
182            .render("edge_label", &context)
183            .unwrap_or_else(|e| e.to_string());
184        let tooltip = self
185            .0
186            .render("edge_tooltip", &context)
187            .unwrap_or_else(|e| e.to_string());
188        (label, tooltip)
189    }
190}
191
192fn features(features: &BTreeMap<String, Vec<String>>) -> String {
193    features
194        .iter()
195        .map(|(f, d)| {
196            if d.is_empty() {
197                f.clone()
198            } else {
199                format!("{f}({})", d.join(","))
200            }
201        })
202        .collect::<Vec<String>>()
203        .join(",\n")
204}