Skip to main content

garbage_code_hunter/badge/
generator.rs

1//! SVG badge generator.
2
3/// Badge style.
4#[derive(Debug, Clone, Default, PartialEq)]
5pub enum BadgeStyle {
6    #[default]
7    Flat,
8    Plastic,
9}
10
11/// Get the color hex for a given score.
12fn score_color(score: f64) -> &'static str {
13    match score as u32 {
14        90..=100 => "#4c1",
15        70..=89 => "#97CA00",
16        50..=69 => "#dfb317",
17        30..=49 => "#fe7d37",
18        _ => "#e05d44",
19    }
20}
21
22/// Generate an SVG badge string.
23pub fn generate_svg(score: f64, style: &BadgeStyle) -> String {
24    let label = "garbage";
25    let score_str = format!("{:.0}", score);
26    let color = score_color(score);
27
28    let label_width: u32 = 75;
29    let score_width: u32 = 85.max(score_str.len() as u32 * 10 + 20);
30    let total_width = label_width + score_width;
31    let label_center = label_width / 2;
32    let score_center = label_width + score_width / 2;
33
34    match style {
35        BadgeStyle::Flat => {
36            let mut s = String::new();
37            s.push_str(&format!(
38                "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"20\">\n",
39                total_width
40            ));
41            s.push_str("  <linearGradient id=\"b\" x2=\"0\" y2=\"100%\">\n");
42            s.push_str("    <stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>\n");
43            s.push_str("    <stop offset=\"1\" stop-opacity=\".1\"/>\n");
44            s.push_str("  </linearGradient>\n");
45            s.push_str(&format!(
46                "  <mask id=\"a\"><rect width=\"{}\" height=\"20\" rx=\"3\" fill=\"#fff\"/></mask>\n",
47                total_width
48            ));
49            s.push_str("  <g mask=\"url(#a)\">\n");
50            s.push_str(&format!(
51                "    <rect width=\"{}\" height=\"20\" fill=\"#555\"/>\n",
52                label_width
53            ));
54            s.push_str(&format!(
55                "    <rect x=\"{}\" width=\"{}\" height=\"20\" fill=\"{}\"/>\n",
56                label_width, score_width, color
57            ));
58            s.push_str(&format!(
59                "    <rect width=\"{}\" height=\"20\" fill=\"url(#b)\"/>\n",
60                total_width
61            ));
62            s.push_str("  </g>\n");
63            s.push_str("  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"11\">\n");
64            s.push_str(&format!(
65                "    <text x=\"{}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{}</text>\n",
66                label_center, label
67            ));
68            s.push_str(&format!(
69                "    <text x=\"{}\" y=\"14\">{}</text>\n",
70                label_center, label
71            ));
72            s.push_str(&format!(
73                "    <text x=\"{}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{}</text>\n",
74                score_center, score_str
75            ));
76            s.push_str(&format!(
77                "    <text x=\"{}\" y=\"14\">{}</text>\n",
78                score_center, score_str
79            ));
80            s.push_str("  </g>\n");
81            s.push_str("</svg>");
82            s
83        }
84        BadgeStyle::Plastic => {
85            let mut s = String::new();
86            s.push_str(&format!(
87                "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"{}\" height=\"20\">\n",
88                total_width
89            ));
90            // Top shine highlight gradient (visible 3D effect)
91            s.push_str("  <linearGradient id=\"a\" x2=\"0\" y2=\"100%\">\n");
92            s.push_str("    <stop offset=\"0\" stop-color=\"#fff\" stop-opacity=\".35\"/>\n");
93            s.push_str("    <stop offset=\"1\" stop-opacity=\".15\"/>\n");
94            s.push_str("  </linearGradient>\n");
95            // Bottom shadow gradient
96            s.push_str("  <linearGradient id=\"b\" x2=\"0\" y2=\"100%\">\n");
97            s.push_str("    <stop offset=\"0\" stop-color=\"#000\" stop-opacity=\".12\"/>\n");
98            s.push_str("    <stop offset=\"1\" stop-opacity=\".25\"/>\n");
99            s.push_str("  </linearGradient>\n");
100            s.push_str(&format!(
101                "  <mask id=\"m\"><rect width=\"{}\" height=\"20\" rx=\"5\" fill=\"#fff\"/></mask>\n",
102                total_width
103            ));
104            s.push_str("  <g mask=\"url(#m)\">\n");
105            // Base colors
106            s.push_str(&format!(
107                "    <rect width=\"{}\" height=\"20\" fill=\"#555\"/>\n",
108                label_width
109            ));
110            s.push_str(&format!(
111                "    <rect x=\"{}\" width=\"{}\" height=\"20\" fill=\"{}\"/>\n",
112                label_width, score_width, color
113            ));
114            // Top shine strip (the signature plastic look)
115            s.push_str(&format!(
116                "    <rect width=\"{}\" height=\"10\" fill=\"url(#a)\"/>\n",
117                total_width
118            ));
119            // Bottom shadow
120            s.push_str(&format!(
121                "    <rect y=\"10\" width=\"{}\" height=\"10\" fill=\"url(#b)\"/>\n",
122                total_width
123            ));
124            s.push_str("  </g>\n");
125            // Thin white highlight line at very top for extra 3D pop
126            s.push_str(&format!(
127                "  <rect width=\"{}\" height=\"1\" fill=\"#fff\" fill-opacity=\".3\" rx=\"5\"/>\n",
128                total_width
129            ));
130            s.push_str("  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"11\">\n");
131            s.push_str(&format!(
132                "    <text x=\"{}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{}</text>\n",
133                label_center, label
134            ));
135            s.push_str(&format!(
136                "    <text x=\"{}\" y=\"14\">{}</text>\n",
137                label_center, label
138            ));
139            s.push_str(&format!(
140                "    <text x=\"{}\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">{}</text>\n",
141                score_center, score_str
142            ));
143            s.push_str(&format!(
144                "    <text x=\"{}\" y=\"14\">{}</text>\n",
145                score_center, score_str
146            ));
147            s.push_str("  </g>\n");
148            s.push_str("</svg>");
149            s
150        }
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157
158    #[test]
159    fn test_score_color_ranges() {
160        assert_eq!(score_color(95.0), "#4c1");
161        assert_eq!(score_color(80.0), "#97CA00");
162        assert_eq!(score_color(60.0), "#dfb317");
163        assert_eq!(score_color(40.0), "#fe7d37");
164        assert_eq!(score_color(20.0), "#e05d44");
165    }
166
167    #[test]
168    fn test_generate_flat_contains_svg() {
169        let svg = generate_svg(75.0, &BadgeStyle::Flat);
170        assert!(svg.contains("<svg"));
171        assert!(svg.contains("</svg>"));
172        assert!(svg.contains("garbage"));
173        assert!(svg.contains("75"));
174    }
175
176    #[test]
177    fn test_generate_plastic_contains_svg() {
178        let svg = generate_svg(90.0, &BadgeStyle::Plastic);
179        assert!(svg.contains("<svg"));
180        assert!(svg.contains("</svg>"));
181        assert!(svg.contains("90"));
182    }
183
184    #[test]
185    fn test_badge_uses_correct_color() {
186        let svg = generate_svg(95.0, &BadgeStyle::Flat);
187        assert!(svg.contains("#4c1"));
188
189        let svg = generate_svg(25.0, &BadgeStyle::Flat);
190        assert!(svg.contains("#e05d44"));
191    }
192
193    #[test]
194    fn test_badge_score_formatting() {
195        let svg = generate_svg(72.8, &BadgeStyle::Flat);
196        assert!(svg.contains("73")); // rounded
197    }
198}