Line data Source code
1 : const CHARACTER_WIDTH: usize = 7;
2 : const PERCENT_WIDTH: usize = 11;
3 : const PADDING: usize = 10;
4 :
5 10 : fn value_text_width(value: &str) -> usize {
6 73 : value.chars().map(|c| if c == '%' { PERCENT_WIDTH } else { CHARACTER_WIDTH }).sum()
7 10 : }
8 :
9 10 : fn build_badge(label: &str, label_text_width: usize, value: &str, color: &str) -> String {
10 10 : let escaped_value = value
11 10 : .replace('&', "&")
12 10 : .replace('<', "<")
13 10 : .replace('>', ">")
14 10 : .replace('"', """)
15 10 : .replace('\'', "'");
16 10 : let label_width = label_text_width + PADDING;
17 10 : let value_width = value_text_width(value) + PADDING;
18 10 : let width = label_width + value_width;
19 10 : let label_center = label_width * 5;
20 10 : let value_center = label_width * 10 + value_width * 5;
21 10 : let label_text_length = label_text_width * 10;
22 10 : let value_text_length = (value_width - PADDING) * 10;
23 10 : let title = format!("{label}: {escaped_value}");
24 :
25 10 : format!(
26 : r##"<svg xmlns="http://www.w3.org/2000/svg" width="{width}" height="20" role="img" aria-label="{title}"><title>{title}</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><clipPath id="r"><rect width="{width}" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="{label_width}" height="20" fill="#555"/><rect x="{label_width}" width="{value_width}" height="20" fill="{color}"/><rect width="{width}" height="20" fill="url(#s)"/></g><g fill="#fff" text-anchor="middle" font-family="Verdana,Geneva,DejaVu Sans,sans-serif" text-rendering="geometricPrecision" font-size="110"><text aria-hidden="true" x="{label_center}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="{label_text_length}">{label}</text><text x="{label_center}" y="140" transform="scale(.1)" fill="#fff" textLength="{label_text_length}">{label}</text><text aria-hidden="true" x="{value_center}" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="{value_text_length}">{escaped_value}</text><text x="{value_center}" y="140" transform="scale(.1)" fill="#fff" textLength="{value_text_length}">{escaped_value}</text></g></svg>"##
27 : )
28 10 : }
29 :
30 6 : pub fn build_version_badge(version: &str) -> String {
31 6 : build_badge("version", 41, version, "#007ec6")
32 6 : }
33 :
34 : // The thresholds and colors of the Coveralls badges, with the green of the license badge from 90 up
35 10 : fn coverage_color(percentage: f64) -> &'static str {
36 10 : if percentage < 80.0 {
37 4 : "#e05d44"
38 6 : } else if percentage < 90.0 {
39 2 : "#dfb317"
40 : } else {
41 4 : "#4c1"
42 : }
43 10 : }
44 :
45 4 : pub fn build_coverage_badge(percentage: f64) -> String {
46 : // The color follows the number on the badge
47 4 : let rounded = percentage.round();
48 4 : build_badge("coverage", 51, &format!("{rounded}%"), coverage_color(rounded))
49 4 : }
50 :
51 : #[cfg(test)]
52 : mod tests {
53 : use super::*;
54 :
55 : #[test]
56 1 : fn builds_version_badge() {
57 1 : let badge = build_version_badge("0.0.7-dev.6");
58 :
59 1 : assert!(badge.contains("width=\"138\""));
60 1 : assert!(badge.contains("aria-label=\"version: 0.0.7-dev.6\""));
61 1 : assert!(badge.contains(">version</text>"));
62 1 : assert!(badge.contains("fill=\"#007ec6\""));
63 1 : assert_eq!(badge.matches(">0.0.7-dev.6</text>").count(), 2);
64 1 : }
65 :
66 : #[test]
67 1 : fn escapes_version_badge_text() {
68 1 : let badge = build_version_badge("1<&\"'>");
69 :
70 1 : assert!(badge.contains("version: 1<&"'>"));
71 1 : assert!(!badge.contains("version: 1<&\"'>"));
72 1 : }
73 :
74 : #[test]
75 1 : fn builds_coverage_badge() {
76 1 : let badge = build_coverage_badge(90.53);
77 :
78 1 : assert!(badge.contains("width=\"96\""));
79 1 : assert!(badge.contains("aria-label=\"coverage: 91%\""));
80 1 : assert!(badge.contains(">coverage</text>"));
81 1 : assert!(badge.contains("fill=\"#4c1\""));
82 1 : assert_eq!(badge.matches(">91%</text>").count(), 2);
83 1 : }
84 :
85 : #[test]
86 1 : fn colors_coverage_by_thresholds() {
87 1 : assert_eq!(coverage_color(0.0), "#e05d44");
88 1 : assert_eq!(coverage_color(79.0), "#e05d44");
89 1 : assert_eq!(coverage_color(80.0), "#dfb317");
90 1 : assert_eq!(coverage_color(89.0), "#dfb317");
91 1 : assert_eq!(coverage_color(90.0), "#4c1");
92 1 : assert_eq!(coverage_color(100.0), "#4c1");
93 1 : }
94 :
95 : #[test]
96 1 : fn colors_coverage_by_the_number_on_the_badge() {
97 1 : let badge = build_coverage_badge(89.6);
98 1 : assert!(badge.contains("aria-label=\"coverage: 90%\""));
99 1 : assert!(badge.contains("fill=\"#4c1\""));
100 :
101 1 : let badge = build_coverage_badge(79.4);
102 1 : assert!(badge.contains("aria-label=\"coverage: 79%\""));
103 1 : assert!(badge.contains("fill=\"#e05d44\""));
104 1 : }
105 : }
|