Skip to main content

tokmd_badge/
lib.rs

1//! SVG badge rendering helpers.
2
3fn escape_xml_text(s: &str) -> String {
4    // Minimal XML escaping for text nodes to keep SVG valid and safe.
5    let mut out = String::with_capacity(s.len());
6    for ch in s.chars() {
7        match ch {
8            '&' => out.push_str("&"),
9            '<' => out.push_str("&lt;"),
10            '>' => out.push_str("&gt;"),
11            '"' => out.push_str("&quot;"),
12            '\'' => out.push_str("&apos;"),
13            _ => out.push(ch),
14        }
15    }
16    out
17}
18
19/// Build a compact two-segment SVG badge.
20pub fn badge_svg(label: &str, value: &str) -> String {
21    // Width is heuristic; char count avoids UTF-8 byte-length drift.
22    let label_chars = label.chars().count() as i32;
23    let value_chars = value.chars().count() as i32;
24    let label_width = (label_chars * 7 + 20).max(60);
25    let value_width = (value_chars * 7 + 20).max(60);
26    let width = label_width + value_width;
27    let height = 24;
28    let label_x = label_width / 2;
29    let value_x = label_width + value_width / 2;
30    let label_escaped = escape_xml_text(label);
31    let value_escaped = escape_xml_text(value);
32    format!(
33        "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{width}\" height=\"{height}\" role=\"img\"><rect width=\"{label_width}\" height=\"{height}\" fill=\"#555\"/><rect x=\"{label_width}\" width=\"{value_width}\" height=\"{height}\" fill=\"#4c9aff\"/><text x=\"{label_x}\" y=\"16\" fill=\"#fff\" font-family=\"Verdana\" font-size=\"11\" text-anchor=\"middle\">{label}</text><text x=\"{value_x}\" y=\"16\" fill=\"#fff\" font-family=\"Verdana\" font-size=\"11\" text-anchor=\"middle\">{value}</text></svg>",
34        width = width,
35        height = height,
36        label_width = label_width,
37        value_width = value_width,
38        label_x = label_x,
39        value_x = value_x,
40        label = label_escaped,
41        value = value_escaped
42    )
43}
44
45#[cfg(test)]
46mod tests {
47    use super::{badge_svg, escape_xml_text};
48
49    #[test]
50    fn badge_svg_contains_label_and_value() {
51        let svg = badge_svg("lines", "1234");
52        assert!(svg.contains("lines"));
53        assert!(svg.contains("1234"));
54    }
55
56    #[test]
57    fn badge_svg_is_valid_svg() {
58        let svg = badge_svg("test", "42");
59        assert!(svg.starts_with("<svg"));
60        assert!(svg.ends_with("</svg>"));
61        assert!(svg.contains("xmlns=\"http://www.w3.org/2000/svg\""));
62    }
63
64    #[test]
65    fn badge_svg_dimensions_calculated_correctly() {
66        let svg = badge_svg("ab", "1");
67        assert!(svg.contains("width=\"120\""));
68
69        let svg = badge_svg("longlabel", "longvalue");
70        assert!(svg.contains("width=\"166\""));
71    }
72
73    #[test]
74    fn badge_svg_positions_are_centered() {
75        let svg = badge_svg("ab", "1");
76        assert!(svg.contains("x=\"30\""));
77        assert!(svg.contains("x=\"90\""));
78    }
79
80    #[test]
81    fn badge_svg_width_scales_with_text() {
82        let short_svg = badge_svg("a", "1");
83        let long_svg = badge_svg("averylonglabel", "averylongvalue");
84        assert!(extract_svg_width(&long_svg) > extract_svg_width(&short_svg));
85    }
86
87    #[test]
88    fn badge_svg_escapes_xml_text_nodes() {
89        let label = "a<&>\"'";
90        let value = "b<&>\"'";
91        let svg = badge_svg(label, value);
92        assert!(svg.contains(&escape_xml_text(label)));
93        assert!(svg.contains(&escape_xml_text(value)));
94        assert!(!svg.contains(label));
95        assert!(!svg.contains(value));
96    }
97
98    fn extract_svg_width(svg: &str) -> i32 {
99        let start = svg.find("width=\"").expect("width attr") + 7;
100        let end = svg[start..].find('"').expect("width close") + start;
101        svg[start..end].parse().expect("numeric width")
102    }
103}