#![no_std]
#![doc(html_root_url = "https://docs.rs/parse-srcset/0.1.0")]
extern crate alloc;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
#[cfg(doctest)]
#[doc = include_str!("../README.md")]
struct ReadmeDoctests;
#[derive(Debug, Clone, PartialEq, Default)]
pub struct ImageCandidate {
pub url: String,
pub density: Option<f64>,
pub width: Option<u64>,
pub height: Option<u64>,
}
#[must_use]
pub fn parse_srcset(input: &str) -> Vec<ImageCandidate> {
let chars: Vec<char> = input.chars().collect();
let len = chars.len();
let mut pos = 0;
let mut candidates: Vec<ImageCandidate> = Vec::new();
loop {
while pos < len && is_comma_or_space(chars[pos]) {
pos += 1;
}
if pos >= len {
return candidates;
}
let url_start = pos;
while pos < len && !is_space(chars[pos]) {
pos += 1;
}
let url_token: String = chars[url_start..pos].iter().collect();
if url_token.ends_with(',') {
let url = url_token.trim_end_matches(',').to_string();
if let Some(c) = parse_descriptors(url, &[]) {
candidates.push(c);
}
} else if let Some(c) = parse_descriptors(url_token, &tokenize(&chars, &mut pos, len)) {
candidates.push(c);
}
}
}
#[derive(Clone, Copy)]
enum State {
InDescriptor,
InParens,
AfterDescriptor,
}
fn tokenize(chars: &[char], pos: &mut usize, len: usize) -> Vec<String> {
while *pos < len && is_space(chars[*pos]) {
*pos += 1;
}
let mut descriptors: Vec<String> = Vec::new();
let mut current = String::new();
let mut state = State::InDescriptor;
loop {
let c = chars.get(*pos).copied(); match state {
State::InDescriptor => match c {
Some(ch) if is_space(ch) => {
if !current.is_empty() {
descriptors.push(core::mem::take(&mut current));
state = State::AfterDescriptor;
}
}
Some(',') => {
*pos += 1;
if !current.is_empty() {
descriptors.push(current);
}
return descriptors;
}
Some('(') => {
current.push('(');
state = State::InParens;
}
None => {
if !current.is_empty() {
descriptors.push(current);
}
return descriptors;
}
Some(ch) => current.push(ch),
},
State::InParens => match c {
Some(')') => {
current.push(')');
state = State::InDescriptor;
}
None => {
descriptors.push(current);
return descriptors;
}
Some(ch) => current.push(ch),
},
State::AfterDescriptor => match c {
Some(ch) if is_space(ch) => {} None => return descriptors,
Some(_) => {
state = State::InDescriptor;
*pos -= 1;
}
},
}
*pos += 1;
}
}
fn parse_descriptors(url: String, descriptors: &[String]) -> Option<ImageCandidate> {
let mut error = false;
let mut width: Option<u64> = None;
let mut density: Option<f64> = None;
let mut height: Option<u64> = None;
let truthy_w = |w: Option<u64>| w.is_some();
let truthy_h = |h: Option<u64>| h.is_some();
let truthy_d = |d: Option<f64>| d.is_some_and(|v| v != 0.0);
for desc in descriptors {
let last = desc.chars().next_back().unwrap_or('\0');
let value: String = {
let mut it = desc.chars();
it.next_back();
it.collect()
};
if last == 'w' && is_non_negative_integer(&value) {
if truthy_w(width) || truthy_d(density) {
error = true;
}
match value.parse::<u64>() {
Ok(0) | Err(_) => error = true,
Ok(n) => width = Some(n),
}
} else if last == 'x' && is_floating_point(&value) {
if truthy_w(width) || truthy_d(density) || truthy_h(height) {
error = true;
}
match value.parse::<f64>() {
Ok(f) if f >= 0.0 => density = Some(f),
_ => error = true,
}
} else if last == 'h' && is_non_negative_integer(&value) {
if truthy_h(height) || truthy_d(density) {
error = true;
}
match value.parse::<u64>() {
Ok(0) | Err(_) => error = true,
Ok(n) => height = Some(n),
}
} else {
error = true;
}
}
if error {
return None;
}
Some(ImageCandidate {
url,
density: if truthy_d(density) { density } else { None },
width: if truthy_w(width) { width } else { None },
height: if truthy_h(height) { height } else { None },
})
}
fn is_space(c: char) -> bool {
matches!(c, ' ' | '\t' | '\n' | '\u{000c}' | '\r')
}
fn is_comma_or_space(c: char) -> bool {
c == ',' || is_space(c)
}
fn is_non_negative_integer(s: &str) -> bool {
!s.is_empty() && s.bytes().all(|b| b.is_ascii_digit())
}
fn is_floating_point(s: &str) -> bool {
let b = s.as_bytes();
let len = b.len();
let mut i = 0;
if i < len && b[i] == b'-' {
i += 1;
}
let int_digits = count_digits(b, &mut i);
if i < len && b[i] == b'.' {
i += 1;
if count_digits(b, &mut i) == 0 {
return false; }
} else if int_digits == 0 {
return false; }
if i < len && (b[i] == b'e' || b[i] == b'E') {
i += 1;
if i < len && (b[i] == b'+' || b[i] == b'-') {
i += 1;
}
if count_digits(b, &mut i) == 0 {
return false; }
}
i == len
}
fn count_digits(b: &[u8], i: &mut usize) -> usize {
let start = *i;
while *i < b.len() && b[*i].is_ascii_digit() {
*i += 1;
}
*i - start
}