shields/
builder.rs

1use crate::{
2    BadgeParams, BadgeStyle, default_label_color, default_message_color, render_badge_svg,
3};
4
5pub struct NoLink;
6pub struct OneLink {
7    link: String,
8}
9pub struct TwoLink {
10    link: String,
11    extra_link: String,
12}
13
14// 共通数据结构体
15pub struct CommonBadgeData {
16    label: String,
17    message: String,
18}
19
20impl CommonBadgeData {
21    fn new() -> Self {
22        Self {
23            label: String::new(),
24            message: String::new(),
25        }
26    }
27
28    // 将实际的 set 逻辑移到这里
29    fn set_label_internal(&mut self, label: &str) {
30        self.label = label.to_string();
31    }
32
33    fn set_message_internal(&mut self, message: &str) {
34        self.message = message.to_string();
35    }
36}
37
38pub struct SocialBadgeBuilder<State = NoLink> {
39    common_data: CommonBadgeData,
40    state: State,
41}
42
43impl<S> SocialBadgeBuilder<S> {
44    pub fn set_label(mut self, label: &str) -> Self {
45        self.common_data.set_label_internal(label);
46        self
47    }
48    pub fn set_message(mut self, message: &str) -> Self {
49        self.common_data.set_message_internal(message);
50        self
51    }
52}
53
54impl SocialBadgeBuilder<NoLink> {
55    pub fn new() -> Self {
56        Self {
57            common_data: CommonBadgeData::new(),
58            state: NoLink,
59        }
60    }
61    // 第一次设置 link,返回 OneLink 状态的新 Builder
62    pub fn set_link(&mut self, link: &str) -> SocialBadgeBuilder<OneLink> {
63        SocialBadgeBuilder {
64            common_data: CommonBadgeData {
65                label: self.common_data.label.clone(),
66                message: self.common_data.message.clone(),
67            },
68            state: OneLink {
69                link: link.to_string(),
70            },
71        }
72    }
73    pub fn build(&self) -> String {
74        render_badge_svg(&BadgeParams {
75            style: BadgeStyle::Social,
76            label: Some(self.common_data.label.as_str()),
77            message: self.common_data.message.as_str(),
78            label_color: None,
79            message_color: &"",
80            link: None,
81            extra_link: None,
82            logo: None,
83            logo_color: None,
84        })
85    }
86}
87
88impl SocialBadgeBuilder<OneLink> {
89    pub fn set_extra_link(self, link: &str) -> SocialBadgeBuilder<TwoLink> {
90        SocialBadgeBuilder {
91            common_data: self.common_data,
92            state: TwoLink {
93                link: self.state.link,
94                extra_link: link.to_string(),
95            },
96        }
97    }
98    pub fn build(&self) -> String {
99        render_badge_svg(&BadgeParams {
100            style: BadgeStyle::Social,
101            label: Some(self.common_data.label.as_str()),
102            message: self.common_data.message.as_str(),
103            label_color: None,
104            message_color: &"",
105            link: Some(&self.state.link),
106            extra_link: None,
107            logo: None,
108            logo_color: None,
109        })
110    }
111}
112
113impl SocialBadgeBuilder<TwoLink> {
114    pub fn build(&self) -> String {
115        render_badge_svg(&BadgeParams {
116            style: BadgeStyle::Social,
117            label: Some(self.common_data.label.as_str()),
118            message: self.common_data.message.as_str(),
119            label_color: None,
120            message_color: &"",
121            link: Some(&self.state.link),
122            extra_link: Some(&self.state.extra_link),
123            logo: None,
124            logo_color: None,
125        })
126    }
127}
128
129pub struct ColorBadgeBuilder<State = NoLink> {
130    common_data: CommonBadgeData,
131    style: BadgeStyle,
132    label_color: String,
133    message_color: String,
134    state: State,
135}
136
137impl ColorBadgeBuilder<NoLink> {
138    pub fn new(style: BadgeStyle) -> Self {
139        Self {
140            style,
141            common_data: CommonBadgeData::new(),
142            label_color: String::from(default_label_color()),
143            message_color: String::from(default_message_color()),
144            state: NoLink,
145        }
146    }
147    pub fn set_link(self, link: &str) -> ColorBadgeBuilder<OneLink> {
148        ColorBadgeBuilder {
149            style: self.style,
150            common_data: self.common_data,
151            label_color: self.label_color,
152            message_color: self.message_color,
153            state: OneLink {
154                link: link.to_string(),
155            },
156        }
157    }
158}
159
160impl ColorBadgeBuilder<OneLink> {
161    pub fn set_extra_link(self, extra: &str) -> ColorBadgeBuilder<TwoLink> {
162        ColorBadgeBuilder {
163            style: self.style,
164            common_data: self.common_data,
165            label_color: self.label_color,
166            message_color: self.message_color,
167            state: TwoLink {
168                link: self.state.link,
169                extra_link: extra.to_string(),
170            },
171        }
172    }
173}
174
175impl<S> ColorBadgeBuilder<S> {
176    pub fn set_label_color(&mut self, color: &str) -> &mut Self {
177        self.label_color = color.to_string();
178        self
179    }
180    pub fn set_message_color(&mut self, color: &str) -> &mut Self {
181        self.message_color = color.to_string();
182        self
183    }
184
185    pub fn build(&self) -> String {
186        // 这里可以根据 style, label, message 等生成 badge 的 URL
187        return render_badge_svg(&BadgeParams {
188            style: self.style,
189            label: Some(self.common_data.label.as_str()),
190            message: self.common_data.message.as_str(),
191            label_color: Some(self.label_color.as_str()),
192            message_color: self.message_color.as_str(),
193            link: None,
194            extra_link: None,
195            logo: None,
196            logo_color: None,
197        });
198    }
199}
200
201impl<S> ColorBadgeBuilder<S> {
202    pub fn set_label(mut self, label: &str) -> Self {
203        self.common_data.set_label_internal(label);
204        self
205    }
206    pub fn set_message(mut self, message: &str) -> Self {
207        self.common_data.set_message_internal(message);
208        self
209    }
210}
211
212pub struct Builder {}
213
214impl Builder {
215    pub fn social(self) -> SocialBadgeBuilder {
216        SocialBadgeBuilder::new()
217    }
218
219    pub fn flat(self) -> ColorBadgeBuilder {
220        ColorBadgeBuilder::new(BadgeStyle::Base(crate::BaseBadgeStyle::Flat))
221    }
222
223    pub fn flat_square(self) -> ColorBadgeBuilder {
224        ColorBadgeBuilder::new(BadgeStyle::Base(crate::BaseBadgeStyle::FlatSquare))
225    }
226
227    pub fn plastic(self) -> ColorBadgeBuilder {
228        ColorBadgeBuilder::new(BadgeStyle::Base(crate::BaseBadgeStyle::Plastic))
229    }
230}
231
232#[cfg(test)]
233
234mod tests {
235    use super::*;
236    #[test]
237    fn test_builder() {
238        let builder = Builder {};
239        let badge = builder
240            .flat()
241            .set_label("test")
242            .set_message("test")
243            .set_label_color("#000000")
244            .set_message_color("#FFFFFF")
245            .build();
246        assert_eq!(
247            badge,
248            r##"<svg xmlns="http://www.w3.org/2000/svg" width="62" height="20" role="img" aria-label="test: test"><title>test: test</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="62" height="20" rx="3" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="31" height="20" fill="#000000"/><rect x="31" width="31" height="20" fill="#ffffff"/><rect width="62" 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="165" y="150" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">test</text><text x="165" y="140" transform="scale(.1)" fill="#fff" textLength="210">test</text><text aria-hidden="true" x="455" y="150" fill="#ccc" fill-opacity=".3" transform="scale(.1)" textLength="210">test</text><text x="455" y="140" transform="scale(.1)" fill="#333" textLength="210">test</text></g></svg>"##
249        );
250    }
251    #[test]
252    fn test_builder_social_with_link() {
253        let builder = Builder {};
254        let badge = builder
255            .social()
256            .set_label("test")
257            .set_link("https://example.com")
258            .set_message("test")
259            .set_extra_link("https://example.com/extra")
260            .build();
261        assert_eq!(
262            badge,
263            r##"<svg xmlns="http://www.w3.org/2000/svg" width="67" height="20"><style>a:hover #llink{fill:url(#b);stroke:#ccc}a:hover #rlink{fill:#4183c4}</style><linearGradient id="a" x2="0" y2="100%"><stop offset="0" stop-color="#fcfcfc" stop-opacity="0"/><stop offset="1" stop-opacity=".1"/></linearGradient><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#ccc" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><g stroke="#d5d5d5"><rect stroke="none" fill="#fcfcfc" x="0.5" y="0.5" width="33" height="19" rx="2"/><rect x="39.5" y="0.5" width="27" height="19" rx="2" fill="#fafafa"/><rect x="39" y="7.5" width="0.5" height="5" stroke="#fafafa"/><path d="M39.5 6.5 l-3 3v1 l3 3" stroke="d5d5d5" fill="#fafafa"/></g><g aria-hidden="false" fill="#333" text-anchor="middle" font-family="Helvetica Neue,Helvetica,Arial,sans-serif" text-rendering="geometricPrecision" font-weight="700" font-size="110px" line-height="14px"><a target="_blank" href="https://example.com"><text aria-hidden="true" x="165" y="150" fill="#fff" transform="scale(.1)" textLength="230">Test</text><text x="165" y="140" transform="scale(.1)" textLength="230">Test</text><rect id="llink" stroke="#d5d5d5" fill="url(#a)" x=".5" y=".5" width="33" height="19" rx="2"/></a><a target="_blank" href="https://example.com/extra"><rect width="28" x="39" height="20" fill="rgba(0,0,0,0)"/><text aria-hidden="true" x="525" y="150" fill="#fff" transform="scale(.1)" textLength="190">test</text><text id="rlink" x="525" y="140" transform="scale(.1)" textLength="190">test</text></a></g></svg>"##
264        )
265    }
266
267    #[test]
268    fn test_builder_plastic_with_link() {
269        let builder = Builder {};
270        let badge = builder
271            .plastic()
272            .set_label("test")
273            .set_link("https://example.com")
274            .set_message("test")
275            .build();
276        assert_eq!(
277            badge,
278            r##"<svg xmlns="http://www.w3.org/2000/svg" width="62" height="18" role="img" aria-label="test: test"><title>test: test</title><linearGradient id="s" x2="0" y2="100%"><stop offset="0" stop-color="#fff" stop-opacity=".7"/><stop offset=".1" stop-color="#aaa" stop-opacity=".1"/><stop offset=".9" stop-color="#000" stop-opacity=".3"/><stop offset="1" stop-color="#000" stop-opacity=".5"/></linearGradient><clipPath id="r"><rect width="62" height="18" rx="4" fill="#fff"/></clipPath><g clip-path="url(#r)"><rect width="31" height="18" fill="#555"/><rect x="31" width="31" height="18" fill="#007ec6"/><rect width="62" height="18" 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="165" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">test</text><text x="165" y="130" transform="scale(.1)" fill="#fff" textLength="210">test</text><text aria-hidden="true" x="455" y="140" fill="#010101" fill-opacity=".3" transform="scale(.1)" textLength="210">test</text><text x="455" y="130" transform="scale(.1)" fill="#fff" textLength="210">test</text></g></svg>"##
279        );
280    }
281}