1#![no_std]
27#![doc(html_root_url = "https://docs.rs/parse-srcset/0.1.0")]
28
29extern crate alloc;
30
31use alloc::string::{String, ToString};
32use alloc::vec::Vec;
33
34#[cfg(doctest)]
36#[doc = include_str!("../README.md")]
37struct ReadmeDoctests;
38
39#[derive(Debug, Clone, PartialEq, Default)]
41pub struct ImageCandidate {
42 pub url: String,
44 pub density: Option<f64>,
46 pub width: Option<u64>,
48 pub height: Option<u64>,
50}
51
52#[must_use]
63pub fn parse_srcset(input: &str) -> Vec<ImageCandidate> {
64 let chars: Vec<char> = input.chars().collect();
65 let len = chars.len();
66 let mut pos = 0;
67 let mut candidates: Vec<ImageCandidate> = Vec::new();
68
69 loop {
70 while pos < len && is_comma_or_space(chars[pos]) {
72 pos += 1;
73 }
74 if pos >= len {
75 return candidates;
76 }
77
78 let url_start = pos;
80 while pos < len && !is_space(chars[pos]) {
81 pos += 1;
82 }
83 let url_token: String = chars[url_start..pos].iter().collect();
84
85 if url_token.ends_with(',') {
86 let url = url_token.trim_end_matches(',').to_string();
88 if let Some(c) = parse_descriptors(url, &[]) {
89 candidates.push(c);
90 }
91 } else if let Some(c) = parse_descriptors(url_token, &tokenize(&chars, &mut pos, len)) {
92 candidates.push(c);
93 }
94 }
95}
96
97#[derive(Clone, Copy)]
98enum State {
99 InDescriptor,
100 InParens,
101 AfterDescriptor,
102}
103
104fn tokenize(chars: &[char], pos: &mut usize, len: usize) -> Vec<String> {
106 while *pos < len && is_space(chars[*pos]) {
108 *pos += 1;
109 }
110
111 let mut descriptors: Vec<String> = Vec::new();
112 let mut current = String::new();
113 let mut state = State::InDescriptor;
114
115 loop {
116 let c = chars.get(*pos).copied(); match state {
118 State::InDescriptor => match c {
119 Some(ch) if is_space(ch) => {
120 if !current.is_empty() {
121 descriptors.push(core::mem::take(&mut current));
122 state = State::AfterDescriptor;
123 }
124 }
125 Some(',') => {
126 *pos += 1;
127 if !current.is_empty() {
128 descriptors.push(current);
129 }
130 return descriptors;
131 }
132 Some('(') => {
133 current.push('(');
134 state = State::InParens;
135 }
136 None => {
137 if !current.is_empty() {
138 descriptors.push(current);
139 }
140 return descriptors;
141 }
142 Some(ch) => current.push(ch),
143 },
144 State::InParens => match c {
145 Some(')') => {
146 current.push(')');
147 state = State::InDescriptor;
148 }
149 None => {
150 descriptors.push(current);
151 return descriptors;
152 }
153 Some(ch) => current.push(ch),
154 },
155 State::AfterDescriptor => match c {
156 Some(ch) if is_space(ch) => {} None => return descriptors,
158 Some(_) => {
159 state = State::InDescriptor;
160 *pos -= 1;
161 }
162 },
163 }
164 *pos += 1;
165 }
166}
167
168fn parse_descriptors(url: String, descriptors: &[String]) -> Option<ImageCandidate> {
170 let mut error = false;
171 let mut width: Option<u64> = None;
172 let mut density: Option<f64> = None;
173 let mut height: Option<u64> = None;
174
175 let truthy_w = |w: Option<u64>| w.is_some();
177 let truthy_h = |h: Option<u64>| h.is_some();
178 let truthy_d = |d: Option<f64>| d.is_some_and(|v| v != 0.0);
179
180 for desc in descriptors {
181 let last = desc.chars().next_back().unwrap_or('\0');
182 let value: String = {
183 let mut it = desc.chars();
184 it.next_back();
185 it.collect()
186 };
187
188 if last == 'w' && is_non_negative_integer(&value) {
189 if truthy_w(width) || truthy_d(density) {
190 error = true;
191 }
192 match value.parse::<u64>() {
193 Ok(0) | Err(_) => error = true,
194 Ok(n) => width = Some(n),
195 }
196 } else if last == 'x' && is_floating_point(&value) {
197 if truthy_w(width) || truthy_d(density) || truthy_h(height) {
198 error = true;
199 }
200 match value.parse::<f64>() {
201 Ok(f) if f >= 0.0 => density = Some(f),
202 _ => error = true,
203 }
204 } else if last == 'h' && is_non_negative_integer(&value) {
205 if truthy_h(height) || truthy_d(density) {
206 error = true;
207 }
208 match value.parse::<u64>() {
209 Ok(0) | Err(_) => error = true,
210 Ok(n) => height = Some(n),
211 }
212 } else {
213 error = true;
214 }
215 }
216
217 if error {
218 return None;
219 }
220 Some(ImageCandidate {
221 url,
222 density: if truthy_d(density) { density } else { None },
223 width: if truthy_w(width) { width } else { None },
224 height: if truthy_h(height) { height } else { None },
225 })
226}
227
228fn is_space(c: char) -> bool {
229 matches!(c, ' ' | '\t' | '\n' | '\u{000c}' | '\r')
230}
231
232fn is_comma_or_space(c: char) -> bool {
233 c == ',' || is_space(c)
234}
235
236fn is_non_negative_integer(s: &str) -> bool {
238 !s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
239}
240
241fn is_floating_point(s: &str) -> bool {
243 let b = s.as_bytes();
244 let len = b.len();
245 let mut i = 0;
246 if i < len && b[i] == b'-' {
247 i += 1;
248 }
249 let int_digits = count_digits(b, &mut i);
250 if i < len && b[i] == b'.' {
251 i += 1;
252 if count_digits(b, &mut i) == 0 {
253 return false; }
255 } else if int_digits == 0 {
256 return false; }
258 if i < len && (b[i] == b'e' || b[i] == b'E') {
259 i += 1;
260 if i < len && (b[i] == b'+' || b[i] == b'-') {
261 i += 1;
262 }
263 if count_digits(b, &mut i) == 0 {
264 return false; }
266 }
267 i == len
268}
269
270fn count_digits(b: &[u8], i: &mut usize) -> usize {
271 let start = *i;
272 while *i < b.len() && b[*i].is_ascii_digit() {
273 *i += 1;
274 }
275 *i - start
276}