diff --git a/Cargo.lock b/Cargo.lock index b0b29f7..a8a0d35 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -73,6 +73,7 @@ dependencies = [ "lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)", "num-bigint 0.2.6 (registry+https://github.com/rust-lang/crates.io-index)", "num-rational 0.2.3 (registry+https://github.com/rust-lang/crates.io-index)", + "num-traits 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile 3.1.0 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/Cargo.toml b/Cargo.toml index 1c869db..73603d4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ clap = { version = "2.33.0", optional = true } lazy_static = "1.4.0" num-rational = "0.2.3" num-bigint = "0.2.6" +num-traits = "0.2.11" [features] default = ["commandline"] diff --git a/src/builtin/color.rs b/src/builtin/color.rs index 1d845d4..06187b8 100644 --- a/src/builtin/color.rs +++ b/src/builtin/color.rs @@ -2,7 +2,6 @@ use std::collections::BTreeMap; use std::convert::TryInto; use num_bigint::BigInt; -use num_rational::BigRational; use super::Builtin; use crate::color::Color; @@ -17,7 +16,23 @@ pub(crate) fn register(f: &mut BTreeMap) { let red: u16 = arg!(args, 0, "red").clone().try_into().unwrap(); let green: u16 = arg!(args, 1, "green").clone().try_into().unwrap(); let blue: u16 = arg!(args, 2, "blue").clone().try_into().unwrap(); - Some(Value::Color(Color::from_values(red, green, blue, 1))) + Some(Value::Color(Color::from_values(red, green, blue, Number::from(1)))) + } else { + todo!("channels variable in `rgb`") + } + }); + decl!(f "rgba", |args, _| { + let channels = args.get("channels").unwrap_or(&Value::Null); + if channels.is_null() { + let red: u16 = arg!(args, 0, "red").clone().try_into().unwrap(); + let green: u16 = arg!(args, 1, "green").clone().try_into().unwrap(); + let blue: u16 = arg!(args, 2, "blue").clone().try_into().unwrap(); + let alpha = match arg!(args, 3, "alpha").clone().eval() { + Value::Dimension(n, Unit::None) => n, + Value::Dimension(n, Unit::Percent) => n / Number::from(100), + _ => todo!("expected either unitless or % number for alpha"), + }; + Some(Value::Color(Color::from_values(red, green, blue, alpha))) } else { todo!("channels variable in `rgb`") } @@ -42,14 +57,14 @@ pub(crate) fn register(f: &mut BTreeMap) { }); decl!(f "opacity", |args, _| { match arg!(args, 0, "color") { - Value::Color(c) => Some(Value::Dimension(Number::new(BigRational::new(BigInt::from(c.alpha()), BigInt::from(255))), Unit::None)), + Value::Color(c) => Some(Value::Dimension(c.alpha() / Number::from(255), Unit::None)), Value::Dimension(num, unit) => Some(Value::Ident(format!("opacity({}{})", num , unit), QuoteKind::None)), _ => todo!("non-color given to builtin function `opacity()`") } }); decl!(f "alpha", |args, _| { match arg!(args, 0, "color") { - Value::Color(c) => Some(Value::Dimension(Number::new(BigRational::new(BigInt::from(c.alpha()), BigInt::from(255))), Unit::None)), + Value::Color(c) => Some(Value::Dimension(c.alpha() / Number::from(255), Unit::None)), _ => todo!("non-color given to builtin function `alpha()`") } }); diff --git a/src/color/mod.rs b/src/color/mod.rs index 0aecd2f..5f58328 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; pub(crate) use name::ColorName; +use crate::value::{Number, Value}; mod name; @@ -9,17 +10,17 @@ pub(crate) struct Color { red: u16, green: u16, blue: u16, - alpha: u16, + alpha: Number, repr: String, } impl Color { - pub const fn new(red: u16, green: u16, blue: u16, alpha: u16, repr: String) -> Self { + pub fn new(red: u16, green: u16, blue: u16, alpha: u16, repr: String) -> Self { Color { red, green, blue, - alpha, + alpha: alpha.into(), repr, } } @@ -36,15 +37,15 @@ impl Color { self.green } - pub const fn alpha(&self) -> u16 { - self.alpha + pub fn alpha(&self) -> Number { + self.alpha.clone() } - pub fn from_values(red: u16, green: u16, blue: u16, alpha: u16) -> Self { - let repr = if alpha >= 1 { + pub fn from_values(red: u16, green: u16, blue: u16, alpha: Number) -> Self { + let repr = if alpha >= Number::from(1) { format!("#{:0>2x}{:0>2x}{:0>2x}", red, green, blue) } else { - format!("#{:0>2x}{:0>2x}{:0>2x}{:0>2x}", red, green, blue, alpha) + format!("rgba({}, {}, {}, {})", red, green, blue, alpha) }; Color { red, diff --git a/src/lexer.rs b/src/lexer.rs index 6ecbf31..17c05d1 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -38,10 +38,24 @@ impl<'a> Iterator for Lexer<'a> { 'a'..='z' | 'A'..='Z' | '-' | '_' => self.lex_ident(), '@' => self.lex_at_rule(), '0'..='9' => self.lex_num(), + '.' => { + self.buf.next(); + self.pos.next_char(); + match self.buf.peek().unwrap() { + '0'..='9' => match self.lex_num() { + TokenKind::Number(n) => { + let mut s = String::from("0."); + s.push_str(&n); + TokenKind::Number(s) + } + _ => unsafe { std::hint::unreachable_unchecked() } + } + _ => TokenKind::Symbol(Symbol::Period) + } + } '$' => self.lex_variable(), ':' => symbol!(self, Colon), ',' => symbol!(self, Comma), - '.' => symbol!(self, Period), ';' => symbol!(self, SemiColon), '(' => symbol!(self, OpenParen), ')' => symbol!(self, CloseParen), diff --git a/src/value/number.rs b/src/value/number.rs index 5a497aa..91c2f1a 100644 --- a/src/value/number.rs +++ b/src/value/number.rs @@ -1,6 +1,6 @@ use std::convert::From; use std::fmt::{self, Display, Write}; -use std::ops::{Add, Sub}; +use std::ops::{Add, Sub, Mul, Div}; use num_bigint::BigInt; use num_rational::BigRational; @@ -30,6 +30,14 @@ impl From for Number { } } +impl From for Number { + fn from(b: u16) -> Self { + Number { + val: BigRational::from_integer(BigInt::from(b)), + } + } +} + impl Display for Number { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.val.to_integer())?; @@ -72,3 +80,23 @@ impl Sub for Number { } } } + +impl Mul for Number { + type Output = Self; + + fn mul(self, other: Self) -> Self { + Number { + val: self.val * other.val, + } + } +} + +impl Div for Number { + type Output = Self; + + fn div(self, other: Self) -> Self { + Number { + val: self.val / other.val, + } + } +} diff --git a/src/value/parse.rs b/src/value/parse.rs index 6ee8254..05d3f41 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -1,6 +1,10 @@ use std::convert::TryFrom; use std::iter::{Iterator, Peekable}; +use num_rational::BigRational; +use num_bigint::BigInt; +use num_traits::pow; + use crate::args::eat_call_args; use crate::builtin::GLOBAL_FUNCTIONS; use crate::color::Color; @@ -163,8 +167,29 @@ impl Value { } else { Unit::None }; + let n = match val.parse::() { + // the number is an integer! + Ok(v) => v, + // the number is floating point + Err(_) => { + let mut num = String::new(); + let mut chars = val.chars().into_iter(); + let mut num_dec = 0; + while let Some(c) = chars.next() { + if c == '.' { + break; + } + num.push(c); + } + for c in chars { + num_dec += 1; + num.push(c); + } + BigRational::new(num.parse().unwrap(), pow(BigInt::from(10), num_dec)) + } + }; Some(Value::Dimension( - Number::new(val.parse().expect("error parsing integer")), + Number::new(n), unit, )) } diff --git a/tests/color.rs b/tests/color.rs index 02397a7..5c1a5b3 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -64,3 +64,18 @@ test!( opacity_function_number_unit, "a {\n color: opacity(1px);\n}\n" ); +test!( + rgba_opacity_over_1, + "a {\n color: rgba(1, 2, 3, 3);\n}\n", + "a {\n color: #010203;\n}\n" +); +test!( + rgba_opacity_decimal, + "a {\n color: rgba(1, 2, 3, .6);\n}\n", + "a {\n color: rgba(1, 2, 3, 0.6);\n}\n" +); +test!( + rgba_opacity_percent, + "a {\n color: rgba(1, 2, 3, 50%);\n}\n", + "a {\n color: rgba(1, 2, 3, 0.5);\n}\n" +); \ No newline at end of file