1fn escape_xml_text(s: &str) -> String {
4 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("<"),
10 '>' => out.push_str(">"),
11 '"' => out.push_str("""),
12 '\'' => out.push_str("'"),
13 _ => out.push(ch),
14 }
15 }
16 out
17}
18
19pub fn badge_svg(label: &str, value: &str) -> String {
21 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}