Skip to content

Commit a4db5d3

Browse files
committed
add hsl css parsing
1 parent c097afd commit a4db5d3

File tree

1 file changed

+126
-15
lines changed

1 file changed

+126
-15
lines changed

src/color/parse.rs

Lines changed: 126 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,35 @@ fn parse_alpha_component(num: &str) -> Result<u8, ColorParseError> {
177177
}
178178
}
179179

180-
/// Parse a hex color from a string.
181-
///
182-
/// The allowed formats are:
183-
/// * #RGB
184-
/// * #RGBA
185-
/// * #RRGGBB
186-
/// * #RRGGBBAA
180+
fn parse_hue_component(num: &str) -> Result<ColorFloat, ColorParseError> {
181+
use ColorParseError::InvalidToken;
182+
183+
if num.ends_with('%') {
184+
return Err(InvalidToken);
185+
}
186+
187+
num.parse::<ColorFloat>().map_err(|_| InvalidToken)
188+
}
189+
190+
fn parse_hsl_percentage(num: &str) -> Result<ColorFloat, ColorParseError> {
191+
use ColorParseError::{InvalidToken, OutOfRange};
192+
193+
let core = num.strip_suffix('%').ok_or(InvalidToken)?;
194+
let v: ColorFloat = core.parse().map_err(|_| InvalidToken)?;
195+
if !(0.0..=100.0).contains(&v) {
196+
return Err(OutOfRange);
197+
}
198+
Ok(v)
199+
}
200+
201+
// Parse a hex color from a string.
202+
//
203+
// The allowed formats are:
204+
// * #RGB
205+
// * #RGBA
206+
// * #RRGGBB
207+
// * #RRGGBBAA
208+
187209
fn parse_hex(hex: &str) -> Result<Color, ColorParseError> {
188210
use ColorParseError::*;
189211

@@ -253,13 +275,14 @@ fn parse_hex(hex: &str) -> Result<Color, ColorParseError> {
253275
Ok(Color::from_rgba([r, g, b, a]))
254276
}
255277

256-
/// Parse a CSS rgb function.
257-
///
258-
/// The allowed styles are:
259-
/// rgb(r,g,b)
260-
/// rgb(r g b)
261-
/// rgb(r% g% b%)
262-
/// rgb(r g b / a)
278+
// Parse a CSS rgb function.
279+
//
280+
// The allowed styles are:
281+
// rgb(r,g,b)
282+
// rgb(r g b)
283+
// rgb(r% g% b%)
284+
// rgb(r g b / a)
285+
263286
fn parse_css_rgb(args: &str) -> Result<Color, ColorParseError> {
264287
use ColorParseError::*;
265288

@@ -342,6 +365,86 @@ fn parse_css_rgb(args: &str) -> Result<Color, ColorParseError> {
342365
Ok(Color::from_rgba([r, g, b, a]))
343366
}
344367

368+
// Parse a CSS hsl/hsla function (modern space or legacy comma syntax).
369+
fn parse_css_hsl(args: &str) -> Result<Color, ColorParseError> {
370+
use ColorParseError::*;
371+
372+
let tokens = tokenize(args)?;
373+
if tokens.is_empty() {
374+
return Err(InvalidFunc);
375+
}
376+
377+
let has_comma = tokens.iter().any(|t| matches!(t, CssColorToken::Comma));
378+
379+
let mut it = tokens.iter().peekable();
380+
let mut comps: Vec<&str> = Vec::new();
381+
let mut alpha: Option<&str> = None;
382+
383+
if has_comma {
384+
// legacy hsl(h, s%, l%) or with trailing alpha
385+
loop {
386+
let num = match it.next() {
387+
Some(CssColorToken::Number(s)) => *s,
388+
Some(_) => return Err(InvalidFunc),
389+
None => break,
390+
};
391+
comps.push(num);
392+
393+
match it.next() {
394+
Some(CssColorToken::Comma) => continue,
395+
Some(CssColorToken::Slash) => {
396+
let next = match it.next() {
397+
Some(CssColorToken::Number(s)) => *s,
398+
_ => return Err(InvalidToken),
399+
};
400+
alpha = Some(next);
401+
if it.next().is_some() {
402+
return Err(InvalidFunc);
403+
}
404+
break;
405+
}
406+
None => break,
407+
Some(_) => return Err(InvalidToken),
408+
}
409+
}
410+
} else {
411+
// modern: hsl(h s% l% / a?)
412+
while let Some(token) = it.next() {
413+
match token {
414+
CssColorToken::Number(s) => {
415+
if alpha.is_some() {
416+
return Err(InvalidFunc); // numbers after alpha not allowed
417+
}
418+
comps.push(*s);
419+
}
420+
CssColorToken::Slash => {
421+
if alpha.is_some() {
422+
return Err(InvalidFunc); // double slash
423+
}
424+
let next = match it.next() {
425+
Some(CssColorToken::Number(s)) => *s,
426+
_ => return Err(InvalidToken),
427+
};
428+
alpha = Some(next);
429+
}
430+
CssColorToken::Comma => return Err(InvalidToken), // commas not allowed in this form
431+
}
432+
}
433+
}
434+
435+
if comps.len() != 3 {
436+
return Err(InvalidFunc);
437+
}
438+
439+
let h = parse_hue_component(comps[0])?;
440+
let s = parse_hsl_percentage(comps[1])?;
441+
let l = parse_hsl_percentage(comps[2])?;
442+
let a = alpha.map(parse_alpha_component).transpose()?.unwrap_or(255);
443+
444+
let color = Color::from_hsl([h, s, l]);
445+
Ok(color.with_alpha(a))
446+
}
447+
345448
pub fn parse_color(mut s: &str) -> Result<Color, ColorParseError> {
346449
use ColorParseError::*;
347450

@@ -365,13 +468,21 @@ pub fn parse_color(mut s: &str) -> Result<Color, ColorParseError> {
365468
if let Some(args) = lower.strip_prefix("rgb(").and_then(|x| x.strip_suffix(')')) {
366469
return parse_css_rgb(args);
367470
}
368-
// rgba() in CSS is just rgb() with the same arg grammar in modern CSS
369471
if let Some(args) = lower
370472
.strip_prefix("rgba(")
371473
.and_then(|x| x.strip_suffix(')'))
372474
{
373475
return parse_css_rgb(args);
374476
}
477+
if let Some(args) = lower.strip_prefix("hsl(").and_then(|x| x.strip_suffix(')')) {
478+
return parse_css_hsl(args);
479+
}
480+
if let Some(args) = lower
481+
.strip_prefix("hsla(")
482+
.and_then(|x| x.strip_suffix(')'))
483+
{
484+
return parse_css_hsl(args);
485+
}
375486

376487
// Hex-like
377488
if let Some(rest) = s.strip_prefix('#') {

0 commit comments

Comments
 (0)