wasm-color-string 0.1.0

WebAssembly library for parsing and generating CSS color strings
Documentation
#[macro_use]
extern crate lazy_static;

use regex::{Match, Regex};
use wasm_bindgen::prelude::*;
use wee_alloc::WeeAlloc;

// Use `wee_alloc` as the global allocator.
#[global_allocator]
static ALLOC: WeeAlloc = WeeAlloc::INIT;

#[wasm_bindgen]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Model {
    Rgb,
    Hsl,
    Hwb,
}

#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct Color(pub Model, pub f32, pub f32, pub f32, pub f32);

#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct Rgb {
    pub r: u8,
    pub g: u8,
    pub b: u8,
    pub a: f32,
}

#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct Hsl {
    pub h: f32,
    pub s: f32,
    pub l: f32,
    pub a: f32,
}

#[wasm_bindgen]
#[derive(Debug, PartialEq)]
pub struct Hwb {
    pub h: f32,
    pub w: f32,
    pub b: f32,
    pub a: f32,
}

#[wasm_bindgen]
pub fn get_color(string: &str) -> Option<Color> {
    let prefix = &string[0..3];
    match prefix {
        "hsl" => {
            println!("HSL");
            let hsl = get_hsl(string)?;
            Some(Color(Model::Hsl, hsl.h, hsl.s, hsl.l, hsl.a as f32))
        }
        "hwb" => {
            println!("HWB");
            let hwb = get_hwb(string)?;
            Some(Color(Model::Hwb, hwb.h, hwb.w, hwb.b, hwb.a as f32))
        }
        _ => {
            let rgb = get_rgb(string)?;
            Some(Color(
                Model::Rgb,
                rgb.r as f32,
                rgb.g as f32,
                rgb.b as f32,
                rgb.a as f32,
            ))
        }
    }
}

#[wasm_bindgen]
pub fn get_rgb(string: &str) -> Option<Rgb> {
    lazy_static! {
        static ref RE_HEX_SHORT: Regex = Regex::new(r"^#([0-9A-Fa-f]{3})([0-9A-Fa-f]{1})?$").unwrap();
        static ref RE_HEX_LONG: Regex = Regex::new(r"^#([0-9A-Fa-f]{6})([0-9A-Fa-f]{2})?$").unwrap();
        static ref RE_RGB: Regex = Regex::new(r"^rgba?\(\s*([+-]?\d+)\s*,?\s*([+-]?\d+)\s*,?\s*([+-]?\d+)\s*(?:[,/]\s*([+-]?\d*(?:\.\d+)?%?)\s*)?\)$").unwrap();
        static ref RE_RGB_PERCENT: Regex = Regex::new(r"^rgba?\(\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,/]\s*([+-]?\d*(?:\.\d+)?%?)\s*)?\)$").unwrap();
    }
    if RE_HEX_SHORT.is_match(string) {
        let groups = RE_HEX_SHORT.captures(string)?;
        let rgb = groups.get(1)?.as_str();
        let r: u8 = parse_u8_hex(&rgb[0..1], 0, 15)?;
        let g: u8 = parse_u8_hex(&rgb[1..2], 0, 15)?;
        let b: u8 = parse_u8_hex(&rgb[2..3], 0, 15)?;
        let a: f32 = match groups.get(2) {
            Some(value) => {
                let alpha = parse_u8_hex(value.as_str(), 0, 15)?;
                ((16 * alpha) + alpha) as f32 / 255.0
            }
            None => 1.0,
        };
        return Some(Rgb {
            r: (16 * r) + r,
            g: (16 * g) + g,
            b: (16 * b) + b,
            a,
        });
    } else if RE_HEX_LONG.is_match(string) {
        let groups = RE_HEX_LONG.captures(string)?;
        let rgb = groups.get(1)?.as_str();
        let r: u8 = parse_u8_hex(&rgb[0..2], 0, 255)?;
        let g: u8 = parse_u8_hex(&rgb[2..4], 0, 255)?;
        let b: u8 = parse_u8_hex(&rgb[4..6], 0, 255)?;
        let a: f32 = match groups.get(2) {
            Some(value) => parse_u8_hex(value.as_str(), 0, 255)? as f32 / 255.0,
            None => 1.0,
        };
        return Some(Rgb { r, g, b, a });
    } else if RE_RGB.is_match(string) {
        let groups = RE_RGB.captures(string)?;
        let r: u8 = parse_u8(groups.get(1)?.as_str(), 0, 255)?;
        let g: u8 = parse_u8(groups.get(2)?.as_str(), 0, 255)?;
        let b: u8 = parse_u8(groups.get(3)?.as_str(), 0, 255)?;
        let a: f32 = get_alpha(groups.get(4))?;
        return Some(Rgb { r, g, b, a });
    } else if RE_RGB_PERCENT.is_match(string) {
        let groups = RE_RGB_PERCENT.captures(string)?;
        let r: u8 = (get_percentage(groups.get(1)?.as_str())? * 2.55) as u8;
        let g: u8 = (get_percentage(groups.get(1)?.as_str())? * 2.55) as u8;
        let b: u8 = (get_percentage(groups.get(1)?.as_str())? * 2.55) as u8;
        let a: f32 = get_alpha(groups.get(4))?;
        return Some(Rgb { r, g, b, a });
    } else {
        return None;
    }
}

