use crate::Color;
use crate::ColorValue;
use crate::ErrorKind;
use crate::Solid;
use crate::gradient::Gradient;
use crate::gradient::GradientCoordinates;
use crate::gradient::is_valid_direction;
use crate::utils::get_accent;
use crate::utils::strip_string;
#[cfg(any(feature = "theme", feature = "theme_yml"))]
use crate::{Theme, utils::PathClean};
use named_colors::ACCENT_TRANSPARENT_PATTERN;
use named_colors::HEX_PATTERN;
use named_colors::HSLA_PATTERN;
use named_colors::RGBA_PATTERN;
#[cfg(feature = "named-colors")]
pub use named_colors::{NAMED_COLOR_PATTERN, NAMED_COLORS};
use regex::Regex;
#[cfg(any(feature = "theme", feature = "theme_yml"))]
use std::{
env::current_dir,
fs::{metadata, read_to_string},
path::PathBuf,
sync::{LazyLock, RwLock},
time::SystemTime,
};
pub use crate::Error;
pub use crate::Result;
mod named_colors;
#[cfg(any(feature = "theme", feature = "theme_yml"))]
type ThemeCache = (String, Theme, SystemTime);
#[cfg(any(feature = "theme", feature = "theme_yml"))]
static THEME_CACHE: LazyLock<RwLock<Option<ThemeCache>>> = LazyLock::new(|| RwLock::new(None));
pub fn parse_solid(s: &str, file_path: Option<&str>) -> Result<Solid> {
let s = s.trim().to_ascii_lowercase();
match s.as_str() {
"transparent" => return Ok(Solid::new(0.0, 0.0, 0.0, 0.0)),
"accent" => return get_accent(true),
"accent_inactive" => return get_accent(false),
_ => {}
}
#[cfg(any(feature = "theme", feature = "theme_yml"))]
if let Some(file_path) = file_path {
if let Some(color) = parse_custom_theme(file_path)?.get_color(&s) {
return parse_solid(color.as_str(), None);
}
}
#[cfg(feature = "named-colors")]
if let Some([r, g, b]) = NAMED_COLORS.get(&*s) {
return Ok(Solid::from_rgba8(*r, *g, *b, 255));
}
if let Some(s) = s.strip_prefix('#') {
return parse_hex(s);
}
let original_s = s.clone();
if let (Some(i), Some(s)) = (s.find('('), s.strip_suffix(')')) {
let fname = &s[..i].trim_end();
let s = &s[i + 1..].replace([',', '/'], " ");
let params = s.split_whitespace().collect::<Vec<&str>>();
return match *fname {
"rgb" | "rgba" => parse_rgb_or_rgba(params, original_s.as_str()),
"hsl" | "hsla" => parse_hsl_or_hsla(params, original_s.as_str()),
_ => Err(Error::new(ErrorKind::InvalidFunction, s)),
};
}
if let Ok(c) = parse_hex(&s) {
return Ok(c);
}
Err(Error::new(ErrorKind::InvalidUnknown, s))
}
pub fn parse_gradient(s: &str, file_path: Option<&str>) -> Result<Gradient> {
if !s.starts_with("gradient(") {
return Err(Error::new(ErrorKind::InvalidGradient, s));
}
let binding = strip_string(s.to_string(), &["gradient("], ')');
let base_pattern = format!(
r"(?i){}|{}|{}|{}",
HEX_PATTERN, RGBA_PATTERN, HSLA_PATTERN, ACCENT_TRANSPARENT_PATTERN
);
let mut color_regex: Regex = Regex::new(base_pattern.as_str()).unwrap();
#[cfg(any(feature = "theme", feature = "theme_yml"))]
{
if let Some(file_path) = file_path {
if let Ok(theme_data) = parse_custom_theme(file_path) {
let theme_keys: Vec<_> = theme_data.colors();
let escaped_keys: Vec<String> = theme_keys
.iter()
.map(|key| key.replace('.', r"\."))
.collect();
let theme_pattern_base = escaped_keys.join("|");
let theme_pattern = format!(r"\b(?:{})\b", theme_pattern_base);
#[cfg(feature = "named-colors")]
{
color_regex = Regex::new(
format!(
r"(?i){}|{}|{}",
base_pattern, theme_pattern, NAMED_COLOR_PATTERN
)
.as_str(),
)
.unwrap();
}
#[cfg(not(feature = "named-colors"))]
{
color_regex =
Regex::new(format!(r"(?i){}|{}", base_pattern, theme_pattern).as_str())
.unwrap();
}
}
}
}
#[cfg(feature = "named-colors")]
#[cfg(not(any(feature = "theme", feature = "theme_yml")))]
{
color_regex =
Regex::new(format!(r"(?i){}|{}", base_pattern, NAMED_COLOR_PATTERN).as_str()).unwrap();
}
let color_matches = color_regex
.captures_iter(&binding)
.filter_map(|cap| cap.get(0).map(|m| m.as_str()))
.collect::<Vec<&str>>();
let remaining_input = s
[s.rfind(color_matches.last().unwrap()).unwrap() + color_matches.last().unwrap().len()..]
.trim_start();
let remaining_input_arr = remaining_input
.split(',')
.filter_map(|s| {
let trimmed = s.trim();
(!trimmed.is_empty()).then_some(trimmed)
})
.collect::<Vec<&str>>();
let direction = remaining_input_arr
.iter()
.find(|&&input| is_valid_direction(input))
.map(|&s| s.to_string())
.unwrap_or_else(|| "to right".to_string());
let colors = color_matches
.iter()
.filter_map(|&color| parse_solid(color, file_path).ok()) .collect::<Vec<Solid>>();
let direction = GradientCoordinates::try_from(direction.as_str())?;
Ok(Gradient { direction, colors })
}
pub fn parse(s: &str, file_path: Option<&str>) -> Result<Color> {
if s.starts_with("gradient(") {
parse_gradient(s, file_path).map(|res| Color(ColorValue::Gradient(res)))
} else {
parse_solid(s, file_path).map(|res| Color(ColorValue::Solid(res)))
}
}
fn parse_hex(s: &str) -> Result<Solid> {
if !matches!(s.len(), 3 | 4 | 6 | 8) || !s[1..].chars().all(|c| c.is_ascii_hexdigit()) {
return Err(Error::new(ErrorKind::InvalidHex, s));
}
let n = s.len();
let parse_digit = |digit: &str| -> Result<u8> {
u8::from_str_radix(digit, 16)
.map(|n| if digit.len() == 1 { (n << 4) | n } else { n })
.map_err(|_| Error::new(ErrorKind::InvalidHex, s))
};
if n == 3 || n == 4 {
let r = parse_digit(&s[0..1])?;
let g = parse_digit(&s[1..2])?;
let b = parse_digit(&s[2..3])?;
let a = if n == 4 { parse_digit(&s[3..4])? } else { 255 };
Ok(Solid::from_rgba8(r, g, b, a))
} else if n == 6 || n == 8 {
let r = parse_digit(&s[0..2])?;
let g = parse_digit(&s[2..4])?;
let b = parse_digit(&s[4..6])?;
let a = if n == 8 { parse_digit(&s[6..8])? } else { 255 };
Ok(Solid::from_rgba8(r, g, b, a))
} else {
Err(Error::new(ErrorKind::InvalidHex, s))
}
}
fn parse_rgb_or_rgba(params: Vec<&str>, original_s: &str) -> Result<Solid> {
if params.len() != 3 && params.len() != 4 {
return Err(Error::new(ErrorKind::InvalidRgb, original_s));
}
let r = parse_percent_or_255(params[0]);
let g = parse_percent_or_255(params[1]);
let b = parse_percent_or_255(params[2]);
let a = if params.len() == 4 {
parse_percent_or_float(params[3])
} else {
Some((1.0, true))
};
if let (Some((r, r_fmt)), Some((g, g_fmt)), Some((b, b_fmt)), Some((a, _))) = (r, g, b, a) {
if r_fmt == g_fmt && g_fmt == b_fmt {
return Ok(Solid::new(
r.clamp(0.0, 1.0),
g.clamp(0.0, 1.0),
b.clamp(0.0, 1.0),
a.clamp(0.0, 1.0),
));
}
}
Err(Error::new(ErrorKind::InvalidRgb, original_s))
}
fn parse_hsl_or_hsla(params: Vec<&str>, original_s: &str) -> Result<Solid> {
if params.len() != 3 && params.len() != 4 {
return Err(Error::new(ErrorKind::InvalidHsl, original_s));
}
let h = parse_angle(params[0]);
let s = parse_percent_or_float(params[1]);
let l = parse_percent_or_float(params[2]);
let a = if params.len() == 4 {
parse_percent_or_float(params[3])
} else {
Some((1.0, true))
};
if let (Some(h), Some((s, s_fmt)), Some((l, l_fmt)), Some((a, _))) = (h, s, l, a) {
if s_fmt == l_fmt {
return Ok(Solid::from_normalized_hsla(h, s, l, a));
}
}
Err(Error::new(ErrorKind::InvalidHsl, original_s))
}
fn parse_percent_or_float(s: &str) -> Option<(f32, bool)> {
match s.strip_suffix('%') {
Some(num) => num.parse().ok().map(|t: f32| (t / 100.0, true)),
None => s.parse().ok().map(|t| (t, false)),
}
}
fn parse_percent_or_255(s: &str) -> Option<(f32, bool)> {
match s.strip_suffix('%') {
Some(num) => num.parse().ok().map(|t: f32| (t / 100.0, true)),
None => s.parse().ok().map(|t: f32| (t / 255.0, false)),
}
}
fn parse_angle(s: &str) -> Option<f32> {
if let Some(s) = s.strip_suffix("deg") {
return s.parse().ok();
}
if let Some(s) = s.strip_suffix("grad") {
return s.parse::<f32>().ok().map(|t| t * 360.0 / 400.0);
}
if let Some(s) = s.strip_suffix("rad") {
return s.parse::<f32>().ok().map(|t| t.to_degrees());
}
if let Some(s) = s.strip_suffix("turn") {
return s.parse::<f32>().ok().map(|t| t * 360.0);
}
s.parse().ok()
}
#[cfg(any(feature = "theme", feature = "theme_yml"))]
fn parse_custom_theme(file_path: &str) -> Result<Theme> {
let mut full_path = PathBuf::from(file_path).clean();
if full_path.is_relative() {
let cwd =
current_dir().map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
full_path = cwd.join(full_path).clean(); }
let current_metadata = metadata(&full_path)
.map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
let current_modified = current_metadata
.modified()
.map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
if let Some((cached_path, theme_data, cached_time)) = THEME_CACHE.read().unwrap().as_ref() {
if cached_path == &full_path.to_string_lossy().into_owned()
&& current_modified != *cached_time
{
return Ok(theme_data.clone());
}
}
reload_and_cache_theme(full_path)
}
#[cfg(any(feature = "theme", feature = "theme_yml"))]
fn reload_and_cache_theme(file_path: PathBuf) -> Result<Theme> {
let path = file_path.as_path();
let contents = read_to_string(path)
.map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
let theme = Theme::parse_theme(contents.as_str()).unwrap();
let current_metadata = std::fs::metadata(path)
.map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
let current_modified = current_metadata
.modified()
.map_err(|e| Error::new(ErrorKind::InvalidUnknown, format!("{:?}", e)))?;
let mut cache = THEME_CACHE.write().unwrap();
let path_string = path.to_string_lossy().into_owned();
*cache = Some((path_string, theme.clone(), current_modified));
Ok(theme)
}