1use regex::Regex;
2use std::sync::OnceLock;
3
4#[derive(Debug, Clone, PartialEq)]
6pub struct ImageCandidate {
7 pub url: String,
8 pub width: Option<f64>,
9 pub density: Option<f64>,
10}
11
12impl PartialOrd for ImageCandidate {
13 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
14 match (self.width, self.density, other.width, other.density) {
15 (Some(a), None, Some(b), None) => Some(a.partial_cmp(&b).unwrap()),
16 (None, Some(a), None, Some(b)) => Some(a.partial_cmp(&b).unwrap()),
17 _ => None,
18 }
19 }
20}
21
22static SRCSEG_PATTERN: &str = r"(\S*[^,\s])(\s+([\d.]+)(x|w))?";
33static SRCSEG_REGEX: OnceLock<Regex> = OnceLock::new();
34
35pub fn parse(srcset: &str) -> Vec<ImageCandidate> {
47 let re = SRCSEG_REGEX.get_or_init(|| Regex::new(SRCSEG_PATTERN).expect("Invalid regex"));
48 let mut results = Vec::new();
49
50 for caps in re.captures_iter(srcset) {
51 let url = caps
53 .get(1)
54 .map(|m| m.as_str().to_string())
55 .unwrap_or_default();
56
57 let value = caps.get(3).map(|m| m.as_str());
59 let descriptor = caps.get(4).map(|m| m.as_str());
61
62 let parsed_value = value.map(|v| v.parse::<f64>().unwrap_or_default());
64
65 let (width, density) = match descriptor {
67 Some("w") => (parsed_value, None),
68 Some("x") => (None, parsed_value),
69 _ => (None, None),
70 };
71
72 results.push(ImageCandidate {
73 url,
74 width,
75 density,
76 });
77 }
78
79 results
80}
81
82#[cfg(test)]
83mod tests {
84
85 use super::{parse, ImageCandidate};
86
87 #[test]
88 fn parses_srcset_strings() {
89 let srcset = "cat-@2x.jpeg 2x, dog.jpeg 100w";
90 let result = parse(srcset);
91 assert_eq!(
92 result,
93 vec![
94 ImageCandidate {
95 url: "cat-@2x.jpeg".to_string(),
96 width: None,
97 density: Some(2.0),
98 },
99 ImageCandidate {
100 url: "dog.jpeg".to_string(),
101 width: Some(100.0),
102 density: None,
103 },
104 ]
105 );
106 }
107
108 #[test]
109 fn ignores_extra_whitespaces() {
110 let srcset = r#"
111 foo-bar.png 2x ,
112 bar-baz.png 100w
113 "#;
114
115 let result = parse(srcset);
116 assert_eq!(
117 result,
118 vec![
119 ImageCandidate {
120 url: "foo-bar.png".to_string(),
121 width: None,
122 density: Some(2.0),
123 },
124 ImageCandidate {
125 url: "bar-baz.png".to_string(),
126 width: Some(100.0),
127 density: None,
128 },
129 ]
130 );
131 }
132
133 #[test]
134 fn properly_parses_float_descriptors() {
135 let srcset = "cat.jpeg 2.4x, dog.jpeg 1.5x";
136 let result = parse(srcset);
137 assert_eq!(
138 result,
139 vec![
140 ImageCandidate {
141 url: "cat.jpeg".to_string(),
142 width: None,
143 density: Some(2.4),
144 },
145 ImageCandidate {
146 url: "dog.jpeg".to_string(),
147 width: None,
148 density: Some(1.5),
149 },
150 ]
151 );
152 }
153
154 #[test]
155 fn supports_urls_that_contain_comma() {
156 let srcset = r#"
157 https://foo.bar/w=100,h=200/dog.png 100w,
158 https://baz.bar/cat.png?meow=yes 1024w
159 "#;
160
161 let result = parse(srcset);
162 assert_eq!(
163 result,
164 vec![
165 ImageCandidate {
166 url: "https://foo.bar/w=100,h=200/dog.png".to_string(),
167 width: Some(100.0),
168 density: None,
169 },
170 ImageCandidate {
171 url: "https://baz.bar/cat.png?meow=yes".to_string(),
172 width: Some(1024.0),
173 density: None,
174 },
175 ]
176 );
177 }
178
179 #[test]
180 fn supports_single_urls() {
181 let srcset = "/cat.jpg";
182 let result = parse(srcset);
183 assert_eq!(
184 result,
185 vec![ImageCandidate {
186 url: "/cat.jpg".to_string(),
187 width: None,
188 density: None,
189 }]
190 );
191 }
192
193 #[test]
194 fn supports_optional_descriptors() {
195 let srcset = "/cat.jpg, /dog.png 3x , /lol ";
196 let result = parse(srcset);
197 assert_eq!(
198 result,
199 vec![
200 ImageCandidate {
201 url: "/cat.jpg".to_string(),
202 width: None,
203 density: None,
204 },
205 ImageCandidate {
206 url: "/dog.png".to_string(),
207 width: None,
208 density: Some(3.0),
209 },
210 ImageCandidate {
211 url: "/lol".to_string(),
212 width: None,
213 density: None,
214 },
215 ]
216 );
217 }
218}