#[wasm_bindgen]
pub fn get_hsl(string: &str) -> Option<Hsl> {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"^hsla?\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,/]\s*([+-]?\d*(?:\.\d+)?%?)\s*)?\)$").unwrap();
    }
    let groups = RE.captures(string)?;
    let h: f32 = groups
        .get(1)?
        .as_str()
        .parse::<f32>()
        .ok()?
        .rem_euclid(360.0);
    let s: f32 = get_percentage(groups.get(2)?.as_str())?;
    let l: f32 = get_percentage(groups.get(3)?.as_str())?;
    let a: f32 = get_alpha(groups.get(4))?;
    Some(Hsl { h, s, l, a })
}

#[wasm_bindgen]
pub fn get_hwb(string: &str) -> Option<Hwb> {
    lazy_static! {
        static ref RE: Regex = Regex::new(r"^hwb\(\s*([+-]?\d{0,3}(?:\.\d+)?)(?:deg)?\s*,?\s*([+-]?[\d\.]+)%\s*,?\s*([+-]?[\d\.]+)%\s*(?:[,/]\s*([+-]?\d*(?:\.\d+)?%?)\s*)?\)$").unwrap();
    }
    let groups = RE.captures(string)?;
    let h: f32 = groups
        .get(1)?
        .as_str()
        .parse::<f32>()
        .ok()?
        .rem_euclid(360.0);
    let w: f32 = get_percentage(groups.get(2)?.as_str())?;
    let b: f32 = get_percentage(groups.get(3)?.as_str())?;
    let a: f32 = get_alpha(groups.get(4))?;
    Some(Hwb { h, w, b, a })
}

fn get_alpha(option: Option<Match>) -> Option<f32> {
    match option {
        Some(value) => parse_alpha(value.as_str()),
        None => Some(1.0),
    }
}

fn parse_alpha(string: &str) -> Option<f32> {
    let len = string.len();
    let last_char = string.chars().last().unwrap();
    if last_char == '%' {
        Some(get_percentage(&string[0..len - 1])? / 100.0)
    } else {
        parse_f32(&string, 0.0, 1.0)
    }
}

fn get_percentage(string: &str) -> Option<f32> {
    parse_f32(&string, 0.0, 100.0)
}

fn parse_f32(string: &str, min: f32, max: f32) -> Option<f32> {
    Some(string.parse::<f32>().ok()?.max(min).min(max))
}

fn parse_u8(string: &str, min: u8, max: u8) -> Option<u8> {
    Some(string.parse::<u8>().ok()?.max(min).min(max))
}

fn parse_u8_hex(string: &str, min: u8, max: u8) -> Option<u8> {
    Some(u8::from_str_radix(string, 16).ok()?.max(min).min(max))
}