diff --git a/CHANGELOG.md b/CHANGELOG.md index d861cea..36aa1d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# TBD + + - **implement `@use` and the module system** + - support the filter syntax for function arguments, e.g. `alpha(opacity=1)` + - disallow certain at-rules in functions, resolving several panics + - allow vendor-prefixed special CSS functions, e.g. `-webkit-calc(...)` + - allow decimal percent selectors inside `@keyframes` + - allow vendor-prefixed `@keyframes` + - resolve parsing bug for maps involving silent comments + - allow escaped `!` in selectors + - allow multiline comments in functions + - resolve several panics on malformed input when parsing bracketed lists + # 0.10.0 - bugfixes for `@media` query regressions diff --git a/README.md b/README.md index cf3ed26..531cf72 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The large features remaining are ``` indented syntax css imports -@use, @forward, and the module system +@forward compressed output ``` @@ -77,6 +77,13 @@ cargo b --release These numbers come from a default run of the Sass specification as shown above. +``` +2020-08-07 +PASSING: 3375 +FAILING: 1718 +TOTAL: 5093 +``` + ``` 2020-07-24 PASSING: 2935 diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs index a85bcf8..b89d2f7 100644 --- a/src/atrule/mixin.rs +++ b/src/atrule/mixin.rs @@ -1,21 +1,67 @@ -use crate::{args::FuncArgs, Token}; +use std::fmt; + +use crate::{ + args::{CallArgs, FuncArgs}, + error::SassResult, + parse::{Parser, Stmt}, + Token, +}; + +pub(crate) type BuiltinMixin = fn(CallArgs, &mut Parser<'_>) -> SassResult>; + +#[derive(Clone)] +pub(crate) enum Mixin { + UserDefined(UserDefinedMixin), + Builtin(BuiltinMixin), +} + +impl fmt::Debug for Mixin { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UserDefined(u) => f + .debug_struct("UserDefinedMixin") + .field("args", &u.args) + .field("body", &u.body) + .field("accepts_content_block", &u.accepts_content_block) + .field("declared_at_root", &u.declared_at_root) + .finish(), + Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(), + } + } +} + +impl Mixin { + pub fn new_user_defined( + args: FuncArgs, + body: Vec, + accepts_content_block: bool, + declared_at_root: bool, + ) -> Self { + Mixin::UserDefined(UserDefinedMixin::new( + args, + body, + accepts_content_block, + declared_at_root, + )) + } +} #[derive(Debug, Clone)] -pub(crate) struct Mixin { +pub(crate) struct UserDefinedMixin { pub args: FuncArgs, pub body: Vec, pub accepts_content_block: bool, pub declared_at_root: bool, } -impl Mixin { +impl UserDefinedMixin { pub fn new( args: FuncArgs, body: Vec, accepts_content_block: bool, declared_at_root: bool, ) -> Self { - Mixin { + Self { args, body, accepts_content_block, diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 7c0bda1..80304d0 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -1,6 +1,5 @@ pub(crate) use function::Function; pub(crate) use kind::AtRuleKind; -pub(crate) use mixin::{Content, Mixin}; pub(crate) use supports::SupportsRule; pub(crate) use unknown::UnknownAtRule; @@ -8,6 +7,6 @@ mod function; pub mod keyframes; mod kind; pub mod media; -mod mixin; +pub mod mixin; mod supports; mod unknown; diff --git a/src/builtin/color/hsl.rs b/src/builtin/functions/color/hsl.rs similarity index 81% rename from src/builtin/color/hsl.rs rename to src/builtin/functions/color/hsl.rs index 64c07a6..b97ef24 100644 --- a/src/builtin/color/hsl.rs +++ b/src/builtin/functions/color/hsl.rs @@ -35,7 +35,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } let lightness = match channels.pop() { - Some(Value::Dimension(n, ..)) => n / Number::from(100), + Some(Value::Dimension(Some(n), ..)) => n / Number::from(100), + Some(Value::Dimension(None, ..)) => todo!(), Some(v) => { return Err(( format!("$lightness: {} is not a number.", v.inspect(args.span())?), @@ -47,7 +48,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> }; let saturation = match channels.pop() { - Some(Value::Dimension(n, ..)) => n / Number::from(100), + Some(Value::Dimension(Some(n), ..)) => n / Number::from(100), + Some(Value::Dimension(None, ..)) => todo!(), Some(v) => { return Err(( format!("$saturation: {} is not a number.", v.inspect(args.span())?), @@ -59,7 +61,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> }; let hue = match channels.pop() { - Some(Value::Dimension(n, ..)) => n, + Some(Value::Dimension(Some(n), ..)) => n, + Some(Value::Dimension(None, ..)) => todo!(), Some(v) => { return Err(( format!("$hue: {} is not a number.", v.inspect(args.span())?), @@ -78,7 +81,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> )))) } else { let hue = match args.get_err(0, "hue")? { - Value::Dimension(n, ..) => n, + Value::Dimension(Some(n), ..) => n, + Value::Dimension(None, ..) => todo!(), v if v.is_special_function() => { let saturation = args.get_err(1, "saturation")?; let lightness = args.get_err(2, "lightness")?; @@ -105,7 +109,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } }; let saturation = match args.get_err(1, "saturation")? { - Value::Dimension(n, ..) => n / Number::from(100), + Value::Dimension(Some(n), ..) => n / Number::from(100), + Value::Dimension(None, ..) => todo!(), v if v.is_special_function() => { let lightness = args.get_err(2, "lightness")?; let mut string = format!( @@ -134,7 +139,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } }; let lightness = match args.get_err(2, "lightness")? { - Value::Dimension(n, ..) => n / Number::from(100), + Value::Dimension(Some(n), ..) => n / Number::from(100), + Value::Dimension(None, ..) => todo!(), v if v.is_special_function() => { let mut string = format!( "{}({}, {}, {}", @@ -164,10 +170,11 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> let alpha = match args.default_arg( 3, "alpha", - Value::Dimension(Number::one(), Unit::None, true), + Value::Dimension(Some(Number::one()), Unit::None, true), )? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -205,18 +212,18 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } } -fn hsl(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn hsl(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { inner_hsl("hsl", args, parser) } -fn hsla(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn hsla(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { inner_hsl("hsla", args, parser) } -fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.hue()), Unit::Deg, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -225,10 +232,10 @@ fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.saturation()), Unit::Percent, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -237,10 +244,10 @@ fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult } } -fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.lightness()), Unit::Percent, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -249,7 +256,7 @@ fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, @@ -262,7 +269,8 @@ fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult } }; let degrees = match args.get_err(1, "degrees")? { - Value::Dimension(n, ..) => n, + Value::Dimension(Some(n), ..) => n, + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -290,7 +298,8 @@ fn lighten(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -318,7 +327,8 @@ fn darken(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -346,7 +356,8 @@ fn saturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -360,7 +371,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { }; let color = match args.get_err(0, "color")? { Value::Color(c) => c, - Value::Dimension(n, u, _) => { + Value::Dimension(Some(n), u, _) => { return Ok(Value::String( format!("saturate({}{})", n, u), QuoteKind::None, @@ -390,7 +401,8 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -405,11 +417,11 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult Ok(Value::Color(Box::new(color.desaturate(amount)))) } -fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, - Value::Dimension(n, u, _) => { + Value::Dimension(Some(n), u, _) => { return Ok(Value::String( format!("grayscale({}{})", n, u), QuoteKind::None, @@ -426,7 +438,7 @@ fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::Color(Box::new(color.desaturate(Number::one())))) } -fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, @@ -441,14 +453,15 @@ fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult Ok(Value::Color(Box::new(color.complement()))) } -fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let weight = match args.default_arg( 1, "weight", - Value::Dimension(Number::from(100), Unit::Percent, true), + Value::Dimension(Some(Number::from(100)), Unit::Percent, true), )? { - Value::Dimension(n, u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( @@ -462,9 +475,10 @@ fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { }; match args.get_err(0, "color")? { Value::Color(c) => Ok(Value::Color(Box::new(c.invert(weight)))), - Value::Dimension(n, Unit::Percent, _) => { + Value::Dimension(Some(n), Unit::Percent, _) => { Ok(Value::String(format!("invert({}%)", n), QuoteKind::None)) } + Value::Dimension(None, ..) => todo!(), Value::Dimension(..) => Err(( "Only one argument may be passed to the plain-CSS invert() function.", args.span(), diff --git a/src/builtin/color/mod.rs b/src/builtin/functions/color/mod.rs similarity index 76% rename from src/builtin/color/mod.rs rename to src/builtin/functions/color/mod.rs index 42bb71d..55c6425 100644 --- a/src/builtin/color/mod.rs +++ b/src/builtin/functions/color/mod.rs @@ -1,9 +1,9 @@ use super::{Builtin, GlobalFunctionMap}; -mod hsl; -mod opacity; -mod other; -mod rgb; +pub mod hsl; +pub mod opacity; +pub mod other; +pub mod rgb; pub(crate) fn declare(f: &mut GlobalFunctionMap) { hsl::declare(f); diff --git a/src/builtin/color/opacity.rs b/src/builtin/functions/color/opacity.rs similarity index 79% rename from src/builtin/color/opacity.rs rename to src/builtin/functions/color/opacity.rs index 313ee60..2686a23 100644 --- a/src/builtin/color/opacity.rs +++ b/src/builtin/functions/color/opacity.rs @@ -5,35 +5,18 @@ use crate::{ value::Value, }; +/// Check if `s` matches the regex `^[a-zA-Z]+\s*=` fn is_ms_filter(s: &str) -> bool { - let mut chars = s.chars(); + let mut bytes = s.bytes(); - if let Some(c) = chars.next() { - if !matches!(c, 'a'..='z' | 'A'..='Z') { - return false; - } - } else { + if !bytes.next().map_or(false, |c| c.is_ascii_alphabetic()) { return false; } - for c in &mut chars { - match c { - ' ' | '\t' | '\n' => break, - 'a'..='z' | 'A'..='Z' => continue, - '=' => return true, - _ => return false, - } - } - - for c in chars { - match c { - ' ' | '\t' | '\n' => continue, - '=' => return true, - _ => return false, - } - } - - false + bytes + .skip_while(u8::is_ascii_alphabetic) + .find(|c| !matches!(c, b' ' | b'\t' | b'\n')) + == Some(b'=') } #[cfg(test)] @@ -52,10 +35,10 @@ mod test { } } -fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { if args.len() <= 1 { match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), Value::String(s, QuoteKind::None) if is_ms_filter(&s) => { Ok(Value::String(format!("alpha({})", s), QuoteKind::None)) } @@ -86,14 +69,15 @@ fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None, true)), - Value::Dimension(num, unit, _) => Ok(Value::String( + Value::Color(c) => Ok(Value::Dimension(Some(c.alpha()), Unit::None, true)), + Value::Dimension(Some(num), unit, _) => Ok(Value::String( format!("opacity({}{})", num, unit), QuoteKind::None, )), + Value::Dimension(None, ..) => todo!(), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -102,6 +86,7 @@ fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } +// todo: unify `opacify` and `fade_in` fn opacify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let color = match args.get_err(0, "color")? { @@ -115,7 +100,8 @@ fn opacify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$amount: {} is not a number.", v.inspect(args.span())?), @@ -140,7 +126,8 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$amount: {} is not a number.", v.inspect(args.span())?), @@ -152,6 +139,7 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::Color(Box::new(color.fade_in(amount)))) } +// todo: unify with `fade_out` fn transparentize(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let color = match args.get_err(0, "color")? { @@ -165,7 +153,8 @@ fn transparentize(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult bound!(args, "amount", n, u, 0, 1), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$amount: {} is not a number.", v.inspect(args.span())?), @@ -190,7 +179,8 @@ fn fade_out(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let amount = match args.get_err(1, "amount")? { - Value::Dimension(n, u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(Some(n), u, _) => bound!(args, "amount", n, u, 0, 1), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$amount: {} is not a number.", v.inspect(args.span())?), diff --git a/src/builtin/color/other.rs b/src/builtin/functions/color/other.rs similarity index 90% rename from src/builtin/color/other.rs rename to src/builtin/functions/color/other.rs index af1367d..5b0b66e 100644 --- a/src/builtin/color/other.rs +++ b/src/builtin/functions/color/other.rs @@ -15,7 +15,8 @@ use crate::{ macro_rules! opt_rgba { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { let $name = match $args.default_named_arg($arg, Value::Null)? { - Value::Dimension(n, u, _) => Some(bound!($args, $arg, n, u, $low, $high)), + Value::Dimension(Some(n), u, _) => Some(bound!($args, $arg, n, u, $low, $high)), + Value::Dimension(None, ..) => todo!(), Value::Null => None, v => { return Err(( @@ -31,9 +32,10 @@ macro_rules! opt_rgba { macro_rules! opt_hsl { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { let $name = match $args.default_named_arg($arg, Value::Null)? { - Value::Dimension(n, u, _) => { + Value::Dimension(Some(n), u, _) => { Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100)) } + Value::Dimension(None, ..) => todo!(), Value::Null => None, v => { return Err(( @@ -46,7 +48,7 @@ macro_rules! opt_hsl { }; } -fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { if args.positional_arg(1).is_some() { return Err(( "Only one positional argument is allowed. All other arguments must be passed by name.", @@ -81,7 +83,8 @@ fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult Some(n), + Value::Dimension(Some(n), ..) => Some(n), + Value::Dimension(None, ..) => todo!(), Value::Null => None, v => { return Err(( @@ -113,7 +116,7 @@ fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let color = match args.get_err(0, "color")? { Value::Color(c) => c, v => { @@ -140,7 +143,8 @@ fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult Some(n), + Value::Dimension(Some(n), ..) => Some(n), + Value::Dimension(None, ..) => todo!(), Value::Null => None, v => { return Err(( @@ -175,8 +179,8 @@ fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { - fn scale(val: Number, by: Number, max: Number) -> Number { +pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { + pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number { if by.is_zero() { return val; } @@ -198,9 +202,10 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult macro_rules! opt_scale_arg { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { let $name = match $args.default_named_arg($arg, Value::Null)? { - Value::Dimension(n, Unit::Percent, _) => { + Value::Dimension(Some(n), Unit::Percent, _) => { Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100)) } + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -288,7 +293,7 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult })) } -fn ie_hex_str(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn ie_hex_str(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, diff --git a/src/builtin/color/rgb.rs b/src/builtin/functions/color/rgb.rs similarity index 82% rename from src/builtin/color/rgb.rs rename to src/builtin/functions/color/rgb.rs index 3249f44..9bf36ab 100644 --- a/src/builtin/color/rgb.rs +++ b/src/builtin/functions/color/rgb.rs @@ -38,10 +38,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } let blue = match channels.pop() { - Some(Value::Dimension(n, Unit::None, _)) => n, - Some(Value::Dimension(n, Unit::Percent, _)) => { + Some(Value::Dimension(Some(n), Unit::None, _)) => n, + Some(Value::Dimension(Some(n), Unit::Percent, _)) => { (n / Number::from(100)) * Number::from(255) } + Some(Value::Dimension(None, ..)) => todo!(), Some(v) if v.is_special_function() => { let green = channels.pop().unwrap(); let red = channels.pop().unwrap(); @@ -67,10 +68,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> }; let green = match channels.pop() { - Some(Value::Dimension(n, Unit::None, _)) => n, - Some(Value::Dimension(n, Unit::Percent, _)) => { + Some(Value::Dimension(Some(n), Unit::None, _)) => n, + Some(Value::Dimension(Some(n), Unit::Percent, _)) => { (n / Number::from(100)) * Number::from(255) } + Some(Value::Dimension(None, ..)) => todo!(), Some(v) if v.is_special_function() => { let string = match channels.pop() { Some(red) => format!( @@ -95,10 +97,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> }; let red = match channels.pop() { - Some(Value::Dimension(n, Unit::None, _)) => n, - Some(Value::Dimension(n, Unit::Percent, _)) => { + Some(Value::Dimension(Some(n), Unit::None, _)) => n, + Some(Value::Dimension(Some(n), Unit::Percent, _)) => { (n / Number::from(100)) * Number::from(255) } + Some(Value::Dimension(None, ..)) => todo!(), Some(v) if v.is_special_function() => { return Ok(Value::String( format!( @@ -148,8 +151,9 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } }; let alpha = match args.get_err(1, "alpha")? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -184,8 +188,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> Ok(Value::Color(Box::new(color.with_alpha(alpha)))) } else { let red = match args.get_err(0, "red")? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => { + (n / Number::from(100)) * Number::from(255) + } + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -222,8 +229,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } }; let green = match args.get_err(1, "green")? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => { + (n / Number::from(100)) * Number::from(255) + } + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -259,8 +269,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } }; let blue = match args.get_err(2, "blue")? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => { + (n / Number::from(100)) * Number::from(255) + } + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -297,10 +310,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> let alpha = match args.default_arg( 3, "alpha", - Value::Dimension(Number::one(), Unit::None, true), + Value::Dimension(Some(Number::one()), Unit::None, true), )? { - Value::Dimension(n, Unit::None, _) => n, - Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -336,18 +350,18 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) -> } } -fn rgb(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn rgb(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { inner_rgb("rgb", args, parser) } -fn rgba(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn rgba(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { inner_rgb("rgba", args, parser) } -fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.red()), Unit::None, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -356,10 +370,10 @@ fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.green()), Unit::None, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -368,10 +382,10 @@ fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { - Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None, true)), + Value::Color(c) => Ok(Value::Dimension(Some(c.blue()), Unit::None, true)), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), @@ -380,7 +394,7 @@ fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let color1 = match args.get_err(0, "color1")? { Value::Color(c) => c, @@ -407,9 +421,10 @@ fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let weight = match args.default_arg( 2, "weight", - Value::Dimension(Number::from(50), Unit::None, true), + Value::Dimension(Some(Number::from(50)), Unit::None, true), )? { - Value::Dimension(n, u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!( diff --git a/src/builtin/list.rs b/src/builtin/functions/list.rs similarity index 87% rename from src/builtin/list.rs rename to src/builtin/functions/list.rs index 9dbb483..56ac391 100644 --- a/src/builtin/list.rs +++ b/src/builtin/functions/list.rs @@ -11,20 +11,21 @@ use crate::{ value::{Number, Value}, }; -fn length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; Ok(Value::Dimension( - Number::from(args.get_err(0, "list")?.as_list().len()), + Some(Number::from(args.get_err(0, "list")?.as_list().len())), Unit::None, true, )) } -fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let mut list = args.get_err(0, "list")?.as_list(); let n = match args.get_err(1, "n")? { - Value::Dimension(num, ..) => num, + Value::Dimension(Some(num), ..) => num, + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$n: {} is not a number.", v.inspect(args.span())?), @@ -61,7 +62,7 @@ fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { })) } -fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; Ok(Value::String( match args.get_err(0, "list")? { @@ -73,7 +74,7 @@ fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), @@ -81,7 +82,8 @@ fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { v => (vec![v], ListSeparator::Space, Brackets::None), }; let n = match args.get_err(1, "n")? { - Value::Dimension(num, ..) => num, + Value::Dimension(Some(num), ..) => num, + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("$n: {} is not a number.", v.inspect(args.span())?), @@ -120,7 +122,7 @@ fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::List(list, sep, brackets)) } -fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), @@ -158,7 +160,7 @@ fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::List(list, sep, brackets)) } -fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(4)?; let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { Value::List(v, sep, brackets) => (v, sep, brackets), @@ -225,7 +227,7 @@ fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::List(list1, sep, brackets)) } -fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; Ok(Value::bool(match args.get_err(0, "list")? { Value::List(.., brackets) => match brackets { @@ -236,7 +238,7 @@ fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let list = args.get_err(0, "list")?.as_list(); let value = args.get_err(1, "value")?; @@ -244,10 +246,10 @@ fn index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Some(v) => Number::from(v + 1), None => return Ok(Value::Null), }; - Ok(Value::Dimension(index, Unit::None, true)) + Ok(Value::Dimension(Some(index), Unit::None, true)) } -fn zip(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn zip(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let lists = args .get_variadic()? .into_iter() diff --git a/src/builtin/macros.rs b/src/builtin/functions/macros.rs similarity index 100% rename from src/builtin/macros.rs rename to src/builtin/functions/macros.rs diff --git a/src/builtin/map.rs b/src/builtin/functions/map.rs similarity index 87% rename from src/builtin/map.rs rename to src/builtin/functions/map.rs index 068d4af..5572b12 100644 --- a/src/builtin/map.rs +++ b/src/builtin/functions/map.rs @@ -8,7 +8,7 @@ use crate::{ value::{SassMap, Value}, }; -fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let key = args.get_err(1, "key")?; let map = match args.get_err(0, "map")? { @@ -26,7 +26,7 @@ fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(map.get(&key)?.unwrap_or(Value::Null)) } -fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let key = args.get_err(1, "key")?; let map = match args.get_err(0, "map")? { @@ -44,7 +44,7 @@ fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult Ok(Value::bool(map.get(&key)?.is_some())) } -fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let map = match args.get_err(0, "map")? { Value::Map(m) => m, @@ -65,7 +65,7 @@ fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { )) } -fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let map = match args.get_err(0, "map")? { Value::Map(m) => m, @@ -86,7 +86,7 @@ fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult )) } -fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let mut map1 = match args.get_err(0, "map1")? { Value::Map(m) => m, @@ -116,7 +116,7 @@ fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::Map(map1)) } -fn map_remove(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let mut map = match args.get_err(0, "map")? { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), diff --git a/src/builtin/math.rs b/src/builtin/functions/math.rs similarity index 72% rename from src/builtin/math.rs rename to src/builtin/functions/math.rs index 94104ec..470937b 100644 --- a/src/builtin/math.rs +++ b/src/builtin/functions/math.rs @@ -13,10 +13,11 @@ use crate::{ value::{Number, Value}, }; -fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let num = match args.get_err(0, "number")? { - Value::Dimension(n, Unit::None, _) => n * Number::from(100), + Value::Dimension(Some(n), Unit::None, _) => n * Number::from(100), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -35,13 +36,14 @@ fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult .into()) } }; - Ok(Value::Dimension(num, Unit::Percent, true)) + Ok(Value::Dimension(Some(num), Unit::Percent, true)) } -fn round(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn round(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "number")? { - Value::Dimension(n, u, _) => Ok(Value::Dimension(n.round(), u, true)), + Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.round()), u, true)), + Value::Dimension(None, ..) => todo!(), v => Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), @@ -50,10 +52,11 @@ fn round(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "number")? { - Value::Dimension(n, u, _) => Ok(Value::Dimension(n.ceil(), u, true)), + Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.ceil()), u, true)), + Value::Dimension(None, ..) => todo!(), v => Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), @@ -62,10 +65,11 @@ fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "number")? { - Value::Dimension(n, u, _) => Ok(Value::Dimension(n.floor(), u, true)), + Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.floor()), u, true)), + Value::Dimension(None, ..) => todo!(), v => Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), @@ -74,10 +78,11 @@ fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "number")? { - Value::Dimension(n, u, _) => Ok(Value::Dimension(n.abs(), u, true)), + Value::Dimension(Some(n), u, _) => Ok(Value::Dimension(Some(n.abs()), u, true)), + Value::Dimension(None, ..) => todo!(), v => Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), @@ -86,7 +91,7 @@ fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let unit1 = match args.get_err(0, "number1")? { Value::Dimension(_, u, _) => u, @@ -114,14 +119,15 @@ fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult // TODO: write tests for this #[cfg(feature = "random")] -fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let limit = match args.default_arg(0, "limit", Value::Null)? { - Value::Dimension(n, ..) => n, + Value::Dimension(Some(n), ..) => n, + Value::Dimension(None, ..) => todo!(), Value::Null => { let mut rng = rand::thread_rng(); return Ok(Value::Dimension( - Number::from(rng.gen_range(0.0, 1.0)), + Some(Number::from(rng.gen_range(0.0, 1.0))), Unit::None, true, )); @@ -136,7 +142,7 @@ fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { }; if limit.is_one() { - return Ok(Value::Dimension(Number::one(), Unit::None, true)); + return Ok(Value::Dimension(Some(Number::one()), Unit::None, true)); } if limit.is_decimal() { @@ -164,20 +170,21 @@ fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let mut rng = rand::thread_rng(); Ok(Value::Dimension( - Number::from(rng.gen_range(0, limit) + 1), + Some(Number::from(rng.gen_range(0, limit) + 1)), Unit::None, true, )) } -fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { - Value::Dimension(number, unit, _) => Ok((number, unit)), + Value::Dimension(Some(number), unit, _) => Ok((number, unit)), + Value::Dimension(None, ..) => todo!(), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? @@ -190,12 +197,12 @@ fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { if ValueVisitor::new(parser, span) .less_than( HigherIntermediateValue::Literal(Value::Dimension( - num.0.clone(), + Some(num.0.clone()), num.1.clone(), true, )), HigherIntermediateValue::Literal(Value::Dimension( - min.0.clone(), + Some(min.0.clone()), min.1.clone(), true, )), @@ -205,17 +212,18 @@ fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { min = num; } } - Ok(Value::Dimension(min.0, min.1, true)) + Ok(Value::Dimension(Some(min.0), min.1, true)) } -fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { - Value::Dimension(number, unit, _) => Ok((number, unit)), + Value::Dimension(Some(number), unit, _) => Ok((number, unit)), + Value::Dimension(None, ..) => todo!(), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? @@ -228,12 +236,12 @@ fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { if ValueVisitor::new(parser, span) .greater_than( HigherIntermediateValue::Literal(Value::Dimension( - num.0.clone(), + Some(num.0.clone()), num.1.clone(), true, )), HigherIntermediateValue::Literal(Value::Dimension( - max.0.clone(), + Some(max.0.clone()), max.1.clone(), true, )), @@ -243,7 +251,7 @@ fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { max = num; } } - Ok(Value::Dimension(max.0, max.1, true)) + Ok(Value::Dimension(Some(max.0), max.1, true)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { diff --git a/src/builtin/meta.rs b/src/builtin/functions/meta.rs similarity index 60% rename from src/builtin/meta.rs rename to src/builtin/functions/meta.rs index 9255de7..1bdc1b9 100644 --- a/src/builtin/meta.rs +++ b/src/builtin/functions/meta.rs @@ -7,7 +7,6 @@ use crate::{ common::{Identifier, QuoteKind}, error::SassResult, parse::Parser, - unit::Unit, value::{SassFunction, Value}, }; @@ -20,7 +19,7 @@ fn if_(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "feature")? { #[allow(clippy::match_same_arms)] @@ -50,7 +49,7 @@ fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let unit = match args.get_err(0, "number")? { Value::Dimension(_, u, _) => u.to_string(), @@ -65,23 +64,18 @@ fn unit(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { Ok(Value::String(unit, QuoteKind::Quoted)) } -fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; let value = args.get_err(0, "value")?; Ok(Value::String(value.kind().to_owned(), QuoteKind::None)) } -fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; - #[allow(clippy::match_same_arms)] - Ok(match args.get_err(0, "number")? { - Value::Dimension(_, Unit::None, _) => Value::True, - Value::Dimension(..) => Value::False, - _ => Value::True, - }) + Ok(Value::bool(args.get_err(0, "number")?.unitless())) } -fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; Ok(Value::String( args.get_err(0, "value")?.inspect(args.span())?.into_owned(), @@ -89,7 +83,7 @@ fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { )) } -fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "name")? { Value::String(s, _) => Ok(Value::bool( @@ -103,33 +97,81 @@ fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { - args.max_args(1)?; - match args.get_err(0, "name")? { - Value::String(s, _) => Ok(Value::bool(parser.global_scope.var_exists(s.into()))), - v => Err(( - format!("$name: {} is not a string.", v.inspect(args.span())?), - args.span(), - ) - .into()), - } -} - -fn mixin_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn global_variable_exists( + mut args: CallArgs, + parser: &mut Parser<'_>, +) -> SassResult { args.max_args(2)?; - match args.get_err(0, "name")? { - Value::String(s, _) => Ok(Value::bool( - parser.scopes.mixin_exists(s.into(), parser.global_scope), - )), - v => Err(( - format!("$name: {} is not a string.", v.inspect(args.span())?), - args.span(), - ) - .into()), - } + + let name: Identifier = match args.get_err(0, "name")? { + Value::String(s, _) => s.into(), + v => { + return Err(( + format!("$name: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let module = match args.default_arg(1, "module", Value::Null)? { + Value::String(s, _) => Some(s), + Value::Null => None, + v => { + return Err(( + format!("$module: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::bool(if let Some(module_name) = module { + parser + .modules + .get(module_name.into(), args.span())? + .var_exists(name) + } else { + parser.global_scope.var_exists(name) + })) } -fn function_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { + args.max_args(2)?; + let name: Identifier = match args.get_err(0, "name")? { + Value::String(s, _) => s.into(), + v => { + return Err(( + format!("$name: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let module = match args.default_arg(1, "module", Value::Null)? { + Value::String(s, _) => Some(s), + Value::Null => None, + v => { + return Err(( + format!("$module: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::bool(if let Some(module_name) = module { + parser + .modules + .get(module_name.into(), args.span())? + .mixin_exists(name) + } else { + parser.scopes.mixin_exists(name, parser.global_scope) + })) +} + +pub(crate) fn function_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; match args.get_err(0, "name")? { Value::String(s, _) => Ok(Value::bool( @@ -143,7 +185,7 @@ fn function_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let name: Identifier = match args.get_err(0, "name")? { Value::String(s, _) => s.into(), @@ -168,22 +210,26 @@ fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult SassFunction::UserDefined(Box::new(f), name), + parser + .modules + .get(module_name.into(), args.span())? + .get_fn(Spanned { + node: name, + span: args.span(), + })? + } else { + parser.scopes.get_fn(name, parser.global_scope) + } { + Some(f) => f, None => match GLOBAL_FUNCTIONS.get(name.as_str()) { Some(f) => SassFunction::Builtin(f.clone(), name), None => return Err((format!("Function not found: {}", name), args.span()).into()), @@ -193,7 +239,7 @@ fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn call(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let func = match args.get_err(0, "function")? { Value::FunctionRef(f) => f, v => { @@ -211,7 +257,7 @@ fn call(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } #[allow(clippy::needless_pass_by_value)] -fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(0)?; if !parser.flags.in_mixin() { return Err(( @@ -225,6 +271,13 @@ fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult )) } +#[allow(unused_variables)] +pub(crate) fn keywords(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + drop(args); + todo!("builtin function `keywords` blocked on better handling of call args") +} + pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("if", Builtin::new(if_)); f.insert("feature-exists", Builtin::new(feature_exists)); @@ -242,4 +295,5 @@ pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("get-function", Builtin::new(get_function)); f.insert("call", Builtin::new(call)); f.insert("content-exists", Builtin::new(content_exists)); + f.insert("keywords", Builtin::new(keywords)); } diff --git a/src/builtin/functions/mod.rs b/src/builtin/functions/mod.rs new file mode 100644 index 0000000..1c28974 --- /dev/null +++ b/src/builtin/functions/mod.rs @@ -0,0 +1,60 @@ +// A reference to the parser is only necessary for some functions +#![allow(unused_variables)] + +use std::{ + collections::HashMap, + sync::atomic::{AtomicUsize, Ordering}, +}; + +use once_cell::sync::Lazy; + +use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value}; + +#[macro_use] +mod macros; + +pub mod color; +pub mod list; +pub mod map; +pub mod math; +pub mod meta; +pub mod selector; +pub mod string; + +pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>; + +static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0); + +// TODO: impl Fn +#[derive(Clone)] +pub(crate) struct Builtin( + pub fn(CallArgs, &mut Parser<'_>) -> SassResult, + usize, +); + +impl Builtin { + pub fn new(body: fn(CallArgs, &mut Parser<'_>) -> SassResult) -> Builtin { + let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); + Self(body, count) + } +} + +impl PartialEq for Builtin { + fn eq(&self, other: &Self) -> bool { + self.1 == other.1 + } +} + +impl Eq for Builtin {} + +pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { + let mut m = HashMap::new(); + color::declare(&mut m); + list::declare(&mut m); + map::declare(&mut m); + math::declare(&mut m); + meta::declare(&mut m); + selector::declare(&mut m); + string::declare(&mut m); + m +}); diff --git a/src/builtin/selector.rs b/src/builtin/functions/selector.rs similarity index 89% rename from src/builtin/selector.rs rename to src/builtin/functions/selector.rs index dae2d00..f7d50e9 100644 --- a/src/builtin/selector.rs +++ b/src/builtin/functions/selector.rs @@ -9,7 +9,7 @@ use crate::{ value::Value, }; -fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let parent_selector = args .get_err(0, "super")? @@ -21,7 +21,7 @@ fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; // todo: Value::to_compound_selector let selector = args @@ -51,7 +51,7 @@ fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; Ok(args .get_err(0, "selector")? @@ -59,7 +59,7 @@ fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { @@ -80,7 +80,7 @@ fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { .into_value()) } -fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { @@ -138,7 +138,7 @@ fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult .into_value()) } -fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let selector = args .get_err(0, "selector")? @@ -153,7 +153,7 @@ fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let selector = args .get_err(0, "selector")? @@ -167,7 +167,7 @@ fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let selector1 = args .get_err(0, "selector1")? diff --git a/src/builtin/string.rs b/src/builtin/functions/string.rs similarity index 80% rename from src/builtin/string.rs rename to src/builtin/functions/string.rs index 989d393..edcbbd7 100644 --- a/src/builtin/string.rs +++ b/src/builtin/functions/string.rs @@ -15,7 +15,7 @@ use crate::{ value::{Number, Value}, }; -fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(mut i, q) => { @@ -30,7 +30,7 @@ fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(mut i, q) => { @@ -45,11 +45,11 @@ fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { +pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(i, _) => Ok(Value::Dimension( - Number::from(i.chars().count()), + Some(Number::from(i.chars().count())), Unit::None, true, )), @@ -61,7 +61,7 @@ fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult } } -fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)), @@ -73,7 +73,7 @@ fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(1)?; match args.get_err(0, "string")? { i @ Value::String(..) => Ok(i.unquote()), @@ -85,7 +85,7 @@ fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (string, quotes) = match args.get_err(0, "string")? { Value::String(s, q) => (s, q), @@ -99,17 +99,18 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { }; let str_len = string.chars().count(); let start = match args.get_err(1, "start-at")? { - Value::Dimension(n, Unit::None, _) if n.is_decimal() => { + Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("{} is not an int.", n), args.span()).into()) } - Value::Dimension(n, Unit::None, _) if n.is_positive() => { + Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) } - Value::Dimension(n, Unit::None, _) if n.is_zero() => 1_usize, - Value::Dimension(n, Unit::None, _) if n < -Number::from(str_len) => 1_usize, - Value::Dimension(n, Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) + Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize, + Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 1_usize, + Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) .to_usize() .unwrap(), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -129,17 +130,18 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } }; let mut end = match args.default_arg(2, "end-at", Value::Null)? { - Value::Dimension(n, Unit::None, _) if n.is_decimal() => { + Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("{} is not an int.", n), args.span()).into()) } - Value::Dimension(n, Unit::None, _) if n.is_positive() => { + Value::Dimension(Some(n), Unit::None, _) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) } - Value::Dimension(n, Unit::None, _) if n.is_zero() => 0_usize, - Value::Dimension(n, Unit::None, _) if n < -Number::from(str_len) => 0_usize, - Value::Dimension(n, Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) + Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize, + Value::Dimension(Some(n), Unit::None, _) if n < -Number::from(str_len) => 0_usize, + Value::Dimension(Some(n), Unit::None, _) => (n.to_integer() + BigInt::from(str_len + 1)) .to_usize() .unwrap_or(str_len + 1), + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -178,7 +180,7 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { } } -fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(2)?; let s1 = match args.get_err(0, "string")? { Value::String(i, _) => i, @@ -203,12 +205,12 @@ fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { }; Ok(match s1.find(&substr) { - Some(v) => Value::Dimension(Number::from(v + 1), Unit::None, true), + Some(v) => Value::Dimension(Some(Number::from(v + 1)), Unit::None, true), None => Value::Null, }) } -fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { +pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { args.max_args(3)?; let (s1, quotes) = match args.get_err(0, "string")? { Value::String(i, q) => (i, q), @@ -233,10 +235,11 @@ fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult }; let index = match args.get_err(2, "index")? { - Value::Dimension(n, Unit::None, _) if n.is_decimal() => { + Value::Dimension(Some(n), Unit::None, _) if n.is_decimal() => { return Err((format!("$index: {} is not an int.", n), args.span()).into()) } - Value::Dimension(n, Unit::None, _) => n, + Value::Dimension(Some(n), Unit::None, _) => n, + Value::Dimension(None, ..) => todo!(), v @ Value::Dimension(..) => { return Err(( format!( @@ -305,7 +308,7 @@ fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult #[cfg(feature = "random")] #[allow(clippy::needless_pass_by_value)] -fn unique_id(args: CallArgs, _: &mut Parser<'_>) -> SassResult { +pub(crate) fn unique_id(args: CallArgs, _: &mut Parser<'_>) -> SassResult { args.max_args(0)?; let mut rng = thread_rng(); let string = std::iter::repeat(()) diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index 4ec4354..7dd9b7d 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -1,60 +1,6 @@ -// A reference to the parser is only necessary for some functions -#![allow(unused_variables)] +mod functions; +pub(crate) mod modules; -use std::{ - collections::HashMap, - sync::atomic::{AtomicUsize, Ordering}, +pub(crate) use functions::{ + color, list, map, math, meta, selector, string, Builtin, GLOBAL_FUNCTIONS, }; - -use once_cell::sync::Lazy; - -use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value}; - -#[macro_use] -mod macros; - -mod color; -mod list; -mod map; -mod math; -mod meta; -mod selector; -mod string; - -pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>; - -static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0); - -// TODO: impl Fn -#[derive(Clone)] -pub(crate) struct Builtin( - pub fn(CallArgs, &mut Parser<'_>) -> SassResult, - usize, -); - -impl Builtin { - pub fn new(body: fn(CallArgs, &mut Parser<'_>) -> SassResult) -> Builtin { - let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); - Self(body, count) - } -} - -impl PartialEq for Builtin { - fn eq(&self, other: &Self) -> bool { - self.1 == other.1 - } -} - -impl Eq for Builtin {} - -pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { - let mut m = HashMap::new(); - color::declare(&mut m); - list::declare(&mut m); - map::declare(&mut m); - math::declare(&mut m); - meta::declare(&mut m); - selector::declare(&mut m); - string::declare(&mut m); - m -}); diff --git a/src/builtin/modules/color.rs b/src/builtin/modules/color.rs new file mode 100644 index 0000000..f2cd754 --- /dev/null +++ b/src/builtin/modules/color.rs @@ -0,0 +1,27 @@ +use crate::builtin::{ + color::{ + hsl::{complement, grayscale, hue, invert, lightness, saturation}, + opacity::alpha, + other::{adjust_color, change_color, ie_hex_str, scale_color}, + rgb::{blue, green, mix, red}, + }, + modules::Module, +}; + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("adjust", adjust_color); + f.insert_builtin("alpha", alpha); + f.insert_builtin("blue", blue); + f.insert_builtin("change", change_color); + f.insert_builtin("complement", complement); + f.insert_builtin("grayscale", grayscale); + f.insert_builtin("green", green); + f.insert_builtin("hue", hue); + f.insert_builtin("ie-hex-str", ie_hex_str); + f.insert_builtin("invert", invert); + f.insert_builtin("lightness", lightness); + f.insert_builtin("mix", mix); + f.insert_builtin("red", red); + f.insert_builtin("saturation", saturation); + f.insert_builtin("scale", scale_color); +} diff --git a/src/builtin/modules/list.rs b/src/builtin/modules/list.rs new file mode 100644 index 0000000..16c7a6c --- /dev/null +++ b/src/builtin/modules/list.rs @@ -0,0 +1,16 @@ +use crate::builtin::{ + list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip}, + modules::Module, +}; + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("append", append); + f.insert_builtin("index", index); + f.insert_builtin("is-bracketed", is_bracketed); + f.insert_builtin("join", join); + f.insert_builtin("length", length); + f.insert_builtin("separator", list_separator); + f.insert_builtin("nth", nth); + f.insert_builtin("set-nth", set_nth); + f.insert_builtin("zip", zip); +} diff --git a/src/builtin/modules/map.rs b/src/builtin/modules/map.rs new file mode 100644 index 0000000..13446ad --- /dev/null +++ b/src/builtin/modules/map.rs @@ -0,0 +1,13 @@ +use crate::builtin::{ + map::{map_get, map_has_key, map_keys, map_merge, map_remove, map_values}, + modules::Module, +}; + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("get", map_get); + f.insert_builtin("has-key", map_has_key); + f.insert_builtin("keys", map_keys); + f.insert_builtin("merge", map_merge); + f.insert_builtin("remove", map_remove); + f.insert_builtin("values", map_values); +} diff --git a/src/builtin/modules/math.rs b/src/builtin/modules/math.rs new file mode 100644 index 0000000..ecc553b --- /dev/null +++ b/src/builtin/modules/math.rs @@ -0,0 +1,622 @@ +use std::cmp::Ordering; + +use num_traits::{One, Signed, Zero}; + +use crate::{ + args::CallArgs, + builtin::{ + math::{abs, ceil, comparable, floor, max, min, percentage, round}, + meta::{unit, unitless}, + modules::Module, + }, + common::Op, + error::SassResult, + parse::Parser, + unit::Unit, + value::{Number, Value}, +}; + +#[cfg(feature = "random")] +use crate::builtin::math::random; + +fn clamp(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(3)?; + let span = args.span(); + + let min = match args.get_err(0, "min")? { + v @ Value::Dimension(..) => v, + v => { + return Err(( + format!("$min: {} is not a number.", v.inspect(args.span())?), + span, + ) + .into()) + } + }; + + let number = match args.get_err(1, "number")? { + v @ Value::Dimension(..) => v, + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(span)?), + span, + ) + .into()) + } + }; + + let max = match args.get_err(2, "max")? { + v @ Value::Dimension(..) => v, + v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()), + }; + + // ensure that `min` and `max` are compatible + min.cmp(&max, span, Op::LessThan)?; + + let min_unit = match min { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + let number_unit = match number { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + let max_unit = match max { + Value::Dimension(_, ref u, _) => u, + _ => unreachable!(), + }; + + if min_unit == &Unit::None && number_unit != &Unit::None { + return Err(( + format!( + "$min is unitless but $number has unit {}. Arguments must all have units or all be unitless.", + number_unit + ), span).into()); + } else if min_unit != &Unit::None && number_unit == &Unit::None { + return Err(( + format!( + "$min has unit {} but $number is unitless. Arguments must all have units or all be unitless.", + min_unit + ), span).into()); + } else if min_unit != &Unit::None && max_unit == &Unit::None { + return Err(( + format!( + "$min has unit {} but $max is unitless. Arguments must all have units or all be unitless.", + min_unit + ), span).into()); + } + + match min.cmp(&number, span, Op::LessThan)? { + Ordering::Greater => return Ok(min), + Ordering::Equal => return Ok(number), + Ordering::Less => {} + } + + match max.cmp(&number, span, Op::GreaterThan)? { + Ordering::Less => return Ok(max), + Ordering::Equal => return Ok(number), + Ordering::Greater => {} + } + + Ok(number) +} + +fn hypot(args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.min_args(1)?; + + let span = args.span(); + + let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> { + match v.node { + Value::Dimension(n, u, ..) => Ok((n, u)), + v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), + } + }); + + let first: (Number, Unit) = match numbers.next().unwrap()? { + (Some(n), u) => (n.clone() * n, u), + (None, u) => return Ok(Value::Dimension(None, u, true)), + }; + + let rest = numbers + .enumerate() + .map(|(idx, val)| -> SassResult> { + let (number, unit) = val?; + if first.1 == Unit::None { + if unit == Unit::None { + Ok(number.map(|n| n.clone() * n)) + } else { + Err(( + format!( + "Argument 1 is unitless but argument {} has unit {}. \ + Arguments must all have units or all be unitless.", + idx + 2, + unit + ), + span, + ) + .into()) + } + } else if unit == Unit::None { + Err(( + format!( + "Argument 1 has unit {} but argument {} is unitless. \ + Arguments must all have units or all be unitless.", + first.1, + idx + 2, + ), + span, + ) + .into()) + } else if first.1.comparable(&unit) { + Ok(number + .map(|n| n.convert(&unit, &first.1)) + .map(|n| n.clone() * n)) + } else { + Err(( + format!("Incompatible units {} and {}.", first.1, unit), + span, + ) + .into()) + } + }) + .collect::>>>()?; + + let rest = match rest { + Some(v) => v, + None => return Ok(Value::Dimension(None, first.1, true)), + }; + + let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b); + + Ok(Value::Dimension(sum.sqrt(), first.1, true)) +} + +fn log(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(2)?; + + let number = match args.get_err(0, "number")? { + Value::Dimension(Some(n), Unit::None, ..) => n, + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be unitless.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + v @ Value::Dimension(None, ..) => return Ok(v), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let base = match args.default_arg(1, "base", Value::Null)? { + Value::Null => None, + Value::Dimension(Some(n), Unit::None, ..) => Some(n), + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be unitless.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + v @ Value::Dimension(None, ..) => return Ok(v), + v => { + return Err(( + format!("$base: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::Dimension( + if let Some(base) = base { + if base.is_zero() { + Some(Number::zero()) + } else { + (|| Some(number.ln()? / base.ln()?))() + } + } else if number.is_negative() { + None + } else if number.is_zero() { + todo!() + } else { + number.ln() + }, + Unit::None, + true, + )) +} + +fn pow(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(2)?; + + let base = match args.get_err(0, "base")? { + Value::Dimension(Some(n), Unit::None, ..) => n, + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$base: Expected {} to have no units.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)), + v => { + return Err(( + format!("$base: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let exponent = match args.get_err(1, "exponent")? { + Value::Dimension(Some(n), Unit::None, ..) => n, + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$exponent: Expected {} to have no units.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => return Ok(Value::Dimension(None, Unit::None, true)), + v => { + return Err(( + format!("$exponent: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::Dimension(base.pow(exponent), Unit::None, true)) +} + +fn sqrt(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + let number = args.get_err(0, "number")?; + + Ok(match number { + Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension(n.sqrt(), Unit::None, true), + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to have no units.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }) +} + +macro_rules! trig_fn { + ($name:ident, $name_deg:ident) => { + fn $name(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + let number = args.get_err(0, "number")?; + + Ok(match number { + Value::Dimension(Some(n), Unit::None, ..) + | Value::Dimension(Some(n), Unit::Rad, ..) => { + Value::Dimension(n.$name(), Unit::None, true) + } + Value::Dimension(Some(n), Unit::Deg, ..) => { + Value::Dimension(n.$name_deg(), Unit::None, true) + } + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be an angle.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => Value::Dimension(None, Unit::None, true), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }) + } + }; +} + +trig_fn!(cos, cos_deg); +trig_fn!(sin, sin_deg); +trig_fn!(tan, tan_deg); + +fn acos(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + let number = args.get_err(0, "number")?; + + Ok(match number { + Value::Dimension(Some(n), Unit::None, ..) => Value::Dimension( + if n > Number::from(1) || n < Number::from(-1) { + None + } else if n.is_one() { + Some(Number::zero()) + } else { + n.acos() + }, + Unit::Deg, + true, + ), + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be unitless.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }) +} + +fn asin(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + let number = args.get_err(0, "number")?; + + Ok(match number { + Value::Dimension(Some(n), Unit::None, ..) => { + if n > Number::from(1) || n < Number::from(-1) { + return Ok(Value::Dimension(None, Unit::Deg, true)); + } else if n.is_zero() { + return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true)); + } + + Value::Dimension(n.asin(), Unit::Deg, true) + } + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be unitless.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }) +} + +fn atan(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + let number = args.get_err(0, "number")?; + + Ok(match number { + Value::Dimension(Some(n), Unit::None, ..) => { + if n.is_zero() { + return Ok(Value::Dimension(Some(Number::zero()), Unit::Deg, true)); + } + + Value::Dimension(n.atan(), Unit::Deg, true) + } + v @ Value::Dimension(Some(..), ..) => { + return Err(( + format!( + "$number: Expected {} to be unitless.", + v.inspect(args.span())? + ), + args.span(), + ) + .into()) + } + Value::Dimension(None, ..) => Value::Dimension(None, Unit::Deg, true), + v => { + return Err(( + format!("$number: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }) +} + +fn atan2(mut args: CallArgs, _: &mut Parser<'_>) -> SassResult { + args.max_args(2)?; + let (y_num, y_unit) = match args.get_err(0, "y")? { + Value::Dimension(n, u, ..) => (n, u), + v => { + return Err(( + format!("$y: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let (x_num, x_unit) = match args.get_err(1, "x")? { + Value::Dimension(n, u, ..) => (n, u), + v => { + return Err(( + format!("$x: {} is not a number.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None { + let x = match x_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + let y = match y_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + (x, y) + } else if y_unit == Unit::None { + return Err(( + format!( + "$y is unitless but $x has unit {}. \ + Arguments must all have units or all be unitless.", + x_unit + ), + args.span(), + ) + .into()); + } else if x_unit == Unit::None { + return Err(( + format!( + "$y has unit {} but $x is unitless. \ + Arguments must all have units or all be unitless.", + y_unit + ), + args.span(), + ) + .into()); + } else if x_unit.comparable(&y_unit) { + let x = match x_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + let y = match y_num { + Some(n) => n, + None => return Ok(Value::Dimension(None, Unit::Deg, true)), + }; + + (x, y.convert(&y_unit, &x_unit)) + } else { + return Err(( + format!("Incompatible units {} and {}.", y_unit, x_unit), + args.span(), + ) + .into()); + }; + + Ok( + match ( + NumberState::from_number(&x_num), + NumberState::from_number(&y_num), + ) { + (NumberState::Zero, NumberState::FiniteNegative) => { + Value::Dimension(Some(Number::from(-90)), Unit::Deg, true) + } + (NumberState::Zero, NumberState::Zero) | (NumberState::Finite, NumberState::Zero) => { + Value::Dimension(Some(Number::zero()), Unit::Deg, true) + } + (NumberState::Zero, NumberState::Finite) => { + Value::Dimension(Some(Number::from(90)), Unit::Deg, true) + } + (NumberState::Finite, NumberState::Finite) + | (NumberState::FiniteNegative, NumberState::Finite) + | (NumberState::Finite, NumberState::FiniteNegative) + | (NumberState::FiniteNegative, NumberState::FiniteNegative) => Value::Dimension( + y_num + .atan2(x_num) + .map(|n| (n * Number::from(180)) / Number::pi()), + Unit::Deg, + true, + ), + (NumberState::FiniteNegative, NumberState::Zero) => { + Value::Dimension(Some(Number::from(180)), Unit::Deg, true) + } + }, + ) +} + +enum NumberState { + Zero, + Finite, + FiniteNegative, +} + +impl NumberState { + fn from_number(num: &Number) -> Self { + match (num.is_zero(), num.is_positive()) { + (true, _) => NumberState::Zero, + (false, true) => NumberState::Finite, + (false, false) => NumberState::FiniteNegative, + } + } +} + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("ceil", ceil); + f.insert_builtin("floor", floor); + f.insert_builtin("max", max); + f.insert_builtin("min", min); + f.insert_builtin("round", round); + f.insert_builtin("abs", abs); + f.insert_builtin("compatible", comparable); + f.insert_builtin("is-unitless", unitless); + f.insert_builtin("unit", unit); + f.insert_builtin("percentage", percentage); + f.insert_builtin("clamp", clamp); + f.insert_builtin("sqrt", sqrt); + f.insert_builtin("cos", cos); + f.insert_builtin("sin", sin); + f.insert_builtin("tan", tan); + f.insert_builtin("acos", acos); + f.insert_builtin("asin", asin); + f.insert_builtin("atan", atan); + f.insert_builtin("log", log); + f.insert_builtin("pow", pow); + f.insert_builtin("hypot", hypot); + f.insert_builtin("atan2", atan2); + #[cfg(feature = "random")] + f.insert_builtin("random", random); + + f.insert_builtin_var( + "e", + Value::Dimension(Some(Number::from(std::f64::consts::E)), Unit::None, true), + ); + f.insert_builtin_var( + "pi", + Value::Dimension(Some(Number::from(std::f64::consts::PI)), Unit::None, true), + ); +} diff --git a/src/builtin/modules/meta.rs b/src/builtin/modules/meta.rs new file mode 100644 index 0000000..fb0b1ab --- /dev/null +++ b/src/builtin/modules/meta.rs @@ -0,0 +1,127 @@ +use codemap::Spanned; + +use crate::{ + args::CallArgs, + builtin::{ + meta::{ + call, content_exists, feature_exists, function_exists, get_function, + global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists, + }, + modules::{Module, ModuleConfig}, + }, + error::SassResult, + parse::{Parser, Stmt}, + value::Value, +}; + +fn load_css(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult> { + args.max_args(2)?; + + let span = args.span(); + + // todo: https://github.com/sass/dart-sass/issues/1054 + let url = match args.get_err(0, "module")? { + Value::String(s, ..) => s, + v => { + return Err(( + format!("$module: {} is not a string.", v.inspect(span)?), + span, + ) + .into()) + } + }; + + let with = match args.default_arg(1, "with", Value::Null)? { + Value::Map(map) => Some(map), + Value::Null => None, + v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()), + }; + + // todo: tests for `with` + if let Some(with) = with { + let mut config = ModuleConfig::default(); + + for (key, value) in with { + let key = match key { + Value::String(s, ..) => s, + v => { + return Err(( + format!("$with key: {} is not a string.", v.inspect(span)?), + span, + ) + .into()) + } + }; + + config.insert( + Spanned { + node: key.into(), + span, + }, + value.span(span), + )?; + } + + let (_, stmts) = parser.load_module(&url, &mut config)?; + + Ok(stmts) + } else { + parser.parse_single_import(&url, span) + } +} + +fn module_functions(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + + let module = match args.get_err(0, "module")? { + Value::String(s, ..) => s, + v => { + return Err(( + format!("$module: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::Map( + parser.modules.get(module.into(), args.span())?.functions(), + )) +} + +fn module_variables(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult { + args.max_args(1)?; + + let module = match args.get_err(0, "module")? { + Value::String(s, ..) => s, + v => { + return Err(( + format!("$module: {} is not a string.", v.inspect(args.span())?), + args.span(), + ) + .into()) + } + }; + + Ok(Value::Map( + parser.modules.get(module.into(), args.span())?.variables(), + )) +} + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("feature-exists", feature_exists); + f.insert_builtin("inspect", inspect); + f.insert_builtin("type-of", type_of); + f.insert_builtin("keywords", keywords); + f.insert_builtin("global-variable-exists", global_variable_exists); + f.insert_builtin("variable-exists", variable_exists); + f.insert_builtin("function-exists", function_exists); + f.insert_builtin("mixin-exists", mixin_exists); + f.insert_builtin("content-exists", content_exists); + f.insert_builtin("module-variables", module_variables); + f.insert_builtin("module-functions", module_functions); + f.insert_builtin("get-function", get_function); + f.insert_builtin("call", call); + + f.insert_builtin_mixin("load-css", load_css); +} diff --git a/src/builtin/modules/mod.rs b/src/builtin/modules/mod.rs new file mode 100644 index 0000000..f9da0c6 --- /dev/null +++ b/src/builtin/modules/mod.rs @@ -0,0 +1,282 @@ +use std::collections::BTreeMap; + +use codemap::{Span, Spanned}; + +use crate::{ + args::CallArgs, + atrule::mixin::{BuiltinMixin, Mixin}, + builtin::Builtin, + common::{Identifier, QuoteKind}, + error::SassResult, + parse::Parser, + scope::Scope, + value::{SassFunction, SassMap, Value}, +}; + +mod color; +mod list; +mod map; +mod math; +mod meta; +mod selector; +mod string; + +#[derive(Debug, Default)] +pub(crate) struct Module { + pub scope: Scope, + + /// Whether or not this module is builtin + /// e.g. `"sass:math"` + is_builtin: bool, +} + +#[derive(Debug, Default)] +pub(crate) struct Modules(BTreeMap); + +#[derive(Debug, Default)] +pub(crate) struct ModuleConfig(BTreeMap); + +impl ModuleConfig { + /// Removes and returns element with name + pub fn get(&mut self, name: Identifier) -> Option { + self.0.remove(&name) + } + + /// If this structure is not empty at the end of + /// an `@use`, we must throw an error + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + pub fn insert(&mut self, name: Spanned, value: Spanned) -> SassResult<()> { + if self.0.insert(name.node, value.node).is_some() { + Err(( + "The same variable may only be configured once.", + name.span.merge(value.span), + ) + .into()) + } else { + Ok(()) + } + } +} + +impl Modules { + pub fn insert(&mut self, name: Identifier, module: Module, span: Span) -> SassResult<()> { + if self.0.contains_key(&name) { + return Err(( + format!("There's already a module with namespace \"{}\".", name), + span, + ) + .into()); + } + + self.0.insert(name, module); + + Ok(()) + } + + pub fn get(&self, name: Identifier, span: Span) -> SassResult<&Module> { + match self.0.get(&name) { + Some(v) => Ok(v), + None => Err(( + format!( + "There is no module with the namespace \"{}\".", + name.as_str() + ), + span, + ) + .into()), + } + } + + pub fn get_mut(&mut self, name: Identifier, span: Span) -> SassResult<&mut Module> { + match self.0.get_mut(&name) { + Some(v) => Ok(v), + None => Err(( + format!( + "There is no module with the namespace \"{}\".", + name.as_str() + ), + span, + ) + .into()), + } + } +} + +impl Module { + pub fn new_builtin() -> Self { + Module { + scope: Scope::default(), + is_builtin: true, + } + } + + pub fn get_var(&self, name: Spanned) -> SassResult<&Value> { + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + match self.scope.vars.get(&name.node) { + Some(v) => Ok(v), + None => Err(("Undefined variable.", name.span).into()), + } + } + + pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { + if self.is_builtin { + return Err(("Cannot modify built-in variable.", name.span).into()); + } + + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + if self.scope.insert_var(name.node, value).is_some() { + Ok(()) + } else { + Err(("Undefined variable.", name.span).into()) + } + } + + pub fn get_mixin(&self, name: Spanned) -> SassResult { + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + match self.scope.mixins.get(&name.node) { + Some(v) => Ok(v.clone()), + None => Err(("Undefined mixin.", name.span).into()), + } + } + + pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) { + self.scope.mixins.insert(name.into(), Mixin::Builtin(mixin)); + } + + pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) { + self.scope.vars.insert(name.into(), value); + } + + pub fn get_fn(&self, name: Spanned) -> SassResult> { + if name.node.as_str().starts_with('-') { + return Err(( + "Private members can't be accessed from outside their modules.", + name.span, + ) + .into()); + } + + Ok(self.scope.functions.get(&name.node).cloned()) + } + + pub fn var_exists(&self, name: Identifier) -> bool { + !name.as_str().starts_with('-') && self.scope.var_exists(name) + } + + pub fn mixin_exists(&self, name: Identifier) -> bool { + !name.as_str().starts_with('-') && self.scope.mixin_exists(name) + } + + pub fn insert_builtin( + &mut self, + name: &'static str, + function: fn(CallArgs, &mut Parser<'_>) -> SassResult, + ) { + let ident = name.into(); + self.scope + .functions + .insert(ident, SassFunction::Builtin(Builtin::new(function), ident)); + } + + pub fn functions(&self) -> SassMap { + SassMap::new_with( + self.scope + .functions + .iter() + .filter(|(key, _)| !key.as_str().starts_with('-')) + .map(|(key, value)| { + ( + Value::String(key.to_string(), QuoteKind::Quoted), + Value::FunctionRef(value.clone()), + ) + }) + .collect::>(), + ) + } + + pub fn variables(&self) -> SassMap { + SassMap::new_with( + self.scope + .vars + .iter() + .filter(|(key, _)| !key.as_str().starts_with('-')) + .map(|(key, value)| { + ( + Value::String(key.to_string(), QuoteKind::Quoted), + value.clone(), + ) + }) + .collect::>(), + ) + } + + pub const fn new_from_scope(scope: Scope, is_builtin: bool) -> Self { + Module { scope, is_builtin } + } +} + +pub(crate) fn declare_module_color() -> Module { + let mut module = Module::new_builtin(); + color::declare(&mut module); + module +} + +pub(crate) fn declare_module_list() -> Module { + let mut module = Module::new_builtin(); + list::declare(&mut module); + module +} + +pub(crate) fn declare_module_map() -> Module { + let mut module = Module::new_builtin(); + map::declare(&mut module); + module +} + +pub(crate) fn declare_module_math() -> Module { + let mut module = Module::new_builtin(); + math::declare(&mut module); + module +} + +pub(crate) fn declare_module_meta() -> Module { + let mut module = Module::new_builtin(); + meta::declare(&mut module); + module +} + +pub(crate) fn declare_module_selector() -> Module { + let mut module = Module::new_builtin(); + selector::declare(&mut module); + module +} + +pub(crate) fn declare_module_string() -> Module { + let mut module = Module::new_builtin(); + string::declare(&mut module); + module +} diff --git a/src/builtin/modules/selector.rs b/src/builtin/modules/selector.rs new file mode 100644 index 0000000..3181254 --- /dev/null +++ b/src/builtin/modules/selector.rs @@ -0,0 +1,18 @@ +use crate::{ + builtin::modules::Module, + builtin::selector::{ + is_superselector, selector_append, selector_extend, selector_nest, selector_parse, + selector_replace, selector_unify, simple_selectors, + }, +}; + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("is-superselector", is_superselector); + f.insert_builtin("append", selector_append); + f.insert_builtin("extend", selector_extend); + f.insert_builtin("nest", selector_nest); + f.insert_builtin("parse", selector_parse); + f.insert_builtin("replace", selector_replace); + f.insert_builtin("unify", selector_unify); + f.insert_builtin("simple-selectors", simple_selectors); +} diff --git a/src/builtin/modules/string.rs b/src/builtin/modules/string.rs new file mode 100644 index 0000000..bd1b2cf --- /dev/null +++ b/src/builtin/modules/string.rs @@ -0,0 +1,22 @@ +use crate::builtin::{ + modules::Module, + string::{ + quote, str_index, str_insert, str_length, str_slice, to_lower_case, to_upper_case, unquote, + }, +}; + +#[cfg(feature = "random")] +use crate::builtin::string::unique_id; + +pub(crate) fn declare(f: &mut Module) { + f.insert_builtin("quote", quote); + f.insert_builtin("index", str_index); + f.insert_builtin("insert", str_insert); + f.insert_builtin("length", str_length); + f.insert_builtin("slice", str_slice); + f.insert_builtin("to-lower-case", to_lower_case); + f.insert_builtin("to-upper-case", to_upper_case); + #[cfg(feature = "random")] + f.insert_builtin("unique-id", unique_id); + f.insert_builtin("unquote", unquote); +} diff --git a/src/color/mod.rs b/src/color/mod.rs index ea97721..38db906 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -264,7 +264,7 @@ impl Color { return h.saturation() * Number::from(100); } - let red = self.red() / Number::from(255); + let red: Number = self.red() / Number::from(255); let green = self.green() / Number::from(255); let blue = self.blue() / Number::from(255); @@ -291,7 +291,7 @@ impl Color { return h.luminance() * Number::from(100); } - let red = self.red() / Number::from(255); + let red: Number = self.red() / Number::from(255); let green = self.green() / Number::from(255); let blue = self.blue() / Number::from(255); let min = min(&red, min(&green, &blue)).clone(); diff --git a/src/lib.rs b/src/lib.rs index 61cd1e2..4f29459 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,11 +1,11 @@ /*! # grass An implementation of the Sass specification in pure rust. -Spec progress as of 2020-07-24: +Spec progress as of 2020-08-07: | Passing | Failing | Total | |---------|---------|-------| -| 2935 | 2158 | 5093 | +| 3375 | 1718 | 5093 | ## Use as library ``` @@ -50,6 +50,7 @@ grass input.scss clippy::unknown_clippy_lints, clippy::replace_consts, clippy::single_match, + clippy::float_arithmetic, // temporarily allowed while under heavy development. // eventually these allows should be refactored away @@ -94,6 +95,7 @@ use peekmore::PeekMore; pub use crate::error::{SassError as Error, SassResult as Result}; pub(crate) use crate::token::Token; use crate::{ + builtin::modules::{ModuleConfig, Modules}, lexer::Lexer, output::Css, parse::{ @@ -292,6 +294,8 @@ pub fn from_path(p: &str, options: &Options) -> Result { extender: &mut Extender::new(empty_span), content_scopes: &mut Scopes::new(), options, + modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; @@ -336,6 +340,8 @@ pub fn from_string(p: String, options: &Options) -> Result { extender: &mut Extender::new(empty_span), content_scopes: &mut Scopes::new(), options, + modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; @@ -371,6 +377,8 @@ pub fn from_string(p: String) -> std::result::Result { extender: &mut Extender::new(empty_span), content_scopes: &mut Scopes::new(), options: &Options::default(), + modules: &mut Modules::default(), + module_config: &mut ModuleConfig::default(), } .parse() .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?; diff --git a/src/parse/args.rs b/src/parse/args.rs index d8875d2..8bb3e9d 100644 --- a/src/parse/args.rs +++ b/src/parse/args.rs @@ -1,6 +1,6 @@ use std::{collections::HashMap, mem}; -use codemap::{Span, Spanned}; +use codemap::Span; use crate::{ args::{CallArg, CallArgs, FuncArg, FuncArgs}, @@ -72,19 +72,12 @@ impl<'a> Parser<'a> { } } '.' => { - let next = self.toks.next().ok_or(("expected \".\".", span))?; - if next.kind != '.' { - return Err(("expected \".\".", next.pos()).into()); - } - let next = self.toks.next().ok_or(("expected \".\".", next.pos()))?; - if next.kind != '.' { - return Err(("expected \".\".", next.pos()).into()); - } + self.expect_char('.')?; + self.expect_char('.')?; + self.whitespace_or_comment(); - let next = self.toks.next().ok_or(("expected \")\".", next.pos()))?; - if next.kind != ')' { - return Err(("expected \")\".", next.pos()).into()); - } + + self.expect_char(')')?; is_variadic = true; @@ -119,6 +112,7 @@ impl<'a> Parser<'a> { } self.whitespace_or_comment(); // TODO: this should NOT eat the opening curly brace + // todo: self.expect_char('{')?; match self.toks.next() { Some(v) if v.kind == '{' => {} Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()), @@ -225,11 +219,7 @@ impl<'a> Parser<'a> { return Err(("expected \")\".", pos).into()); } self.toks.next(); - if let Some(Token { kind: '.', .. }) = self.toks.peek() { - self.toks.next(); - } else { - return Err(("expected \".\".", pos).into()); - } + self.expect_char('.')?; } else { return Err(("expected \")\".", pos).into()); } @@ -323,23 +313,16 @@ impl<'a> Parser<'a> { self.whitespace_or_comment(); continue; } - Some(Token { kind: '.', pos }) => { - let pos = *pos; + Some(Token { kind: '.', .. }) => { self.toks.next(); - if let Some(Token { kind: '.', pos }) = self.toks.peek().cloned() { - if !name.is_empty() { - return Err(("expected \")\".", pos).into()); - } - self.toks.next(); - if let Some(Token { kind: '.', .. }) = self.toks.peek() { - self.toks.next(); - } else { - return Err(("expected \".\".", pos).into()); - } - } else { - return Err(("expected \")\".", pos).into()); + self.expect_char('.')?; + + if !name.is_empty() { + return Err(("expected \")\".", self.span_before).into()); } + + self.expect_char('.')?; } Some(Token { pos, .. }) => { return Err(("expected \")\".", *pos).into()); @@ -367,15 +350,8 @@ impl<'a> Parser<'a> { self.scopes.enter_new_scope(); for (idx, mut arg) in fn_args.0.into_iter().enumerate() { if arg.is_variadic { - let span = args.span(); let arg_list = Value::ArgList(args.get_variadic()?); - scope.insert_var( - arg.name, - Spanned { - node: arg_list, - span, - }, - ); + scope.insert_var(arg.name, arg_list); break; } let val = match args.get(idx, arg.name) { @@ -388,7 +364,8 @@ impl<'a> Parser<'a> { ) } }, - }?; + }? + .node; self.scopes.insert_var(arg.name, val.clone()); scope.insert_var(arg.name, val); } diff --git a/src/parse/common.rs b/src/parse/common.rs index fa724b8..d23514a 100644 --- a/src/parse/common.rs +++ b/src/parse/common.rs @@ -2,7 +2,7 @@ use std::ops::{BitAnd, BitOr}; use codemap::Spanned; -use crate::{interner::InternedString, value::Value}; +use crate::{common::Identifier, interner::InternedString, value::Value}; #[derive(Debug, Clone)] pub(crate) struct NeverEmptyVec { @@ -42,6 +42,7 @@ impl NeverEmptyVec { pub(super) enum SelectorOrStyle { Selector(String), Style(InternedString, Option>>), + ModuleVariableRedeclaration(Identifier), } #[derive(Debug, Copy, Clone)] diff --git a/src/parse/control_flow.rs b/src/parse/control_flow.rs index 4902ab2..100cb3a 100644 --- a/src/parse/control_flow.rs +++ b/src/parse/control_flow.rs @@ -24,14 +24,10 @@ impl<'a> Parser<'a> { let init_cond = self.parse_value(true, &|_| false)?.node; - // consume the open curly brace - let span_before = match self.toks.next() { - Some(Token { kind: '{', pos }) => pos, - Some(..) | None => return Err(("expected \"{\".", self.span_before).into()), - }; + self.expect_char('{')?; if self.toks.peek().is_none() { - return Err(("expected \"}\".", span_before).into()); + return Err(("expected \"}\".", self.span_before).into()); } self.whitespace_or_comment(); @@ -53,6 +49,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; } else { @@ -87,12 +85,7 @@ impl<'a> Parser<'a> { false } else { let v = self.parse_value(true, &|_| false)?.node.is_true(); - match self.toks.next() { - Some(Token { kind: '{', .. }) => {} - Some(..) | None => { - return Err(("expected \"{\".", self.span_before).into()) - } - } + self.expect_char('{')?; v }; if cond { @@ -112,6 +105,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; } else { @@ -140,6 +135,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_stmt(); } @@ -158,17 +155,15 @@ impl<'a> Parser<'a> { } pub(super) fn parse_for(&mut self) -> SassResult> { + // todo: whitespace or comment self.whitespace(); - let next = self - .toks - .next() - .ok_or(("expected \"$\".", self.span_before))?; - let var: Spanned = match next.kind { - '$' => self - .parse_identifier_no_interpolation(false)? - .map_node(|i| i.into()), - _ => return Err(("expected \"$\".", self.span_before).into()), - }; + // todo: test for error here + self.expect_char('$')?; + + let var = self + .parse_identifier_no_interpolation(false)? + .map_node(|n| n.into()); + self.whitespace(); self.span_before = match self.toks.peek() { Some(tok) => tok.pos, @@ -242,10 +237,11 @@ impl<'a> Parser<'a> { self.whitespace(); let from_val = self.parse_value_from_vec(from_toks, true)?; let from = match from_val.node { - Value::Dimension(n, ..) => match n.to_integer().to_isize() { + Value::Dimension(Some(n), ..) => match n.to_integer().to_isize() { Some(v) => v, None => return Err((format!("{} is not a int.", n), from_val.span).into()), }, + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("{} is not an integer.", v.inspect(from_val.span)?), @@ -257,10 +253,11 @@ impl<'a> Parser<'a> { let to_val = self.parse_value(true, &|_| false)?; let to = match to_val.node { - Value::Dimension(n, ..) => match n.to_integer().to_isize() { + Value::Dimension(Some(n), ..) => match n.to_integer().to_isize() { Some(v) => v, None => return Err((format!("{} is not a int.", n), to_val.span).into()), }, + Value::Dimension(None, ..) => todo!(), v => { return Err(( format!("{} is not an integer.", v.to_css_string(to_val.span)?), @@ -270,11 +267,7 @@ impl<'a> Parser<'a> { } }; - // consume the open curly brace - match self.toks.next() { - Some(Token { kind: '{', pos }) => pos, - Some(..) | None => return Err(("expected \"{\".", to_val.span).into()), - }; + self.expect_char('{')?; let body = read_until_closing_curly_brace(self.toks)?; self.toks.next(); @@ -299,10 +292,7 @@ impl<'a> Parser<'a> { for i in iter { self.scopes.insert_var_last( var.node, - Spanned { - node: Value::Dimension(Number::from(i), Unit::None, true), - span: var.span, - }, + Value::Dimension(Some(Number::from(i)), Unit::None, true), ); if self.flags.in_function() { let these_stmts = Parser { @@ -320,8 +310,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?; + .parse_stmt()?; if !these_stmts.is_empty() { return Ok(these_stmts); } @@ -342,8 +334,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?, + .parse_stmt()?, ); } } @@ -392,8 +386,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?; + .parse_stmt()?; if !these_stmts.is_empty() { return Ok(these_stmts); } @@ -414,8 +410,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?, + .parse_stmt()?, ); } val = self.parse_value_from_vec(cond.clone(), true)?; @@ -430,15 +428,11 @@ impl<'a> Parser<'a> { let mut vars: Vec> = Vec::new(); loop { - let next = self - .toks - .next() - .ok_or(("expected \"$\".", self.span_before))?; + self.expect_char('$')?; - match next.kind { - '$' => vars.push(self.parse_identifier()?.map_node(|i| i.into())), - _ => return Err(("expected \"$\".", next.pos()).into()), - } + vars.push(self.parse_identifier()?.map_node(|i| i.into())); + + // todo: whitespace or comment self.whitespace(); if self .toks @@ -478,26 +472,14 @@ impl<'a> Parser<'a> { for row in iter { if vars.len() == 1 { - self.scopes.insert_var_last( - vars[0].node, - Spanned { - node: row, - span: vars[0].span, - }, - ); + self.scopes.insert_var_last(vars[0].node, row); } else { for (var, val) in vars.iter().zip( row.as_list() .into_iter() .chain(std::iter::once(Value::Null).cycle()), ) { - self.scopes.insert_var_last( - var.node, - Spanned { - node: val, - span: var.span, - }, - ); + self.scopes.insert_var_last(var.node, val); } } @@ -517,8 +499,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?; + .parse_stmt()?; if !these_stmts.is_empty() { return Ok(these_stmts); } @@ -539,8 +523,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?, + .parse_stmt()?, ); } } diff --git a/src/parse/function.rs b/src/parse/function.rs index 4560abe..5b02227 100644 --- a/src/parse/function.rs +++ b/src/parse/function.rs @@ -4,11 +4,11 @@ use peekmore::PeekMore; use crate::{ args::CallArgs, atrule::Function, - common::unvendor, + common::{unvendor, Identifier}, error::SassResult, scope::Scopes, utils::{read_until_closing_curly_brace, read_until_semicolon_or_closing_curly_brace}, - value::Value, + value::{SassFunction, Value}, Token, }; @@ -40,11 +40,9 @@ impl<'a> Parser<'a> { } self.whitespace_or_comment(); - let args = match self.toks.next() { - Some(Token { kind: '(', .. }) => self.parse_func_args()?, - Some(Token { pos, .. }) => return Err(("expected \"(\".", pos).into()), - None => return Err(("expected \"(\".", span).into()), - }; + self.expect_char('(')?; + + let args = self.parse_func_args()?; self.whitespace(); @@ -57,10 +55,18 @@ impl<'a> Parser<'a> { let function = Function::new(args, body, self.at_root, span); + let name_as_ident = Identifier::from(name); + if self.at_root { - self.global_scope.insert_fn(name, function); + self.global_scope.insert_fn( + name_as_ident, + SassFunction::UserDefined(Box::new(function), name_as_ident), + ); } else { - self.scopes.insert_fn(name.into(), function); + self.scopes.insert_fn( + name_as_ident, + SassFunction::UserDefined(Box::new(function), name_as_ident), + ); } Ok(()) } @@ -112,8 +118,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?; + .parse_stmt()?; if entered_scope { self.scopes.exit_scope(); diff --git a/src/parse/import.rs b/src/parse/import.rs index ce6d918..cd17e8c 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -13,60 +13,13 @@ use crate::{ use super::{Parser, Stmt}; -/// Searches the current directory of the file then searches in `load_paths` directories -/// if the import has not yet been found. -/// -/// -fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Option { - let paths = [ - file_path.with_file_name(name).with_extension("scss"), - file_path - .with_file_name(format!("_{}", name.to_str().unwrap())) - .with_extension("scss"), - file_path.clone(), - file_path.join("index.scss"), - file_path.join("_index.scss"), - ]; - - for name in &paths { - if name.is_file() { - return Some(name.to_path_buf()); - } - } - - for path in load_paths { - let paths: Vec = if path.is_dir() { - vec![ - path.join(format!("{}.scss", name.to_str().unwrap())), - path.join(format!("_{}.scss", name.to_str().unwrap())), - path.join("index.scss"), - path.join("_index.scss"), - ] - } else { - vec![ - path.to_path_buf(), - path.with_file_name(name).with_extension("scss"), - path.with_file_name(format!("_{}", name.to_str().unwrap())) - .with_extension("scss"), - path.join("index.scss"), - path.join("_index.scss"), - ] - }; - - for name in paths { - if name.is_file() { - return Some(name); - } - } - } - - None -} - impl<'a> Parser<'a> { - fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult> { - let path: &Path = file_name.as_ref(); - + /// Searches the current directory of the file then searches in `load_paths` directories + /// if the import has not yet been found. + /// + /// + /// + pub(super) fn find_import(&self, path: &Path) -> Option { let path_buf = if path.is_absolute() { // todo: test for absolute path imports path.into() @@ -79,7 +32,59 @@ impl<'a> Parser<'a> { let name = path_buf.file_name().unwrap_or_else(|| OsStr::new("..")); - if let Some(name) = find_import(&path_buf, name, &self.options.load_paths) { + let paths = [ + path_buf.with_file_name(name).with_extension("scss"), + path_buf + .with_file_name(format!("_{}", name.to_str().unwrap())) + .with_extension("scss"), + path_buf.clone(), + path_buf.join("index.scss"), + path_buf.join("_index.scss"), + ]; + + for name in &paths { + if name.is_file() { + return Some(name.to_path_buf()); + } + } + + for path in &self.options.load_paths { + let paths: Vec = if path.is_dir() { + vec![ + path.join(format!("{}.scss", name.to_str().unwrap())), + path.join(format!("_{}.scss", name.to_str().unwrap())), + path.join("index.scss"), + path.join("_index.scss"), + ] + } else { + vec![ + path.to_path_buf(), + path.with_file_name(name).with_extension("scss"), + path.with_file_name(format!("_{}", name.to_str().unwrap())) + .with_extension("scss"), + path.join("index.scss"), + path.join("_index.scss"), + ] + }; + + for name in paths { + if name.is_file() { + return Some(name); + } + } + } + + None + } + + pub(crate) fn parse_single_import( + &mut self, + file_name: &str, + span: Span, + ) -> SassResult> { + let path: &Path = file_name.as_ref(); + + if let Some(name) = self.find_import(path) { let file = self.map.add_file( name.to_string_lossy().into(), String::from_utf8(fs::read(&name)?)?, @@ -102,10 +107,11 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse(); } - self.whitespace(); Err(("Can't find stylesheet to import.", span).into()) } diff --git a/src/parse/keyframes.rs b/src/parse/keyframes.rs index c02092f..b082d03 100644 --- a/src/parse/keyframes.rs +++ b/src/parse/keyframes.rs @@ -63,9 +63,8 @@ impl<'a, 'b> KeyframesSelectorParser<'a, 'b> { num.push_str(&eat_whole_number(self.parser.toks)); } - if !matches!(self.parser.toks.next(), Some(Token { kind: '%', .. })) { - return Err(("expected \"%\".", tok.pos).into()); - } + self.parser.expect_char('%')?; + selectors.push(KeyframesSelector::Percent(num.into_boxed_str())); } '{' => break, @@ -173,6 +172,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, }) .parse_keyframes_selector()?; @@ -208,6 +209,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_stmt()?; diff --git a/src/parse/media.rs b/src/parse/media.rs index 2a0c036..61f124a 100644 --- a/src/parse/media.rs +++ b/src/parse/media.rs @@ -25,16 +25,6 @@ impl<'a> Parser<'a> { Ok(false) } - pub fn expect_char(&mut self, c: char) -> SassResult<()> { - if let Some(Token { kind, .. }) = self.toks.peek() { - if *kind == c { - self.toks.next(); - return Ok(()); - } - } - Err((format!("expected \"{}\".", c), self.span_before).into()) - } - pub fn scan_char(&mut self, c: char) -> bool { if let Some(Token { kind, .. }) = self.toks.peek() { if *kind == c { diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 7b55003..f0764b7 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -6,7 +6,7 @@ use peekmore::PeekMore; use crate::{ args::{CallArgs, FuncArgs}, - atrule::{Content, Mixin}, + atrule::mixin::{Content, Mixin, UserDefinedMixin}, error::SassResult, scope::Scopes, utils::read_until_closing_curly_brace, @@ -55,7 +55,7 @@ impl<'a> Parser<'a> { // this is blocked on figuring out just how to check for this. presumably we could have a check // not when parsing initially, but rather when `@include`ing to see if an `@content` was found. - let mixin = Mixin::new(args, body, false, self.at_root); + let mixin = Mixin::new_user_defined(args, body, false, self.at_root); if self.at_root { self.global_scope.insert_mixin(name, mixin); @@ -73,6 +73,19 @@ impl<'a> Parser<'a> { self.whitespace_or_comment(); let name = self.parse_identifier()?.map_node(Into::into); + let mixin = if let Some(Token { kind: '.', .. }) = self.toks.peek() { + self.toks.next(); + + let module = name; + let name = self.parse_identifier()?.map_node(Into::into); + + self.modules + .get(module.node, module.span)? + .get_mixin(name)? + } else { + self.scopes.get_mixin(name, self.global_scope)? + }; + self.whitespace_or_comment(); let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() { @@ -91,9 +104,7 @@ impl<'a> Parser<'a> { ident.node.make_ascii_lowercase(); if ident.node == "using" { self.whitespace_or_comment(); - if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) { - return Err(("expected \"(\".", ident.span).into()); - } + self.expect_char('(')?; Some(self.parse_func_args()?) } else { @@ -125,12 +136,17 @@ impl<'a> Parser<'a> { self.toks.next(); } - let Mixin { + let UserDefinedMixin { body, args: fn_args, declared_at_root, .. - } = self.scopes.get_mixin(name, self.global_scope)?; + } = match mixin { + Mixin::UserDefined(u) => u, + Mixin::Builtin(b) => { + return b(args, self); + } + }; let scope = self.eval_args(fn_args, args)?; @@ -164,8 +180,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()?; + .parse_stmt()?; self.content.pop(); self.scopes.exit_scope(); @@ -225,8 +243,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()? + .parse_stmt()? } else { Vec::new() }; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index de5d5ee..13c8a5e 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -7,8 +7,10 @@ use crate::{ atrule::{ keyframes::{Keyframes, KeyframesRuleSet}, media::MediaRule, - AtRuleKind, Content, SupportsRule, UnknownAtRule, + mixin::Content, + AtRuleKind, SupportsRule, UnknownAtRule, }, + builtin::modules::{ModuleConfig, Modules}, error::SassResult, scope::{Scope, Scopes}, selector::{ @@ -25,6 +27,7 @@ use crate::{ use common::{Comment, ContextFlags, NeverEmptyVec, SelectorOrStyle}; pub(crate) use value::{HigherIntermediateValue, ValueVisitor}; +use variable::VariableValue; mod args; pub mod common; @@ -35,6 +38,7 @@ mod import; mod keyframes; mod media; mod mixin; +mod module; mod style; mod throw_away; mod value; @@ -86,11 +90,18 @@ pub(crate) struct Parser<'a> { pub extender: &'a mut Extender, pub options: &'a Options<'a>, + + pub modules: &'a mut Modules, + pub module_config: &'a mut ModuleConfig, } impl<'a> Parser<'a> { pub fn parse(&mut self) -> SassResult> { let mut stmts = Vec::new(); + + self.whitespace(); + stmts.append(&mut self.load_modules()?); + while self.toks.peek().is_some() { stmts.append(&mut self.parse_stmt()?); if self.flags.in_function() && !stmts.is_empty() { @@ -101,6 +112,26 @@ impl<'a> Parser<'a> { Ok(stmts) } + pub fn expect_char(&mut self, c: char) -> SassResult<()> { + match self.toks.peek() { + Some(Token { kind, pos }) if *kind == c => { + self.span_before = *pos; + self.toks.next(); + Ok(()) + } + Some(Token { pos, .. }) => Err((format!("expected \"{}\".", c), *pos).into()), + None => Err((format!("expected \"{}\".", c), self.span_before).into()), + } + } + + pub fn consume_char_if_exists(&mut self, c: char) { + if let Some(Token { kind, .. }) = self.toks.peek() { + if *kind == c { + self.toks.next(); + } + } + } + fn parse_stmt(&mut self) -> SassResult> { let mut stmts = Vec::new(); while let Some(Token { kind, pos }) = self.toks.peek() { @@ -212,7 +243,13 @@ impl<'a> Parser<'a> { AtRuleKind::Unknown(_) => { stmts.push(self.parse_unknown_at_rule(kind_string.node)?) } - AtRuleKind::Use => todo!("@use not yet implemented"), + AtRuleKind::Use => { + return Err(( + "@use rules must be written before any other rules.", + kind_string.span, + ) + .into()) + } AtRuleKind::Forward => todo!("@forward not yet implemented"), AtRuleKind::Extend => self.parse_extend()?, AtRuleKind::Supports => stmts.push(self.parse_supports()?), @@ -258,6 +295,9 @@ impl<'a> Parser<'a> { } if self.flags.in_keyframes() { match self.is_selector_or_style()? { + SelectorOrStyle::ModuleVariableRedeclaration(module) => { + self.parse_module_variable_redeclaration(module)? + } SelectorOrStyle::Style(property, value) => { if let Some(value) = value { stmts.push(Stmt::Style(Style { property, value })); @@ -285,6 +325,9 @@ impl<'a> Parser<'a> { } match self.is_selector_or_style()? { + SelectorOrStyle::ModuleVariableRedeclaration(module) => { + self.parse_module_variable_redeclaration(module)? + } SelectorOrStyle::Style(property, value) => { if let Some(value) = value { stmts.push(Stmt::Style(Style { property, value })); @@ -412,6 +455,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, }, allows_parent, true, @@ -470,10 +515,11 @@ impl<'a> Parser<'a> { pub fn parse_interpolation(&mut self) -> SassResult> { let val = self.parse_value(true, &|_| false)?; - match self.toks.next() { - Some(Token { kind: '}', .. }) => {} - Some(..) | None => return Err(("expected \"}\".", val.span).into()), - } + + self.span_before = val.span; + + self.expect_char('}')?; + Ok(val.map_node(Value::unquote)) } @@ -640,9 +686,7 @@ impl<'a> Parser<'a> { self.whitespace(); - if !matches!(self.toks.next(), Some(Token { kind: '{', .. })) { - return Err(("expected \"{\".", self.span_before).into()); - } + self.expect_char('{')?; let raw_body = self.parse_stmt()?; @@ -711,8 +755,10 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } - .parse()? + .parse_stmt()? .into_iter() .filter_map(|s| match s { Stmt::Style(..) => { @@ -755,14 +801,15 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_selector(false, true, String::new())?; + // todo: this might be superfluous self.whitespace(); - if let Some(Token { kind: ';', .. }) = self.toks.peek() { - self.toks.next(); - } + self.consume_char_if_exists(';'); let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); diff --git a/src/parse/module.rs b/src/parse/module.rs new file mode 100644 index 0000000..f6a056b --- /dev/null +++ b/src/parse/module.rs @@ -0,0 +1,291 @@ +use std::{convert::TryFrom, fs}; + +use codemap::Spanned; +use peekmore::PeekMore; + +use crate::{ + atrule::AtRuleKind, + builtin::modules::{ + declare_module_color, declare_module_list, declare_module_map, declare_module_math, + declare_module_meta, declare_module_selector, declare_module_string, Module, ModuleConfig, + }, + common::Identifier, + error::SassResult, + lexer::Lexer, + parse::{common::Comment, Parser, Stmt, VariableValue}, + scope::Scope, + utils::peek_ident_no_interpolation, + Token, +}; + +impl<'a> Parser<'a> { + fn parse_module_alias(&mut self) -> SassResult> { + if let Some(Token { kind: 'a', .. }) | Some(Token { kind: 'A', .. }) = self.toks.peek() { + let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; + ident.node.make_ascii_lowercase(); + if ident.node != "as" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + if let Some(Token { kind: '*', .. }) = self.toks.peek() { + self.toks.next(); + return Ok(Some('*'.to_string())); + } else { + let name = self.parse_identifier_no_interpolation(false)?; + + return Ok(Some(name.node)); + } + } + + Ok(None) + } + + fn parse_module_config(&mut self) -> SassResult { + let mut config = ModuleConfig::default(); + + if let Some(Token { kind: 'w', .. }) | Some(Token { kind: 'W', .. }) = self.toks.peek() { + let mut ident = peek_ident_no_interpolation(self.toks, false, self.span_before)?; + ident.node.make_ascii_lowercase(); + if ident.node != "with" { + return Err(("expected \";\".", ident.span).into()); + } + + self.whitespace_or_comment(); + + self.span_before = ident.span; + + self.expect_char('(')?; + + loop { + self.whitespace_or_comment(); + self.expect_char('$')?; + + let name = self.parse_identifier_no_interpolation(false)?; + + self.whitespace_or_comment(); + self.expect_char(':')?; + self.whitespace_or_comment(); + + let value = self.parse_value(false, &|toks| match toks.peek() { + Some(Token { kind: ',', .. }) | Some(Token { kind: ')', .. }) => true, + _ => false, + })?; + + config.insert(name.map_node(|n| n.into()), value)?; + + match self.toks.next() { + Some(Token { kind: ',', .. }) => { + continue; + } + Some(Token { kind: ')', .. }) => { + break; + } + Some(..) | None => { + return Err(("expected \")\".", self.span_before).into()); + } + } + } + } + + Ok(config) + } + + pub fn load_module( + &mut self, + name: &str, + config: &mut ModuleConfig, + ) -> SassResult<(Module, Vec)> { + Ok(match name { + "sass:color" => (declare_module_color(), Vec::new()), + "sass:list" => (declare_module_list(), Vec::new()), + "sass:map" => (declare_module_map(), Vec::new()), + "sass:math" => (declare_module_math(), Vec::new()), + "sass:meta" => (declare_module_meta(), Vec::new()), + "sass:selector" => (declare_module_selector(), Vec::new()), + "sass:string" => (declare_module_string(), Vec::new()), + _ => { + if let Some(import) = self.find_import(name.as_ref()) { + let mut global_scope = Scope::new(); + + let file = self + .map + .add_file(name.to_owned(), String::from_utf8(fs::read(&import)?)?); + + let stmts = Parser { + toks: &mut Lexer::new(&file) + .collect::>() + .into_iter() + .peekmore(), + map: self.map, + path: &import, + scopes: self.scopes, + global_scope: &mut global_scope, + super_selectors: self.super_selectors, + span_before: file.span.subspan(0, 0), + content: self.content, + flags: self.flags, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + content_scopes: self.content_scopes, + options: self.options, + modules: self.modules, + module_config: config, + } + .parse()?; + + if !config.is_empty() { + return Err(( + "This variable was not declared with !default in the @used module.", + self.span_before, + ) + .into()); + } + + (Module::new_from_scope(global_scope, false), stmts) + } else { + return Err(("Can't find stylesheet to import.", self.span_before).into()); + } + } + }) + } + + /// Returns any multiline comments that may have been found + /// while loading modules + pub(super) fn load_modules(&mut self) -> SassResult> { + let mut comments = Vec::new(); + + loop { + self.whitespace(); + match self.toks.peek() { + Some(Token { kind: '@', .. }) => { + self.toks.advance_cursor(); + + if let Some(Token { kind, .. }) = self.toks.peek() { + if !matches!(kind, 'a'..='z' | 'A'..='Z' | '\\') { + break; + } + } + + match AtRuleKind::try_from(&peek_ident_no_interpolation( + self.toks, + false, + self.span_before, + )?)? { + AtRuleKind::Use => { + self.toks.truncate_iterator_to_cursor(); + } + _ => { + break; + } + } + + self.whitespace_or_comment(); + + let quote = match self.toks.next() { + Some(Token { kind: q @ '"', .. }) | Some(Token { kind: q @ '\'', .. }) => q, + Some(..) | None => todo!(), + }; + + let Spanned { node: module, span } = self.parse_quoted_string(quote)?; + let module_name = module.unquote().to_css_string(span)?; + + self.whitespace_or_comment(); + + let module_alias = self.parse_module_alias()?; + + self.whitespace_or_comment(); + + let mut config = self.parse_module_config()?; + + self.whitespace_or_comment(); + self.expect_char(';')?; + + let (module, mut stmts) = + self.load_module(module_name.as_ref(), &mut config)?; + + comments.append(&mut stmts); + + // if the config isn't empty here, that means + // variables were passed to a builtin module + if !config.is_empty() { + return Err(("Built-in modules can't be configured.", span).into()); + } + + let module_name = match module_alias.as_deref() { + Some("*") => { + self.global_scope.merge_module(module); + continue; + } + Some(..) => module_alias.unwrap(), + None => match module_name.as_ref() { + "sass:color" => "color".to_owned(), + "sass:list" => "list".to_owned(), + "sass:map" => "map".to_owned(), + "sass:math" => "math".to_owned(), + "sass:meta" => "meta".to_owned(), + "sass:selector" => "selector".to_owned(), + "sass:string" => "string".to_owned(), + _ => module_name.into_owned(), + }, + }; + + self.modules.insert(module_name.into(), module, span)?; + } + Some(Token { kind: '/', .. }) => { + self.toks.next(); + match self.parse_comment()?.node { + Comment::Silent => continue, + Comment::Loud(s) => comments.push(Stmt::Comment(s)), + } + } + Some(Token { kind: '$', .. }) => self.parse_variable_declaration()?, + Some(..) | None => break, + } + } + + self.toks.reset_cursor(); + + Ok(comments) + } + + pub(super) fn parse_module_variable_redeclaration( + &mut self, + module: Identifier, + ) -> SassResult<()> { + let variable = self + .parse_identifier_no_interpolation(false)? + .map_node(|n| n.into()); + + self.whitespace_or_comment(); + self.expect_char(':')?; + + let VariableValue { + val_toks, + global, + default, + } = self.parse_variable_value()?; + + if global { + return Err(( + "!global isn't allowed for variables in other modules.", + variable.span, + ) + .into()); + } + + if default { + return Ok(()); + } + + let value = self.parse_value_from_vec(val_toks, true)?; + + self.modules + .get_mut(module, variable.span)? + .update_var(variable, value.node)?; + + Ok(()) + } +} diff --git a/src/parse/style.rs b/src/parse/style.rs index 0c40a66..0e10bd1 100644 --- a/src/parse/style.rs +++ b/src/parse/style.rs @@ -111,43 +111,62 @@ impl<'a> Parser<'a> { let mut property = self.parse_identifier()?.node; let whitespace_after_property = self.whitespace(); - if let Some(Token { kind: ':', .. }) = self.toks.peek() { - self.toks.next(); - if let Some(Token { kind, .. }) = self.toks.peek() { - return Ok(match kind { - ':' => { - if whitespace_after_property { - property.push(' '); - } - property.push(':'); - SelectorOrStyle::Selector(property) - } - c if is_name(*c) => { - if let Some(toks) = self.parse_style_value_when_no_space_after_semicolon() { - let len = toks.len(); - if let Ok(val) = self.parse_value_from_vec(toks, false) { - self.toks.take(len).for_each(drop); - return Ok(SelectorOrStyle::Style( - InternedString::get_or_intern(property), - Some(Box::new(val)), - )); + match self.toks.peek() { + Some(Token { kind: ':', .. }) => { + self.toks.next(); + if let Some(Token { kind, .. }) = self.toks.peek() { + return Ok(match kind { + ':' => { + if whitespace_after_property { + property.push(' '); } + property.push(':'); + SelectorOrStyle::Selector(property) } + c if is_name(*c) => { + if let Some(toks) = + self.parse_style_value_when_no_space_after_semicolon() + { + let len = toks.len(); + if let Ok(val) = self.parse_value_from_vec(toks, false) { + self.toks.take(len).for_each(drop); + return Ok(SelectorOrStyle::Style( + InternedString::get_or_intern(property), + Some(Box::new(val)), + )); + } + } - if whitespace_after_property { - property.push(' '); + if whitespace_after_property { + property.push(' '); + } + property.push(':'); + return Ok(SelectorOrStyle::Selector(property)); } - property.push(':'); - return Ok(SelectorOrStyle::Selector(property)); + _ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None), + }); + } + } + Some(Token { kind: '.', .. }) => { + if matches!(self.toks.peek_next(), Some(Token { kind: '$', .. })) { + self.toks.next(); + self.toks.next(); + return Ok(SelectorOrStyle::ModuleVariableRedeclaration( + property.into(), + )); + } else { + if whitespace_after_property { + property.push(' '); } - _ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None), - }); + return Ok(SelectorOrStyle::Selector(property)); + } } - } else { - if whitespace_after_property { - property.push(' '); + _ => { + if whitespace_after_property { + property.push(' '); + } + return Ok(SelectorOrStyle::Selector(property)); } - return Ok(SelectorOrStyle::Selector(property)); } Err(("expected \"{\".", self.span_before).into()) } diff --git a/src/parse/value/eval.rs b/src/parse/value/eval.rs index 94c5101..acb9ef3 100644 --- a/src/parse/value/eval.rs +++ b/src/parse/value/eval.rs @@ -3,12 +3,13 @@ use std::cmp::Ordering; use codemap::{Span, Spanned}; +use num_traits::Zero; use crate::{ args::CallArgs, common::{Op, QuoteKind}, error::SassResult, - unit::{Unit, UNIT_CONVERSION_TABLE}, + unit::Unit, value::{SassFunction, Value}, }; @@ -119,7 +120,11 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { fn unary_minus(&self, val: Value) -> SassResult { Ok(match val { - Value::Dimension(n, u, should_divide) => Value::Dimension(-n, u, should_divide), + Value::Dimension(Some(n), u, should_divide) => { + Value::Dimension(Some(-n), u, should_divide) + } + // todo: NaN test + Value::Dimension(None, u, should_divide) => Value::Dimension(None, u, should_divide), v => Value::String(format!("-{}", v.to_css_string(self.span)?), QuoteKind::None), }) } @@ -205,8 +210,10 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { QuoteKind::None, ), }, - Value::Dimension(num, unit, _) => match right { - Value::Dimension(num2, unit2, _) => { + v @ Value::Dimension(None, ..) => v, + Value::Dimension(Some(num), unit, _) => match right { + v @ Value::Dimension(None, ..) => v, + Value::Dimension(Some(num2), unit2, _) => { if !unit.comparable(&unit2) { return Err(( format!("Incompatible units {} and {}.", unit2, unit), @@ -215,20 +222,13 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { .into()); } if unit == unit2 { - Value::Dimension(num + num2, unit, true) + Value::Dimension(Some(num + num2), unit, true) } else if unit == Unit::None { - Value::Dimension(num + num2, unit2, true) + Value::Dimension(Some(num + num2), unit2, true) } else if unit2 == Unit::None { - Value::Dimension(num + num2, unit, true) + Value::Dimension(Some(num + num2), unit, true) } else { - Value::Dimension( - num + num2 - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone(), - unit, - true, - ) + Value::Dimension(Some(num + num2.convert(&unit2, &unit)), unit, true) } } Value::String(s, q) => Value::String(format!("{}{}{}", num, unit, s), q), @@ -314,8 +314,10 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { format!("-{}", right.to_css_string(self.span)?), QuoteKind::None, ), - Value::Dimension(num, unit, _) => match right { - Value::Dimension(num2, unit2, _) => { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num), unit, _) => match right { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num2), unit2, _) => { if !unit.comparable(&unit2) { return Err(( format!("Incompatible units {} and {}.", unit2, unit), @@ -324,20 +326,13 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { .into()); } if unit == unit2 { - Value::Dimension(num - num2, unit, true) + Value::Dimension(Some(num - num2), unit, true) } else if unit == Unit::None { - Value::Dimension(num - num2, unit2, true) + Value::Dimension(Some(num - num2), unit2, true) } else if unit2 == Unit::None { - Value::Dimension(num - num2, unit, true) + Value::Dimension(Some(num - num2), unit, true) } else { - Value::Dimension( - num - num2 - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone(), - unit, - true, - ) + Value::Dimension(Some(num - num2.convert(&unit2, &unit)), unit, true) } } Value::List(..) @@ -434,14 +429,16 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { v => panic!("{:?}", v), }; Ok(match left { - Value::Dimension(num, unit, _) => match right { - Value::Dimension(num2, unit2, _) => { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num), unit, _) => match right { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num2), unit2, _) => { if unit == Unit::None { - Value::Dimension(num * num2, unit2, true) + Value::Dimension(Some(num * num2), unit2, true) } else if unit2 == Unit::None { - Value::Dimension(num * num2, unit, true) + Value::Dimension(Some(num * num2), unit, true) } else { - Value::Dimension(num * num2, unit * unit2, true) + Value::Dimension(Some(num * num2), unit * unit2, true) } } _ => { @@ -490,28 +487,31 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { format!("/{}", right.to_css_string(self.span)?), QuoteKind::None, ), - Value::Dimension(num, unit, should_divide1) => match right { - Value::Dimension(num2, unit2, should_divide2) => { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num), unit, should_divide1) => match right { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num2), unit2, should_divide2) => { if should_divide1 || should_divide2 || in_parens { + if num.is_zero() && num2.is_zero() { + return Ok(Value::Dimension(None, Unit::None, true)); + } + // `unit(1em / 1em)` => `""` if unit == unit2 { - Value::Dimension(num / num2, Unit::None, true) + Value::Dimension(Some(num / num2), Unit::None, true) // `unit(1 / 1em)` => `"em^-1"` } else if unit == Unit::None { - Value::Dimension(num / num2, Unit::None / unit2, true) + Value::Dimension(Some(num / num2), Unit::None / unit2, true) // `unit(1em / 1)` => `"em"` } else if unit2 == Unit::None { - Value::Dimension(num / num2, unit, true) + Value::Dimension(Some(num / num2), unit, true) // `unit(1in / 1px)` => `""` } else if unit.comparable(&unit2) { Value::Dimension( - num / (num2 - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone()), + Some(num / num2.convert(&unit2, &unit)), Unit::None, true, ) @@ -630,28 +630,30 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { v => panic!("{:?}", v), }; Ok(match left { - Value::Dimension(n, u, _) => match right { - Value::Dimension(n2, u2, _) => { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(n), u, _) => match right { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(n2), u2, _) => { if !u.comparable(&u2) { return Err( (format!("Incompatible units {} and {}.", u2, u), self.span).into() ); } if u == u2 { - Value::Dimension(n % n2, u, true) + Value::Dimension(Some(n % n2), u, true) } else if u == Unit::None { - Value::Dimension(n % n2, u2, true) + Value::Dimension(Some(n % n2), u2, true) } else if u2 == Unit::None { - Value::Dimension(n % n2, u, true) + Value::Dimension(Some(n % n2), u, true) } else { - Value::Dimension(n, u, true) + Value::Dimension(Some(n), u, true) } } _ => { return Err(( format!( "Undefined operation \"{} % {}\".", - Value::Dimension(n, u, true).inspect(self.span)?, + Value::Dimension(Some(n), u, true).inspect(self.span)?, right.inspect(self.span)? ), self.span, @@ -735,53 +737,9 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> { HigherIntermediateValue::Literal(v) => v, v => panic!("{:?}", v), }; - let ordering = match left { - Value::Dimension(num, unit, _) => match &right { - Value::Dimension(num2, unit2, _) => { - if !unit.comparable(unit2) { - return Err(( - format!("Incompatible units {} and {}.", unit2, unit), - self.span, - ) - .into()); - } - if &unit == unit2 || unit == Unit::None || unit2 == &Unit::None { - num.cmp(num2) - } else { - num.cmp( - &(num2.clone() - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone()), - ) - } - } - v => { - return Err(( - format!( - "Undefined operation \"{} {} {}\".", - v.inspect(self.span)?, - op, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }, - _ => { - return Err(( - format!( - "Undefined operation \"{} {} {}\".", - left.inspect(self.span)?, - op, - right.inspect(self.span)? - ), - self.span, - ) - .into()) - } - }; + + let ordering = left.cmp(&right, self.span, op)?; + Ok(match op { Op::GreaterThan => match ordering { Ordering::Greater => Value::True, diff --git a/src/parse/value/parse.rs b/src/parse/value/parse.rs index 3c66b6e..0e59095 100644 --- a/src/parse/value/parse.rs +++ b/src/parse/value/parse.rs @@ -198,6 +198,8 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_value(in_paren, predicate) } @@ -222,11 +224,56 @@ impl<'a> Parser<'a> { extender: self.extender, content_scopes: self.content_scopes, options: self.options, + modules: self.modules, + module_config: self.module_config, } .parse_value(in_paren, &|_| false) } - fn parse_ident_value(&mut self) -> SassResult> { + #[allow(clippy::eval_order_dependence)] + fn parse_module_item( + &mut self, + module: &str, + mut module_span: Span, + ) -> SassResult> { + Ok(IntermediateValue::Value( + if matches!(self.toks.peek(), Some(Token { kind: '$', .. })) { + self.toks.next(); + let var = self + .parse_identifier_no_interpolation(false)? + .map_node(|i| i.into()); + + module_span = module_span.merge(var.span); + + let value = self.modules.get(module.into(), module_span)?.get_var(var)?; + HigherIntermediateValue::Literal(value.clone()) + } else { + let fn_name = self + .parse_identifier_no_interpolation(false)? + .map_node(|i| i.into()); + + let function = self + .modules + .get(module.into(), module_span)? + .get_fn(fn_name)? + .ok_or(("Undefined function.", fn_name.span))?; + + if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) { + todo!() + } + + let call_args = self.parse_call_args()?; + + HigherIntermediateValue::Function(function, call_args) + }, + ) + .span(module_span)) + } + + fn parse_ident_value( + &mut self, + predicate: &dyn Fn(&mut PeekMoreIterator>) -> bool, + ) -> SassResult> { let Spanned { node: mut s, span } = self.parse_identifier()?; self.span_before = span; @@ -247,68 +294,72 @@ impl<'a> Parser<'a> { }); } - if let Some(Token { kind: '(', .. }) = self.toks.peek() { - self.toks.next(); + match self.toks.peek() { + Some(Token { kind: '(', .. }) => { + self.toks.next(); - if lower == "min" || lower == "max" { - match self.try_parse_min_max(&lower, true)? { - Some(val) => { - self.toks.truncate_iterator_to_cursor(); - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::String(val, QuoteKind::None), - )) - .span(span)); + if lower == "min" || lower == "max" { + match self.try_parse_min_max(&lower, true)? { + Some(val) => { + self.toks.truncate_iterator_to_cursor(); + return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( + Value::String(val, QuoteKind::None), + )) + .span(span)); + } + None => { + self.toks.reset_cursor(); + } } + } + + let as_ident = Identifier::from(&s); + let func = match self.scopes.get_fn(as_ident, self.global_scope) { + Some(f) => f, None => { - self.toks.reset_cursor(); + if let Some(f) = GLOBAL_FUNCTIONS.get(as_ident.as_str()) { + return Ok(IntermediateValue::Value( + HigherIntermediateValue::Function( + SassFunction::Builtin(f.clone(), as_ident), + self.parse_call_args()?, + ), + ) + .span(span)); + } else { + // check for special cased CSS functions + match unvendor(&lower) { + "calc" | "element" | "expression" => { + s = lower; + self.parse_calc_args(&mut s)?; + } + "url" => match self.try_parse_url()? { + Some(val) => s = val, + None => s.push_str(&self.parse_call_args()?.to_css_string()?), + }, + _ => s.push_str(&self.parse_call_args()?.to_css_string()?), + } + + return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( + Value::String(s, QuoteKind::None), + )) + .span(span)); + } } + }; + + let call_args = self.parse_call_args()?; + return Ok(IntermediateValue::Value(HigherIntermediateValue::Function( + func, call_args, + )) + .span(span)); + } + Some(Token { kind: '.', .. }) => { + if !predicate(self.toks) { + self.toks.next(); + return self.parse_module_item(&s, span); } } - - let as_ident = Identifier::from(&s); - let func = match self.scopes.get_fn( - Spanned { - node: as_ident, - span, - }, - self.global_scope, - ) { - Some(f) => f, - None => { - if let Some(f) = GLOBAL_FUNCTIONS.get(as_ident.as_str()) { - return Ok(IntermediateValue::Value(HigherIntermediateValue::Function( - SassFunction::Builtin(f.clone(), as_ident), - self.parse_call_args()?, - )) - .span(span)); - } else { - // check for special cased CSS functions - match unvendor(&lower) { - "calc" | "element" | "expression" => { - s = lower; - self.parse_calc_args(&mut s)?; - } - "url" => match self.try_parse_url()? { - Some(val) => s = val, - None => s.push_str(&self.parse_call_args()?.to_css_string()?), - }, - _ => s.push_str(&self.parse_call_args()?.to_css_string()?), - } - - return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( - Value::String(s, QuoteKind::None), - )) - .span(span)); - } - } - }; - - let call_args = self.parse_call_args()?; - return Ok(IntermediateValue::Value(HigherIntermediateValue::Function( - SassFunction::UserDefined(Box::new(func), as_ident), - call_args, - )) - .span(span)); + _ => {} } // check for named colors @@ -442,7 +493,7 @@ impl<'a> Parser<'a> { || (!kind.is_ascii() && !kind.is_control()) || (kind == '-' && self.next_is_hypen()) => { - return Some(self.parse_ident_value()); + return Some(self.parse_ident_value(predicate)); } '0'..='9' | '.' => { let Spanned { @@ -478,7 +529,7 @@ impl<'a> Parser<'a> { let n = Rational64::new_raw(parse_i64(&val.num), 1); return Some(Ok(IntermediateValue::Value( HigherIntermediateValue::Literal(Value::Dimension( - Number::new_small(n), + Some(Number::new_small(n)), unit, false, )), @@ -491,7 +542,7 @@ impl<'a> Parser<'a> { let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len)); return Some(Ok(IntermediateValue::Value( HigherIntermediateValue::Literal(Value::Dimension( - Number::new_small(n), + Some(Number::new_small(n)), unit, false, )), @@ -504,7 +555,7 @@ impl<'a> Parser<'a> { if val.times_ten.is_empty() { return Some(Ok(IntermediateValue::Value( HigherIntermediateValue::Literal(Value::Dimension( - Number::new_big(n), + Some(Number::new_big(n)), unit, false, )), @@ -533,7 +584,7 @@ impl<'a> Parser<'a> { }; IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension( - Number::new_big(n * times_ten), + Some(Number::new_big(n * times_ten)), unit, false, ))) @@ -547,6 +598,7 @@ impl<'a> Parser<'a> { }; // todo: the above shouldn't eat the closing paren if let Some(last_tok) = inner.pop() { + // todo: we should remove this like we did for square braces if last_tok.kind != ')' { return Some(Err(("expected \")\".", span).into())); } @@ -570,7 +622,7 @@ impl<'a> Parser<'a> { if let Some(Token { kind: '{', pos }) = self.toks.peek_forward(1) { self.span_before = *pos; self.toks.reset_cursor(); - return Some(self.parse_ident_value()); + return Some(self.parse_ident_value(predicate)); } self.toks.reset_cursor(); self.toks.next(); diff --git a/src/parse/variable.rs b/src/parse/variable.rs index fe25971..a5a77e4 100644 --- a/src/parse/variable.rs +++ b/src/parse/variable.rs @@ -8,10 +8,10 @@ use crate::{ use super::Parser; #[derive(Debug)] -struct VariableValue { - val_toks: Vec, - global: bool, - default: bool, +pub(crate) struct VariableValue { + pub val_toks: Vec, + pub global: bool, + pub default: bool, } impl VariableValue { @@ -29,9 +29,9 @@ impl<'a> Parser<'a> { assert!(matches!(self.toks.next(), Some(Token { kind: '$', .. }))); let ident: Identifier = self.parse_identifier_no_interpolation(false)?.node.into(); self.whitespace(); - if !matches!(self.toks.next(), Some(Token { kind: ':', .. })) { - return Err(("expected \":\".", self.span_before).into()); - } + + self.expect_char(':')?; + let VariableValue { val_toks, global, @@ -39,13 +39,24 @@ impl<'a> Parser<'a> { } = self.parse_variable_value()?; if default { + let config_val = self.module_config.get(ident); if self.at_root && !self.flags.in_control_flow() { if !self.global_scope.var_exists(ident) { - let value = self.parse_value_from_vec(val_toks, true)?; + let value = if let Some(config_val) = config_val { + config_val + } else { + self.parse_value_from_vec(val_toks, true)?.node + }; + self.global_scope.insert_var(ident, value); } } else { - let value = self.parse_value_from_vec(val_toks, true)?; + let value = if let Some(config_val) = config_val { + config_val + } else { + self.parse_value_from_vec(val_toks, true)?.node + }; + if global && !self.global_scope.var_exists(ident) { self.global_scope.insert_var(ident, value.clone()); } @@ -55,7 +66,7 @@ impl<'a> Parser<'a> { return Ok(()); } - let value = self.parse_value_from_vec(val_toks, true)?; + let value = self.parse_value_from_vec(val_toks, true)?.node; if global { self.global_scope.insert_var(ident, value.clone()); @@ -77,7 +88,7 @@ impl<'a> Parser<'a> { Ok(()) } - fn parse_variable_value(&mut self) -> SassResult { + pub(super) fn parse_variable_value(&mut self) -> SassResult { let mut default = false; let mut global = false; diff --git a/src/scope.rs b/src/scope.rs index 69fb0db..dc968e2 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -3,18 +3,18 @@ use std::collections::BTreeMap; use codemap::Spanned; use crate::{ - atrule::{Function, Mixin}, - builtin::GLOBAL_FUNCTIONS, + atrule::mixin::Mixin, + builtin::{modules::Module, GLOBAL_FUNCTIONS}, common::Identifier, error::SassResult, - value::Value, + value::{SassFunction, Value}, }; #[derive(Debug, Default)] pub(crate) struct Scope { - vars: BTreeMap>, - mixins: BTreeMap, - functions: BTreeMap, + pub vars: BTreeMap, + pub mixins: BTreeMap, + pub functions: BTreeMap, } impl Scope { @@ -31,12 +31,12 @@ impl Scope { fn get_var(&self, name: Spanned) -> SassResult<&Value> { match self.vars.get(&name.node) { - Some(v) => Ok(&v.node), + Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), } } - pub fn insert_var(&mut self, s: Identifier, v: Spanned) -> Option> { + pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option { self.vars.insert(s, v) } @@ -55,16 +55,16 @@ impl Scope { self.mixins.insert(s.into(), v) } - fn mixin_exists(&self, name: Identifier) -> bool { + pub fn mixin_exists(&self, name: Identifier) -> bool { self.mixins.contains_key(&name) } - fn get_fn(&self, name: Identifier) -> Option { + fn get_fn(&self, name: Identifier) -> Option { self.functions.get(&name).cloned() } - pub fn insert_fn>(&mut self, s: T, v: Function) -> Option { - self.functions.insert(s.into(), v) + pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option { + self.functions.insert(s, v) } fn fn_exists(&self, name: Identifier) -> bool { @@ -73,6 +73,16 @@ impl Scope { } self.functions.contains_key(&name) } + + fn merge(&mut self, other: Scope) { + self.vars.extend(other.vars); + self.mixins.extend(other.mixins); + self.functions.extend(other.functions); + } + + pub fn merge_module(&mut self, other: Module) { + self.merge(other.scope); + } } #[derive(Debug, Default)] @@ -111,7 +121,7 @@ impl Scopes { /// Variables impl Scopes { - pub fn insert_var(&mut self, s: Identifier, v: Spanned) -> Option> { + pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option { for scope in self.0.iter_mut().rev() { if scope.var_exists(s) { return scope.insert_var(s, v); @@ -130,7 +140,7 @@ impl Scopes { /// Always insert this variable into the innermost scope /// /// Used, for example, for variables from `@each` and `@for` - pub fn insert_var_last(&mut self, s: Identifier, v: Spanned) -> Option> { + pub fn insert_var_last(&mut self, s: Identifier, v: Value) -> Option { if let Some(scope) = self.0.last_mut() { scope.insert_var(s, v) } else { @@ -141,11 +151,7 @@ impl Scopes { } } - pub fn insert_default_var( - &mut self, - s: Identifier, - v: Spanned, - ) -> Option> { + pub fn insert_default_var(&mut self, s: Identifier, v: Value) -> Option { if let Some(scope) = self.0.last_mut() { if scope.var_exists(s) { None @@ -218,7 +224,7 @@ impl Scopes { /// Functions impl Scopes { - pub fn insert_fn(&mut self, s: Identifier, v: Function) -> Option { + pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option { if let Some(scope) = self.0.last_mut() { scope.insert_fn(s, v) } else { @@ -229,17 +235,13 @@ impl Scopes { } } - pub fn get_fn<'a>( - &'a self, - name: Spanned, - global_scope: &'a Scope, - ) -> Option { + pub fn get_fn<'a>(&'a self, name: Identifier, global_scope: &'a Scope) -> Option { for scope in self.0.iter().rev() { - if scope.fn_exists(name.node) { - return scope.get_fn(name.node); + if scope.fn_exists(name) { + return scope.get_fn(name); } } - global_scope.get_fn(name.node) + global_scope.get_fn(name) } pub fn fn_exists(&self, name: Identifier, global_scope: &Scope) -> bool { diff --git a/src/selector/attribute.rs b/src/selector/attribute.rs index 3fb7dc1..e1b8129 100644 --- a/src/selector/attribute.rs +++ b/src/selector/attribute.rs @@ -5,7 +5,9 @@ use std::{ use codemap::Span; -use crate::{common::QuoteKind, error::SassResult, parse::Parser, utils::is_ident, value::Value}; +use crate::{ + common::QuoteKind, error::SassResult, parse::Parser, utils::is_ident, value::Value, Token, +}; use super::{Namespace, QualifiedName}; @@ -41,13 +43,8 @@ impl Hash for Attribute { fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult { let next = parser.toks.peek().ok_or(("Expected identifier.", start))?; if next.kind == '*' { - let pos = next.pos; parser.toks.next(); - if parser.toks.peek().ok_or(("expected \"|\".", pos))?.kind != '|' { - return Err(("expected \"|\".", pos).into()); - } - - parser.span_before = parser.toks.next().unwrap().pos(); + parser.expect_char('|')?; let ident = parser.parse_identifier()?.node; return Ok(QualifiedName { @@ -89,19 +86,18 @@ fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult) -> SassResult { - let start = parser.span_before; - let op = match parser.toks.next().ok_or(("Expected \"]\".", start))?.kind { - '=' => return Ok(AttributeOp::Equals), - '~' => AttributeOp::Include, - '|' => AttributeOp::Dash, - '^' => AttributeOp::Prefix, - '$' => AttributeOp::Suffix, - '*' => AttributeOp::Contains, - _ => return Err(("Expected \"]\".", start).into()), + let op = match parser.toks.next() { + Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals), + Some(Token { kind: '~', .. }) => AttributeOp::Include, + Some(Token { kind: '|', .. }) => AttributeOp::Dash, + Some(Token { kind: '^', .. }) => AttributeOp::Prefix, + Some(Token { kind: '$', .. }) => AttributeOp::Suffix, + Some(Token { kind: '*', .. }) => AttributeOp::Contains, + Some(..) | None => return Err(("Expected \"]\".", parser.span_before).into()), }; - if parser.toks.next().ok_or(("expected \"=\".", start))?.kind != '=' { - return Err(("expected \"=\".", start).into()); - } + + parser.expect_char('=')?; + Ok(op) } impl Attribute { @@ -145,25 +141,23 @@ impl Attribute { }; parser.whitespace(); - let peek = parser.toks.peek().ok_or(("expected more input.", start))?; - - let modifier = match peek.kind { - c if c.is_alphabetic() => Some(c), + let modifier = match parser.toks.peek().cloned() { + Some(Token { + kind: c @ 'a'..='z', + .. + }) + | Some(Token { + kind: c @ 'A'..='Z', + .. + }) => { + parser.toks.next(); + parser.whitespace(); + Some(c) + } _ => None, }; - let pos = peek.pos(); - - if modifier.is_some() { - parser.toks.next(); - parser.whitespace(); - } - - if parser.toks.peek().ok_or(("expected \"]\".", pos))?.kind != ']' { - return Err(("expected \"]\".", pos).into()); - } - - parser.toks.next(); + parser.expect_char(']')?; Ok(Attribute { op, diff --git a/src/selector/parse.rs b/src/selector/parse.rs index cc7c001..2522bce 100644 --- a/src/selector/parse.rs +++ b/src/selector/parse.rs @@ -317,14 +317,14 @@ impl<'a, 'b> SelectorParser<'a, 'b> { if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.parser.whitespace(); - self.expect_closing_paren()?; + self.parser.expect_char(')')?; } else { argument = Some(self.declaration_value()?.into_boxed_str()); } } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.parser.whitespace(); - self.expect_closing_paren()?; + self.parser.expect_char(')')?; } else if unvendored == "nth-child" || unvendored == "nth-last-child" { let mut this_arg = self.parse_a_n_plus_b()?; let found_whitespace = self.parser.whitespace(); @@ -339,7 +339,7 @@ impl<'a, 'b> SelectorParser<'a, 'b> { } _ => {} } - self.expect_closing_paren()?; + self.parser.expect_char(')')?; argument = Some(this_arg.into_boxed_str()); } else { argument = Some( @@ -541,14 +541,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> { Err((format!("Expected \"{}\".", s), self.span).into()) } } - - fn expect_closing_paren(&mut self) -> SassResult<()> { - if let Some(Token { kind: ')', .. }) = self.parser.toks.next() { - Ok(()) - } else { - Err(("expected \")\".", self.span).into()) - } - } } /// Returns whether `c` can start a simple selector other than a type diff --git a/src/unit/conversion.rs b/src/unit/conversion.rs index b848d19..8862d14 100644 --- a/src/unit/conversion.rs +++ b/src/unit/conversion.rs @@ -7,152 +7,151 @@ use std::{collections::HashMap, f64::consts::PI}; use num_traits::One; use once_cell::sync::Lazy; -use crate::value::Number; +use crate::{unit::Unit, value::Number}; -pub(crate) static UNIT_CONVERSION_TABLE: Lazy< - HashMap<&'static str, HashMap<&'static str, Number>>, -> = Lazy::new(|| { - let mut from_in = HashMap::new(); - from_in.insert("in", Number::one()); - from_in.insert("cm", Number::one() / Number::from(2.54)); - from_in.insert("pc", Number::small_ratio(1, 6)); - from_in.insert("mm", Number::one() / Number::from(25.4)); - from_in.insert("q", Number::one() / Number::from(101.6)); - from_in.insert("pt", Number::small_ratio(1, 72)); - from_in.insert("px", Number::small_ratio(1, 96)); +pub(crate) static UNIT_CONVERSION_TABLE: Lazy>> = + Lazy::new(|| { + let mut from_in = HashMap::new(); + from_in.insert(Unit::In, Number::one()); + from_in.insert(Unit::Cm, Number::one() / Number::from(2.54)); + from_in.insert(Unit::Pc, Number::small_ratio(1, 6)); + from_in.insert(Unit::Mm, Number::one() / Number::from(25.4)); + from_in.insert(Unit::Q, Number::one() / Number::from(101.6)); + from_in.insert(Unit::Pt, Number::small_ratio(1, 72)); + from_in.insert(Unit::Px, Number::small_ratio(1, 96)); - let mut from_cm = HashMap::new(); - from_cm.insert("in", Number::from(2.54)); - from_cm.insert("cm", Number::one()); - from_cm.insert("pc", Number::from(2.54) / Number::from(6)); - from_cm.insert("mm", Number::small_ratio(1, 10)); - from_cm.insert("q", Number::small_ratio(1, 40)); - from_cm.insert("pt", Number::from(2.54) / Number::from(72)); - from_cm.insert("px", Number::from(2.54) / Number::from(96)); + let mut from_cm = HashMap::new(); + from_cm.insert(Unit::In, Number::from(2.54)); + from_cm.insert(Unit::Cm, Number::one()); + from_cm.insert(Unit::Pc, Number::from(2.54) / Number::from(6)); + from_cm.insert(Unit::Mm, Number::small_ratio(1, 10)); + from_cm.insert(Unit::Q, Number::small_ratio(1, 40)); + from_cm.insert(Unit::Pt, Number::from(2.54) / Number::from(72)); + from_cm.insert(Unit::Px, Number::from(2.54) / Number::from(96)); - let mut from_pc = HashMap::new(); - from_pc.insert("in", Number::from(6)); - from_pc.insert("cm", Number::from(6) / Number::from(2.54)); - from_pc.insert("pc", Number::one()); - from_pc.insert("mm", Number::from(6) / Number::from(25.4)); - from_pc.insert("q", Number::from(6) / Number::from(101.6)); - from_pc.insert("pt", Number::small_ratio(1, 12)); - from_pc.insert("px", Number::small_ratio(1, 16)); + let mut from_pc = HashMap::new(); + from_pc.insert(Unit::In, Number::from(6)); + from_pc.insert(Unit::Cm, Number::from(6) / Number::from(2.54)); + from_pc.insert(Unit::Pc, Number::one()); + from_pc.insert(Unit::Mm, Number::from(6) / Number::from(25.4)); + from_pc.insert(Unit::Q, Number::from(6) / Number::from(101.6)); + from_pc.insert(Unit::Pt, Number::small_ratio(1, 12)); + from_pc.insert(Unit::Px, Number::small_ratio(1, 16)); - let mut from_mm = HashMap::new(); - from_mm.insert("in", Number::from(25.4)); - from_mm.insert("cm", Number::from(10)); - from_mm.insert("pc", Number::from(25.4) / Number::from(6)); - from_mm.insert("mm", Number::one()); - from_mm.insert("q", Number::small_ratio(1, 4)); - from_mm.insert("pt", Number::from(25.4) / Number::from(72)); - from_mm.insert("px", Number::from(25.4) / Number::from(96)); + let mut from_mm = HashMap::new(); + from_mm.insert(Unit::In, Number::from(25.4)); + from_mm.insert(Unit::Cm, Number::from(10)); + from_mm.insert(Unit::Pc, Number::from(25.4) / Number::from(6)); + from_mm.insert(Unit::Mm, Number::one()); + from_mm.insert(Unit::Q, Number::small_ratio(1, 4)); + from_mm.insert(Unit::Pt, Number::from(25.4) / Number::from(72)); + from_mm.insert(Unit::Px, Number::from(25.4) / Number::from(96)); - let mut from_q = HashMap::new(); - from_q.insert("in", Number::from(101.6)); - from_q.insert("cm", Number::from(40)); - from_q.insert("pc", Number::from(101.6) / Number::from(6)); - from_q.insert("mm", Number::from(4)); - from_q.insert("q", Number::one()); - from_q.insert("pt", Number::from(101.6) / Number::from(72)); - from_q.insert("px", Number::from(101.6) / Number::from(96)); + let mut from_q = HashMap::new(); + from_q.insert(Unit::In, Number::from(101.6)); + from_q.insert(Unit::Cm, Number::from(40)); + from_q.insert(Unit::Pc, Number::from(101.6) / Number::from(6)); + from_q.insert(Unit::Mm, Number::from(4)); + from_q.insert(Unit::Q, Number::one()); + from_q.insert(Unit::Pt, Number::from(101.6) / Number::from(72)); + from_q.insert(Unit::Px, Number::from(101.6) / Number::from(96)); - let mut from_pt = HashMap::new(); - from_pt.insert("in", Number::from(72)); - from_pt.insert("cm", Number::from(72) / Number::from(2.54)); - from_pt.insert("pc", Number::from(12)); - from_pt.insert("mm", Number::from(72) / Number::from(25.4)); - from_pt.insert("q", Number::from(72) / Number::from(101.6)); - from_pt.insert("pt", Number::one()); - from_pt.insert("px", Number::small_ratio(3, 4)); + let mut from_pt = HashMap::new(); + from_pt.insert(Unit::In, Number::from(72)); + from_pt.insert(Unit::Cm, Number::from(72) / Number::from(2.54)); + from_pt.insert(Unit::Pc, Number::from(12)); + from_pt.insert(Unit::Mm, Number::from(72) / Number::from(25.4)); + from_pt.insert(Unit::Q, Number::from(72) / Number::from(101.6)); + from_pt.insert(Unit::Pt, Number::one()); + from_pt.insert(Unit::Px, Number::small_ratio(3, 4)); - let mut from_px = HashMap::new(); - from_px.insert("in", Number::from(96)); - from_px.insert("cm", Number::from(96) / Number::from(2.54)); - from_px.insert("pc", Number::from(16)); - from_px.insert("mm", Number::from(96) / Number::from(25.4)); - from_px.insert("q", Number::from(96) / Number::from(101.6)); - from_px.insert("pt", Number::small_ratio(4, 3)); - from_px.insert("px", Number::one()); + let mut from_px = HashMap::new(); + from_px.insert(Unit::In, Number::from(96)); + from_px.insert(Unit::Cm, Number::from(96) / Number::from(2.54)); + from_px.insert(Unit::Pc, Number::from(16)); + from_px.insert(Unit::Mm, Number::from(96) / Number::from(25.4)); + from_px.insert(Unit::Q, Number::from(96) / Number::from(101.6)); + from_px.insert(Unit::Pt, Number::small_ratio(4, 3)); + from_px.insert(Unit::Px, Number::one()); - let mut from_deg = HashMap::new(); - from_deg.insert("deg", Number::one()); - from_deg.insert("grad", Number::small_ratio(9, 10)); - from_deg.insert("rad", Number::from(180) / Number::from(PI)); - from_deg.insert("turn", Number::from(360)); + let mut from_deg = HashMap::new(); + from_deg.insert(Unit::Deg, Number::one()); + from_deg.insert(Unit::Grad, Number::small_ratio(9, 10)); + from_deg.insert(Unit::Rad, Number::from(180) / Number::from(PI)); + from_deg.insert(Unit::Turn, Number::from(360)); - let mut from_grad = HashMap::new(); - from_grad.insert("deg", Number::small_ratio(10, 9)); - from_grad.insert("grad", Number::one()); - from_grad.insert("rad", Number::from(200) / Number::from(PI)); - from_grad.insert("turn", Number::from(400)); + let mut from_grad = HashMap::new(); + from_grad.insert(Unit::Deg, Number::small_ratio(10, 9)); + from_grad.insert(Unit::Grad, Number::one()); + from_grad.insert(Unit::Rad, Number::from(200) / Number::from(PI)); + from_grad.insert(Unit::Turn, Number::from(400)); - let mut from_rad = HashMap::new(); - from_rad.insert("deg", Number::from(PI) / Number::from(180)); - from_rad.insert("grad", Number::from(PI) / Number::from(200)); - from_rad.insert("rad", Number::one()); - from_rad.insert("turn", Number::from(2.0 * PI)); + let mut from_rad = HashMap::new(); + from_rad.insert(Unit::Deg, Number::from(PI) / Number::from(180)); + from_rad.insert(Unit::Grad, Number::from(PI) / Number::from(200)); + from_rad.insert(Unit::Rad, Number::one()); + from_rad.insert(Unit::Turn, Number::from(2.0 * PI)); - let mut from_turn = HashMap::new(); - from_turn.insert("deg", Number::small_ratio(1, 360)); - from_turn.insert("grad", Number::small_ratio(1, 400)); - from_turn.insert("rad", Number::one() / Number::from(2.0 * PI)); - from_turn.insert("turn", Number::one()); + let mut from_turn = HashMap::new(); + from_turn.insert(Unit::Deg, Number::small_ratio(1, 360)); + from_turn.insert(Unit::Grad, Number::small_ratio(1, 400)); + from_turn.insert(Unit::Rad, Number::one() / Number::from(2.0 * PI)); + from_turn.insert(Unit::Turn, Number::one()); - let mut from_s = HashMap::new(); - from_s.insert("s", Number::one()); - from_s.insert("ms", Number::small_ratio(1, 1000)); + let mut from_s = HashMap::new(); + from_s.insert(Unit::S, Number::one()); + from_s.insert(Unit::Ms, Number::small_ratio(1, 1000)); - let mut from_ms = HashMap::new(); - from_ms.insert("s", Number::from(1000)); - from_ms.insert("ms", Number::one()); + let mut from_ms = HashMap::new(); + from_ms.insert(Unit::S, Number::from(1000)); + from_ms.insert(Unit::Ms, Number::one()); - let mut from_hz = HashMap::new(); - from_hz.insert("Hz", Number::one()); - from_hz.insert("kHz", Number::from(1000)); + let mut from_hz = HashMap::new(); + from_hz.insert(Unit::Hz, Number::one()); + from_hz.insert(Unit::Khz, Number::from(1000)); - let mut from_khz = HashMap::new(); - from_khz.insert("Hz", Number::small_ratio(1, 1000)); - from_khz.insert("kHz", Number::one()); + let mut from_khz = HashMap::new(); + from_khz.insert(Unit::Hz, Number::small_ratio(1, 1000)); + from_khz.insert(Unit::Khz, Number::one()); - let mut from_dpi = HashMap::new(); - from_dpi.insert("dpi", Number::one()); - from_dpi.insert("dpcm", Number::from(2.54)); - from_dpi.insert("dppx", Number::from(96)); + let mut from_dpi = HashMap::new(); + from_dpi.insert(Unit::Dpi, Number::one()); + from_dpi.insert(Unit::Dpcm, Number::from(2.54)); + from_dpi.insert(Unit::Dppx, Number::from(96)); - let mut from_dpcm = HashMap::new(); - from_dpcm.insert("dpi", Number::one() / Number::from(2.54)); - from_dpcm.insert("dpcm", Number::one()); - from_dpcm.insert("dppx", Number::from(96) / Number::from(2.54)); + let mut from_dpcm = HashMap::new(); + from_dpcm.insert(Unit::Dpi, Number::one() / Number::from(2.54)); + from_dpcm.insert(Unit::Dpcm, Number::one()); + from_dpcm.insert(Unit::Dppx, Number::from(96) / Number::from(2.54)); - let mut from_dppx = HashMap::new(); - from_dppx.insert("dpi", Number::small_ratio(1, 96)); - from_dppx.insert("dpcm", Number::from(2.54) / Number::from(96)); - from_dppx.insert("dppx", Number::one()); + let mut from_dppx = HashMap::new(); + from_dppx.insert(Unit::Dpi, Number::small_ratio(1, 96)); + from_dppx.insert(Unit::Dpcm, Number::from(2.54) / Number::from(96)); + from_dppx.insert(Unit::Dppx, Number::one()); - let mut m = HashMap::new(); - m.insert("in", from_in); - m.insert("cm", from_cm); - m.insert("pc", from_pc); - m.insert("mm", from_mm); - m.insert("q", from_q); - m.insert("pt", from_pt); - m.insert("px", from_px); + let mut m = HashMap::new(); + m.insert(Unit::In, from_in); + m.insert(Unit::Cm, from_cm); + m.insert(Unit::Pc, from_pc); + m.insert(Unit::Mm, from_mm); + m.insert(Unit::Q, from_q); + m.insert(Unit::Pt, from_pt); + m.insert(Unit::Px, from_px); - m.insert("deg", from_deg); - m.insert("grad", from_grad); - m.insert("rad", from_rad); - m.insert("turn", from_turn); + m.insert(Unit::Deg, from_deg); + m.insert(Unit::Grad, from_grad); + m.insert(Unit::Rad, from_rad); + m.insert(Unit::Turn, from_turn); - m.insert("s", from_s); - m.insert("ms", from_ms); + m.insert(Unit::S, from_s); + m.insert(Unit::Ms, from_ms); - m.insert("Hz", from_hz); - m.insert("kHz", from_khz); + m.insert(Unit::Hz, from_hz); + m.insert(Unit::Khz, from_khz); - m.insert("dpi", from_dpi); - m.insert("dpcm", from_dpcm); - m.insert("dppx", from_dppx); + m.insert(Unit::Dpi, from_dpi); + m.insert(Unit::Dpcm, from_dpcm); + m.insert(Unit::Dppx, from_dppx); - m -}); + m + }); diff --git a/src/value/map.rs b/src/value/map.rs index e5071c6..fc2c3f9 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -34,6 +34,10 @@ impl SassMap { SassMap(Vec::new()) } + pub const fn new_with(elements: Vec<(Value, Value)>) -> SassMap { + SassMap(elements) + } + /// We take by value here (consuming the map) in order to /// save a clone of the value, since the only place this /// should be called is in a builtin function, which throws diff --git a/src/value/mod.rs b/src/value/mod.rs index 8d0bbad..7e7291a 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,14 +1,16 @@ +use std::cmp::Ordering; + use peekmore::PeekMore; use codemap::{Span, Spanned}; use crate::{ color::Color, - common::{Brackets, ListSeparator, QuoteKind}, + common::{Brackets, ListSeparator, Op, QuoteKind}, error::SassResult, parse::Parser, selector::Selector, - unit::{Unit, UNIT_CONVERSION_TABLE}, + unit::Unit, utils::hex_char_for, {Cow, Token}, }; @@ -29,7 +31,8 @@ pub(crate) enum Value { True, False, Null, - Dimension(Number, Unit, bool), + /// A `None` value for `Number` indicates a `NaN` value + Dimension(Option, Unit, bool), List(Vec, ListSeparator, Brackets), Color(Box), String(String, QuoteKind), @@ -46,8 +49,8 @@ impl PartialEq for Value { Value::String(s2, ..) => s1 == s2, _ => false, }, - Value::Dimension(n, unit, _) => match other { - Value::Dimension(n2, unit2, _) => { + Value::Dimension(Some(n), unit, _) => match other { + Value::Dimension(Some(n2), unit2, _) => { if !unit.comparable(unit2) { false } else if unit == unit2 { @@ -55,14 +58,12 @@ impl PartialEq for Value { } else if unit == &Unit::None || unit2 == &Unit::None { false } else { - n == &(n2.clone() - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone()) + n == &n2.clone().convert(unit2, unit) } } _ => false, }, + Value::Dimension(None, ..) => false, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { @@ -200,9 +201,21 @@ impl Value { Value::Important => Cow::const_str("!important"), Value::Dimension(num, unit, _) => match unit { Unit::Mul(..) | Unit::Div(..) => { - return Err((format!("{}{} isn't a valid CSS value.", num, unit), span).into()); + if let Some(num) = num { + return Err( + (format!("{}{} isn't a valid CSS value.", num, unit), span).into() + ); + } else { + return Err((format!("NaN{} isn't a valid CSS value.", unit), span).into()); + } + } + _ => { + if let Some(num) = num { + Cow::owned(format!("{}{}", num, unit)) + } else { + Cow::owned(format!("NaN{}", unit)) + } } - _ => Cow::owned(format!("{}{}", num, unit)), }, Value::Map(..) | Value::FunctionRef(..) => { return Err(( @@ -322,14 +335,68 @@ impl Value { } } + pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult { + Ok(match self { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num), unit, _) => match &other { + Value::Dimension(None, ..) => todo!(), + Value::Dimension(Some(num2), unit2, _) => { + if !unit.comparable(unit2) { + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); + } + if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None { + num.cmp(num2) + } else { + num.cmp(&num2.clone().convert(unit2, unit)) + } + } + v => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + v.inspect(span)?, + op, + other.inspect(span)? + ), + span, + ) + .into()) + } + }, + _ => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + self.inspect(span)?, + op, + other.inspect(span)? + ), + span, + ) + .into()) + } + }) + } + + pub fn unitless(&self) -> bool { + #[allow(clippy::match_same_arms)] + match self { + Value::Dimension(_, Unit::None, _) => true, + Value::Dimension(..) => false, + _ => true, + } + } + pub fn not_equals(&self, other: &Self) -> bool { match self { Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 != s2, _ => true, }, - Value::Dimension(n, unit, _) => match other { - Value::Dimension(n2, unit2, _) => { + Value::Dimension(Some(n), unit, _) => match other { + Value::Dimension(Some(n2), unit2, _) => { if !unit.comparable(unit2) { true } else if unit == unit2 { @@ -337,10 +404,7 @@ impl Value { } else if unit == &Unit::None || unit2 == &Unit::None { true } else { - n != &(n2.clone() - * UNIT_CONVERSION_TABLE[unit.to_string().as_str()] - [unit2.to_string().as_str()] - .clone()) + n != &n2.clone().convert(unit2, unit) } } _ => true, @@ -405,7 +469,8 @@ impl Value { .collect::>>()? .join(", ") )), - Value::Dimension(num, unit, _) => Cow::owned(format!("{}{}", num, unit)), + Value::Dimension(Some(num), unit, _) => Cow::owned(format!("{}{}", num, unit)), + Value::Dimension(None, unit, ..) => Cow::owned(format!("NaN{}", unit)), Value::ArgList(args) if args.is_empty() => Cow::const_str("()"), Value::ArgList(args) if args.len() == 1 => Cow::owned(format!( "({},)", @@ -477,6 +542,8 @@ impl Value { extender: parser.extender, content_scopes: parser.content_scopes, options: parser.options, + modules: parser.modules, + module_config: parser.module_config, } .parse_selector(allows_parent, true, String::new())? .0) diff --git a/src/value/number/mod.rs b/src/value/number/mod.rs index 00152b5..522f915 100644 --- a/src/value/number/mod.rs +++ b/src/value/number/mod.rs @@ -8,7 +8,11 @@ use std::{ use num_bigint::BigInt; use num_rational::{BigRational, Rational64}; -use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, Zero}; +use num_traits::{ + CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, ToPrimitive, Zero, +}; + +use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; use integer::Integer; @@ -16,7 +20,7 @@ mod integer; const PRECISION: usize = 10; -#[derive(Clone, Eq, PartialEq, Ord)] +#[derive(Clone, Eq, PartialEq)] pub(crate) enum Number { Small(Rational64), Big(Box), @@ -106,6 +110,84 @@ impl Number { self } + + #[allow(clippy::cast_precision_loss)] + fn as_float(self) -> Option { + Some(match self { + Number::Small(n) => ((*n.numer() as f64) / (*n.denom() as f64)), + Number::Big(n) => ((n.numer().to_f64()?) / (n.denom().to_f64()?)), + }) + } + + pub fn sqrt(self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.sqrt(), + )?))) + } + + pub fn ln(self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.ln(), + )?))) + } + + pub fn pow(self, exponent: Self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.powf(exponent.as_float()?), + )?))) + } + + pub fn pi() -> Self { + Number::from(std::f64::consts::PI) + } + + pub fn atan2(self, other: Self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.atan2(other.as_float()?), + )?))) + } + + /// Invariants: `from.comparable(&to)` must be true + pub fn convert(self, from: &Unit, to: &Unit) -> Self { + self * UNIT_CONVERSION_TABLE[to][from].clone() + } +} + +macro_rules! trig_fn( + ($name:ident, $name_deg:ident) => { + pub fn $name(self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.$name(), + )?))) + } + + pub fn $name_deg(self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.to_radians().$name(), + )?))) + } + } +); + +macro_rules! inverse_trig_fn( + ($name:ident) => { + pub fn $name(self) -> Option { + Some(Number::Big(Box::new(BigRational::from_float( + self.as_float()?.$name().to_degrees(), + )?))) + } + } +); + +/// Trigonometry methods +impl Number { + trig_fn!(cos, cos_deg); + trig_fn!(sin, sin_deg); + trig_fn!(tan, tan_deg); + + inverse_trig_fn!(acos); + inverse_trig_fn!(asin); + inverse_trig_fn!(atan); } impl Default for Number { @@ -321,6 +403,30 @@ impl PartialOrd for Number { } } +impl Ord for Number { + fn cmp(&self, other: &Self) -> Ordering { + match self { + Self::Small(val1) => match other { + Self::Small(val2) => val1.cmp(val2), + Self::Big(val2) => { + let tuple: (i64, i64) = (*val1).into(); + BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)).cmp(val2) + } + }, + Self::Big(val1) => match other { + Self::Small(val2) => { + let tuple: (i64, i64) = (*val2).into(); + (**val1).cmp(&BigRational::new_raw( + BigInt::from(tuple.0), + BigInt::from(tuple.1), + )) + } + Self::Big(val2) => val1.cmp(val2), + }, + } + } +} + impl Add for Number { type Output = Self; diff --git a/tests/division.rs b/tests/division.rs index 1c02dce..ad7bcfb 100644 --- a/tests/division.rs +++ b/tests/division.rs @@ -169,3 +169,8 @@ test!( "a {\n color: 1 + 3 / 4;\n}\n", "a {\n color: 1.75;\n}\n" ); +test!( + zero_div_zero_is_nan, + "a {\n color: (0 / 0);\n}\n", + "a {\n color: NaN;\n}\n" +); diff --git a/tests/get-function.rs b/tests/get-function.rs index 31583f6..2ec4e9f 100644 --- a/tests/get-function.rs +++ b/tests/get-function.rs @@ -121,3 +121,8 @@ test!( "a {\n color: call(call(get-function(get-function), darken), red, 10%);\n}\n", "a {\n color: #cc0000;\n}\n" ); +test!( + get_function_of_module, + "@use 'sass:math';\na {\n color: call(get-function(cos, $module: math), 2);\n}\n", + "a {\n color: -0.4161468365;\n}\n" +); diff --git a/tests/imports.rs b/tests/imports.rs index f076028..9210585 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -1,43 +1,10 @@ #![cfg(test)] + use std::io::Write; -use tempfile::Builder; #[macro_use] mod macros; -/// Create a temporary file with the given name -/// and contents. -/// -/// This must be a macro rather than a function -/// because the tempfile will be deleted when it -/// exits scope -macro_rules! tempfile { - ($name:literal, $content:literal) => { - let mut f = Builder::new() - .rand_bytes(0) - .prefix("") - .suffix($name) - .tempfile_in("") - .unwrap(); - write!(f, "{}", $content).unwrap(); - }; - ($name:literal, $content:literal, dir=$dir:literal) => { - let _d = Builder::new() - .rand_bytes(0) - .prefix("") - .suffix($dir) - .tempdir_in("") - .unwrap(); - let mut f = Builder::new() - .rand_bytes(0) - .prefix("") - .suffix($name) - .tempfile_in($dir) - .unwrap(); - write!(f, "{}", $content).unwrap(); - }; -} - #[test] fn imports_variable() { let input = "@import \"imports_variable\";\na {\n color: $a;\n}"; @@ -59,17 +26,8 @@ fn import_no_semicolon() { fn import_no_quotes() { let input = "@import import_no_quotes"; tempfile!("import_no_quotes", "$a: red;"); - match grass::from_string(input.to_string(), &grass::Options::default()) { - Ok(..) => panic!("did not fail"), - Err(e) => assert_eq!( - "Error: Expected string.", - e.to_string() - .chars() - .take_while(|c| *c != '\n') - .collect::() - .as_str() - ), - } + + assert_err!("Error: Expected string.", input); } #[test] diff --git a/tests/macros.rs b/tests/macros.rs index c6c1c7c..9e4d3c2 100644 --- a/tests/macros.rs +++ b/tests/macros.rs @@ -51,3 +51,54 @@ macro_rules! error { } }; } + +/// Create a temporary file with the given name +/// and contents. +/// +/// This must be a macro rather than a function +/// because the tempfile will be deleted when it +/// exits scope +#[macro_export] +macro_rules! tempfile { + ($name:literal, $content:literal) => { + let mut f = tempfile::Builder::new() + .rand_bytes(0) + .prefix("") + .suffix($name) + .tempfile_in("") + .unwrap(); + write!(f, "{}", $content).unwrap(); + }; + ($name:literal, $content:literal, dir=$dir:literal) => { + let _d = tempfile::Builder::new() + .rand_bytes(0) + .prefix("") + .suffix($dir) + .tempdir_in("") + .unwrap(); + let mut f = tempfile::Builder::new() + .rand_bytes(0) + .prefix("") + .suffix($name) + .tempfile_in($dir) + .unwrap(); + write!(f, "{}", $content).unwrap(); + }; +} + +#[macro_export] +macro_rules! assert_err { + ($err:literal, $input:expr) => { + match grass::from_string($input.to_string(), &grass::Options::default()) { + Ok(..) => panic!("did not fail"), + Err(e) => assert_eq!( + $err, + e.to_string() + .chars() + .take_while(|c| *c != '\n') + .collect::() + .as_str() + ), + } + }; +} diff --git a/tests/math-module.rs b/tests/math-module.rs new file mode 100644 index 0000000..4f83a4d --- /dev/null +++ b/tests/math-module.rs @@ -0,0 +1,592 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + clamp_in_the_middle, + "@use 'sass:math';\na {\n color: math.clamp(0, 1, 2);\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + clamp_first_is_bigger, + "@use 'sass:math';\na {\n color: math.clamp(2, 1, 0);\n}\n", + "a {\n color: 2;\n}\n" +); +test!( + clamp_all_same_unit, + "@use 'sass:math';\na {\n color: math.clamp(0px, 1px, 2px);\n}\n", + "a {\n color: 1px;\n}\n" +); +test!( + clamp_all_different_but_compatible_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2in);\n}\n", + "a {\n color: 1cm;\n}\n" +); +error!( + clamp_only_min_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0, 1cm, 2in);\n}\n", + "Error: $min is unitless but $number has unit cm. Arguments must all have units or all be unitless." +); +error!( + clamp_only_number_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1, 2in);\n}\n", + "Error: $min has unit mm but $number is unitless. Arguments must all have units or all be unitless." +); +error!( + clamp_only_max_has_no_unit, + "@use 'sass:math';\na {\n color: math.clamp(0mm, 1cm, 2);\n}\n", + "Error: $min has unit mm but $max is unitless. Arguments must all have units or all be unitless." +); +test!( + sqrt_zero, + "@use 'sass:math';\na {\n color: math.sqrt(0);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + sqrt_small_positive, + "@use 'sass:math';\na {\n color: math.sqrt(99);\n}\n", + "a {\n color: 9.9498743711;\n}\n" +); +test!( + sqrt_small_negative, + "@use 'sass:math';\na {\n color: math.sqrt(-99);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + sqrt_big_positive, + "@use 'sass:math';\na {\n color: math.sqrt(9999999999999999999999999999999999999999999999999);\n}\n", + "a {\n color: 3162277660168379038695424;\n}\n" +); +test!( + sqrt_big_negative, + "@use 'sass:math';\na {\n color: math.sqrt(-9999999999999999999999999999999999999999999999999);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + sqrt_irrational, + "@use 'sass:math';\na {\n color: math.sqrt(2);\n}\n", + "a {\n color: 1.4142135624;\n}\n" +); +test!( + sqrt_of_nan, + "@use 'sass:math';\na {\n color: math.sqrt((0 / 0));\n}\n", + "a {\n color: NaN;\n}\n" +); +error!( + sqrt_with_units, + "@use 'sass:math';\na {\n color: math.sqrt(1px);\n}\n", + "Error: $number: Expected 1px to have no units." +); +error!( + cos_non_angle, + "@use 'sass:math';\na {\n color: math.cos(1px);\n}\n", + "Error: $number: Expected 1px to be an angle." +); +test!( + cos_small_degree, + "@use 'sass:math';\na {\n color: math.cos(1deg);\n}\n", + "a {\n color: 0.9998476952;\n}\n" +); +test!( + cos_small_radian, + "@use 'sass:math';\na {\n color: math.cos(1rad);\n}\n", + "a {\n color: 0.5403023059;\n}\n" +); +test!( + cos_small_no_unit, + "@use 'sass:math';\na {\n color: math.cos(1);\n}\n", + "a {\n color: 0.5403023059;\n}\n" +); +test!( + cos_small_negative_degree, + "@use 'sass:math';\na {\n color: math.cos(-1deg);\n}\n", + "a {\n color: 0.9998476952;\n}\n" +); +test!( + cos_small_negative_radian, + "@use 'sass:math';\na {\n color: math.cos(-1rad);\n}\n", + "a {\n color: 0.5403023059;\n}\n" +); +test!( + cos_small_negative_no_unit, + "@use 'sass:math';\na {\n color: math.cos(-1);\n}\n", + "a {\n color: 0.5403023059;\n}\n" +); +test!( + cos_pi, + "@use 'sass:math';\na {\n color: math.cos(math.$pi);\n}\n", + "a {\n color: -1;\n}\n" +); +test!( + cos_two_pi, + "@use 'sass:math';\na {\n color: math.cos(2 * math.$pi);\n}\n", + "a {\n color: 1;\n}\n" +); +error!( + sin_non_angle, + "@use 'sass:math';\na {\n color: math.sin(1px);\n}\n", + "Error: $number: Expected 1px to be an angle." +); +test!( + sin_small_degree, + "@use 'sass:math';\na {\n color: math.sin(1deg);\n}\n", + "a {\n color: 0.0174524064;\n}\n" +); +test!( + sin_small_radian, + "@use 'sass:math';\na {\n color: math.sin(1rad);\n}\n", + "a {\n color: 0.8414709848;\n}\n" +); +test!( + sin_small_no_unit, + "@use 'sass:math';\na {\n color: math.sin(1);\n}\n", + "a {\n color: 0.8414709848;\n}\n" +); +test!( + sin_small_negative_degree, + "@use 'sass:math';\na {\n color: math.sin(-1deg);\n}\n", + "a {\n color: -0.0174524064;\n}\n" +); +test!( + sin_small_negative_radian, + "@use 'sass:math';\na {\n color: math.sin(-1rad);\n}\n", + "a {\n color: -0.8414709848;\n}\n" +); +test!( + sin_small_negative_no_unit, + "@use 'sass:math';\na {\n color: math.sin(-1);\n}\n", + "a {\n color: -0.8414709848;\n}\n" +); +test!( + sin_pi, + "@use 'sass:math';\na {\n color: math.sin(math.$pi);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + sin_two_pi, + "@use 'sass:math';\na {\n color: math.sin(2 * math.$pi);\n}\n", + "a {\n color: 0;\n}\n" +); +error!( + tan_non_angle, + "@use 'sass:math';\na {\n color: math.tan(1px);\n}\n", + "Error: $number: Expected 1px to be an angle." +); +test!( + tan_small_degree, + "@use 'sass:math';\na {\n color: math.tan(1deg);\n}\n", + "a {\n color: 0.0174550649;\n}\n" +); +test!( + tan_small_radian, + "@use 'sass:math';\na {\n color: math.tan(1rad);\n}\n", + "a {\n color: 1.5574077247;\n}\n" +); +test!( + tan_small_no_unit, + "@use 'sass:math';\na {\n color: math.tan(1);\n}\n", + "a {\n color: 1.5574077247;\n}\n" +); +test!( + tan_small_negative_degree, + "@use 'sass:math';\na {\n color: math.tan(-1deg);\n}\n", + "a {\n color: -0.0174550649;\n}\n" +); +test!( + tan_small_negative_radian, + "@use 'sass:math';\na {\n color: math.tan(-1rad);\n}\n", + "a {\n color: -1.5574077247;\n}\n" +); +test!( + tan_small_negative_no_unit, + "@use 'sass:math';\na {\n color: math.tan(-1);\n}\n", + "a {\n color: -1.5574077247;\n}\n" +); +test!( + tan_pi, + "@use 'sass:math';\na {\n color: math.tan(math.$pi);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + tan_two_pi, + "@use 'sass:math';\na {\n color: math.tan(2 * math.$pi);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + acos_above_one, + "@use 'sass:math';\na {\n color: math.acos(2);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + acos_below_negative_one, + "@use 'sass:math';\na {\n color: math.acos(-2);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + acos_one, + "@use 'sass:math';\na {\n color: math.acos(1);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + acos_negative_one, + "@use 'sass:math';\na {\n color: math.acos(-1);\n}\n", + "a {\n color: 180deg;\n}\n" +); +test!( + acos_zero, + "@use 'sass:math';\na {\n color: math.acos(0);\n}\n", + "a {\n color: 90deg;\n}\n" +); +test!( + acos_point_five, + "@use 'sass:math';\na {\n color: math.acos(.5);\n}\n", + "a {\n color: 60deg;\n}\n" +); +test!( + acos_nan, + "@use 'sass:math';\na {\n color: math.acos((0 / 0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + asin_above_one, + "@use 'sass:math';\na {\n color: math.asin(2);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + asin_below_negative_one, + "@use 'sass:math';\na {\n color: math.asin(-2);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + asin_one, + "@use 'sass:math';\na {\n color: math.asin(1);\n}\n", + "a {\n color: 90deg;\n}\n" +); +test!( + asin_negative_one, + "@use 'sass:math';\na {\n color: math.asin(-1);\n}\n", + "a {\n color: -90deg;\n}\n" +); +test!( + asin_zero, + "@use 'sass:math';\na {\n color: math.asin(0);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + asin_point_five, + "@use 'sass:math';\na {\n color: math.asin(.5);\n}\n", + "a {\n color: 30deg;\n}\n" +); +test!( + asin_nan, + "@use 'sass:math';\na {\n color: math.asin((0 / 0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan_above_one, + "@use 'sass:math';\na {\n color: math.atan(2);\n}\n", + "a {\n color: 63.4349488229deg;\n}\n" +); +test!( + atan_below_negative_one, + "@use 'sass:math';\na {\n color: math.atan(-2);\n}\n", + "a {\n color: -63.4349488229deg;\n}\n" +); +test!( + atan_one, + "@use 'sass:math';\na {\n color: math.atan(1);\n}\n", + "a {\n color: 45deg;\n}\n" +); +test!( + atan_negative_one, + "@use 'sass:math';\na {\n color: math.atan(-1);\n}\n", + "a {\n color: -45deg;\n}\n" +); +test!( + atan_zero, + "@use 'sass:math';\na {\n color: math.atan(0);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + atan_point_five, + "@use 'sass:math';\na {\n color: math.atan(.5);\n}\n", + "a {\n color: 26.5650511771deg;\n}\n" +); +test!( + atan_nan, + "@use 'sass:math';\na {\n color: math.atan((0 / 0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + log_above_one, + "@use 'sass:math';\na {\n color: math.log(2);\n}\n", + "a {\n color: 0.6931471806;\n}\n" +); +test!( + log_below_negative_one, + "@use 'sass:math';\na {\n color: math.log(-2);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + log_one, + "@use 'sass:math';\na {\n color: math.log(1);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + log_negative_one, + "@use 'sass:math';\na {\n color: math.log(-1);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + #[ignore = "we do not support Infinity"] + log_zero, + "@use 'sass:math';\na {\n color: math.log(0);\n}\n", + "a {\n color: -Infinity;\n}\n" +); +test!( + log_point_five, + "@use 'sass:math';\na {\n color: math.log(.5);\n}\n", + "a {\n color: -0.6931471806;\n}\n" +); +test!( + log_nan, + "@use 'sass:math';\na {\n color: math.log((0 / 0));\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + log_base_nan, + "@use 'sass:math';\na {\n color: math.log(1, (0 / 0));\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + log_base_above_one, + "@use 'sass:math';\na {\n color: math.log(2, 2);\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + log_base_below_negative_one, + "@use 'sass:math';\na {\n color: math.log(2, -2);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + #[ignore = "we do not support Infinity"] + log_base_one, + "@use 'sass:math';\na {\n color: math.log(2, 1);\n}\n", + "a {\n color: Infinity;\n}\n" +); +test!( + log_base_negative_one, + "@use 'sass:math';\na {\n color: math.log(2, -1);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + log_base_zero, + "@use 'sass:math';\na {\n color: math.log(2, 0);\n}\n", + "a {\n color: 0;\n}\n" +); +test!( + log_base_point_five, + "@use 'sass:math';\na {\n color: math.log(2, .5);\n}\n", + "a {\n color: -1;\n}\n" +); + +test!( + pow_exponent_and_base_one, + "@use 'sass:math';\na {\n color: math.pow(1, 1);\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + pow_exponent_and_base_ten, + "@use 'sass:math';\na {\n color: math.pow(10, 10);\n}\n", + "a {\n color: 10000000000;\n}\n" +); +test!( + pow_base_negative_exponent_positive, + "@use 'sass:math';\na {\n color: math.pow(-2, 3);\n}\n", + "a {\n color: -8;\n}\n" +); +test!( + pow_base_positive_exponent_negative, + "@use 'sass:math';\na {\n color: math.pow(2, -3);\n}\n", + "a {\n color: 0.125;\n}\n" +); +test!( + pow_base_negative_exponent_negative, + "@use 'sass:math';\na {\n color: math.pow(-2, -3);\n}\n", + "a {\n color: -0.125;\n}\n" +); +test!( + pow_base_decimal, + "@use 'sass:math';\na {\n color: math.pow(2.4, 3);\n}\n", + "a {\n color: 13.824;\n}\n" +); +test!( + pow_exponent_decimal, + "@use 'sass:math';\na {\n color: math.pow(2, 3.5);\n}\n", + "a {\n color: 11.313708499;\n}\n" +); +test!( + pow_base_nan, + "@use 'sass:math';\na {\n color: math.pow((0 / 0), 3);\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + pow_exponent_nan, + "@use 'sass:math';\na {\n color: math.pow(2, (0 / 0));\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + pow_base_and_exponent_nan, + "@use 'sass:math';\na {\n color: math.pow((0 / 0), (0 / 0));\n}\n", + "a {\n color: NaN;\n}\n" +); +test!( + pow_exponent_zero, + "@use 'sass:math';\na {\n color: math.pow(2, 0);\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + hypot_all_same_unit, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2px, 3px, 4px, 5px);\n}\n", + "a {\n color: 7.4161984871px;\n}\n" +); +test!( + hypot_negative, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2px, 3px, 4px, 5px, -20px);\n}\n", + "a {\n color: 21.3307290077px;\n}\n" +); +test!( + hypot_all_different_but_comparable_unit, + "@use 'sass:math';\na {\n color: math.hypot(1in, 2cm, 3mm, 4pt, 5pc);\n}\n", + "a {\n color: 1.5269191636in;\n}\n" +); +test!( + hypot_all_no_unit, + "@use 'sass:math';\na {\n color: math.hypot(1, 2, 3);\n}\n", + "a {\n color: 3.7416573868;\n}\n" +); +test!( + hypot_nan_has_comparable_unit, + "@use 'sass:math';\na {\n color: math.hypot(1deg, 2deg, math.acos(2));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +error!( + hypot_no_args, + "@use 'sass:math';\na {\n color: math.hypot();\n}\n", + "Error: At least one argument must be passed." +); +error!( + hypot_first_has_no_unit_third_has_unit, + "@use 'sass:math';\na {\n color: math.hypot(1, 2, 3px);\n}\n", + "Error: Argument 1 is unitless but argument 3 has unit px. Arguments must all have units or all be unitless." +); +error!( + hypot_non_numeric_argument, + "@use 'sass:math';\na {\n color: math.hypot(1, red, 3);\n}\n", "Error: red is not a number." +); +error!( + hypot_units_not_comparable, + "@use 'sass:math';\na {\n color: math.hypot(1px, 2in, 3rem);\n}\n", + "Error: Incompatible units px and rem." +); +error!( + hypot_nan_has_no_unit_but_first_has_unit, + "@use 'sass:math';\na {\n color: math.hypot(1deg, 2deg, (0 / 0));\n}\n", + "Error: Argument 1 has unit deg but argument 3 is unitless. Arguments must all have units or all be unitless." +); +test!( + atan2_both_positive, + "@use 'sass:math';\na {\n color: math.atan2(3, 4);\n}\n", + "a {\n color: 36.8698976458deg;\n}\n" +); +test!( + atan2_first_negative, + "@use 'sass:math';\na {\n color: math.atan2(-3, 4);\n}\n", + "a {\n color: -36.8698976458deg;\n}\n" +); +test!( + atan2_second_negative, + "@use 'sass:math';\na {\n color: math.atan2(3, -4);\n}\n", + "a {\n color: 143.1301023542deg;\n}\n" +); +test!( + atan2_both_negative, + "@use 'sass:math';\na {\n color: math.atan2(-3, -4);\n}\n", + "a {\n color: -143.1301023542deg;\n}\n" +); +test!( + atan2_first_positive_second_zero, + "@use 'sass:math';\na {\n color: math.atan2(3, 0);\n}\n", + "a {\n color: 90deg;\n}\n" +); +test!( + atan2_first_negative_second_zero, + "@use 'sass:math';\na {\n color: math.atan2(-3, 0);\n}\n", + "a {\n color: -90deg;\n}\n" +); +test!( + atan2_first_zero_second_positive, + "@use 'sass:math';\na {\n color: math.atan2(0, 4);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + atan2_first_zero_second_negative, + "@use 'sass:math';\na {\n color: math.atan2(0, -4);\n}\n", + "a {\n color: 180deg;\n}\n" +); +test!( + atan2_both_zero, + "@use 'sass:math';\na {\n color: math.atan2(0, 0);\n}\n", + "a {\n color: 0deg;\n}\n" +); +test!( + atan2_both_same_unit, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4px);\n}\n", + "a {\n color: 36.8698976458deg;\n}\n" +); +test!( + atan2_both_different_but_comparable_unit, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4in);\n}\n", + "a {\n color: 0.4476141709deg;\n}\n" +); +error!( + atan2_first_unitless_second_unit, + "@use 'sass:math';\na {\n color: math.atan2(3, 4rem);\n}\n", + "Error: $y is unitless but $x has unit rem. Arguments must all have units or all be unitless." +); +error!( + atan2_first_unit_second_unitless, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4);\n}\n", + "Error: $y has unit px but $x is unitless. Arguments must all have units or all be unitless." +); +error!( + atan2_incompatible_units, + "@use 'sass:math';\na {\n color: math.atan2(3px, 4rem);\n}\n", + "Error: Incompatible units px and rem." +); +error!( + atan2_nan_incompatible_units, + "@use 'sass:math';\na {\n color: math.atan2(math.acos(2), 3);\n}\n", + "Error: $y has unit deg but $x is unitless. Arguments must all have units or all be unitless." +); +test!( + atan2_first_nan, + "@use 'sass:math';\na {\n color: math.atan2((0/0), 0);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_second_nan, + "@use 'sass:math';\na {\n color: math.atan2(0, (0/0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_both_nan, + "@use 'sass:math';\na {\n color: math.atan2((0/0), (0/0));\n}\n", + "a {\n color: NaNdeg;\n}\n" +); +test!( + atan2_nan_with_same_units, + "@use 'sass:math';\na {\n color: math.atan2(math.acos(2), 3deg);\n}\n", + "a {\n color: NaNdeg;\n}\n" +); diff --git a/tests/meta-module.rs b/tests/meta-module.rs new file mode 100644 index 0000000..e6ee9ba --- /dev/null +++ b/tests/meta-module.rs @@ -0,0 +1,65 @@ +#![cfg(test)] + +use std::io::Write; + +#[macro_use] +mod macros; + +test!( + module_functions_builtin, + "@use 'sass:meta';\na {\n color: inspect(meta.module-functions(meta));\n}\n", + "a {\n color: (\"feature-exists\": get-function(\"feature-exists\"), \"inspect\": get-function(\"inspect\"), \"type-of\": get-function(\"type-of\"), \"keywords\": get-function(\"keywords\"), \"global-variable-exists\": get-function(\"global-variable-exists\"), \"variable-exists\": get-function(\"variable-exists\"), \"function-exists\": get-function(\"function-exists\"), \"mixin-exists\": get-function(\"mixin-exists\"), \"content-exists\": get-function(\"content-exists\"), \"module-variables\": get-function(\"module-variables\"), \"module-functions\": get-function(\"module-functions\"), \"get-function\": get-function(\"get-function\"), \"call\": get-function(\"call\"));\n}\n" +); +test!( + module_variables_builtin, + "@use 'sass:meta';\n@use 'sass:math';\na {\n color: inspect(meta.module-variables(math));\n}\n", + "a {\n color: (\"e\": 2.7182818285, \"pi\": 3.1415926536);\n}\n" +); +test!( + global_var_exists_module, + "@use 'sass:math';\na {\n color: global-variable-exists(pi, $module: math);\n}\n", + "a {\n color: true;\n}\n" +); + +#[test] +fn mixin_exists_module() { + let input = "@use \"mixin_exists_module\" as module;\na {\n color: mixin-exists(foo, $module: module);\n}"; + tempfile!("mixin_exists_module.scss", "@mixin foo {}"); + assert_eq!( + "a {\n color: true;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn load_css_simple() { + let input = "@use \"sass:meta\";\na {\n @include meta.load-css(load_css_simple);\n}"; + tempfile!("load_css_simple.scss", "a { color: red; }"); + assert_eq!( + "a a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn load_css_explicit_args() { + let input = "@use \"sass:meta\";\na {\n @include meta.load-css($module: load_css_explicit_args, $with: null);\n}"; + tempfile!("load_css_explicit_args.scss", "a { color: red; }"); + assert_eq!( + "a a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn load_css_non_string_url() { + let input = "@use \"sass:meta\";\na {\n @include meta.load-css(2);\n}"; + tempfile!("load_css_non_string_url.scss", "a { color: red; }"); + assert_err!("Error: $module: 2 is not a string.", input); +} + +#[test] +fn load_css_non_map_with() { + let input = "@use \"sass:meta\";\na {\n @include meta.load-css(foo, 2);\n}"; + assert_err!("Error: $with: 2 is not a map.", input); +} diff --git a/tests/meta.rs b/tests/meta.rs index 6690635..56f4d4b 100644 --- a/tests/meta.rs +++ b/tests/meta.rs @@ -195,6 +195,11 @@ test!( "a {\n color: type-of(- 2)\n}\n", "a {\n color: number;\n}\n" ); +test!( + type_of_nan, + "a {\n color: type-of((0 / 0))\n}\n", + "a {\n color: number;\n}\n" +); test!( type_of_arglist, "@mixin foo($a...) {color: type-of($a);}\na {@include foo(1, 2, 3, 4, 5);}", diff --git a/tests/ordering.rs b/tests/ordering.rs index ae00b88..5a2b644 100644 --- a/tests/ordering.rs +++ b/tests/ordering.rs @@ -63,3 +63,8 @@ test!( "a {\n color: 0 < 1;\n}\n", "a {\n color: true;\n}\n" ); +test!( + ord_the_same_as_partial_ord, + "a {\n color: 2in > 1cm;\n}\n", + "a {\n color: true;\n}\n" +); diff --git a/tests/use.rs b/tests/use.rs new file mode 100644 index 0000000..0dca5c4 --- /dev/null +++ b/tests/use.rs @@ -0,0 +1,333 @@ +#![cfg(test)] + +use std::io::Write; + +#[macro_use] +mod macros; + +error!( + after_style, + "a {} + @use \"foo\"; + ", + "Error: @use rules must be written before any other rules." +); +error!( + interpolation_in_as_identifier, + "@use \"sass:math\" as m#{a}th;", "Error: expected \";\"." +); +error!( + use_as_quoted_string, + "@use \"sass:math\" as \"math\";", "Error: Expected identifier." +); +error!( + use_as_missing_s, + "@use \"sass:math\" a math;", "Error: expected \";\"." +); +error!( + unknown_module_get_variable, + "a { color: foo.$bar; }", "Error: There is no module with the namespace \"foo\"." +); +error!( + unknown_module_get_function, + "a { color: foo.bar(); }", "Error: There is no module with the namespace \"foo\"." +); +error!( + unknown_function, + "@use \"sass:math\";\na { color: math.bar(); }", "Error: Undefined function." +); +test!( + use_as, + "@use \"sass:math\" as foo; + a { + color: foo.clamp(0, 1, 2); + }", + "a {\n color: 1;\n}\n" +); +test!( + use_as_uppercase, + "@use \"sass:math\" AS foo; + a { + color: foo.clamp(0, 1, 2); + }", + "a {\n color: 1;\n}\n" +); +test!( + use_as_universal, + "@use \"sass:math\" as *; + a { + color: cos(2); + }", + "a {\n color: -0.4161468365;\n}\n" +); + +#[test] +fn use_user_defined_same_directory() { + let input = "@use \"use_user_defined_same_directory\";\na {\n color: use_user_defined_same_directory.$a;\n}"; + tempfile!( + "use_user_defined_same_directory.scss", + "$a: red; a { color: $a; }" + ); + assert_eq!( + "a {\n color: red;\n}\n\na {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn private_variable_begins_with_underscore() { + let input = "@use \"private_variable_begins_with_underscore\" as module;\na {\n color: module.$_foo;\n}"; + tempfile!( + "private_variable_begins_with_underscore.scss", + "$_foo: red; a { color: $_foo; }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn private_variable_begins_with_hyphen() { + let input = + "@use \"private_variable_begins_with_hyphen\" as module;\na {\n color: module.$-foo;\n}"; + tempfile!( + "private_variable_begins_with_hyphen.scss", + "$-foo: red; a { color: $-foo; }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn private_function() { + let input = "@use \"private_function\" as module;\na {\n color: module._foo(green);\n}"; + tempfile!( + "private_function.scss", + "@function _foo($a) { @return $a; } a { color: _foo(red); }" + ); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn global_variable_exists_private() { + let input = r#" + @use "global_variable_exists_private" as module; + a { + color: global-variable-exists($name: foo, $module: module); + color: global-variable-exists($name: _foo, $module: module); + }"#; + tempfile!( + "global_variable_exists_private.scss", + "$foo: red;\n$_foo: red;\n" + ); + + assert_eq!( + "a {\n color: true;\n color: false;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_user_defined_as() { + let input = "@use \"use_user_defined_as\" as module;\na {\n color: module.$a;\n}"; + tempfile!("use_user_defined_as.scss", "$a: red; a { color: $a; }"); + assert_eq!( + "a {\n color: red;\n}\n\na {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_user_defined_function() { + let input = "@use \"use_user_defined_function\" as module;\na {\n color: module.foo(red);\n}"; + tempfile!( + "use_user_defined_function.scss", + "@function foo($a) { @return $a; }" + ); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_idempotent_no_alias() { + let input = "@use \"use_idempotent_no_alias\";\n@use \"use_idempotent_no_alias\";\n"; + tempfile!("use_idempotent_no_alias.scss", ""); + + assert_err!( + "Error: There's already a module with namespace \"use-idempotent-no-alias\".", + input + ); +} + +#[test] +fn use_idempotent_with_alias() { + let input = "@use \"use_idempotent_with_alias__a\" as foo;\n@use \"use_idempotent_with_alias__b\" as foo;\n"; + tempfile!("use_idempotent_with_alias__a.scss", ""); + tempfile!("use_idempotent_with_alias__b.scss", ""); + + assert_err!( + "Error: There's already a module with namespace \"foo\".", + input + ); +} + +#[test] +fn use_idempotent_builtin() { + let input = "@use \"sass:math\";\n@use \"sass:math\";\n"; + + assert_err!( + "Error: There's already a module with namespace \"math\".", + input + ); +} + +#[test] +fn use_with_simple() { + let input = "@use \"use_with_simple\" with ($a: red);\na {\n color: use_with_simple.$a;\n}"; + tempfile!("use_with_simple.scss", "$a: green !default;"); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_as_with() { + let input = "@use \"use_as_with\" as module with ($a: red);\na {\n color: module.$a;\n}"; + tempfile!("use_as_with.scss", "$a: green !default;"); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_whitespace_and_comments() { + let input = "@use /**/ \"use_whitespace_and_comments\" /**/ as /**/ foo /**/ with /**/ ( /**/ $a /**/ : /**/ red /**/ ) /**/ ;"; + tempfile!( + "use_whitespace_and_comments.scss", + "$a: green !default; a { color: $a }" + ); + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_with_builtin_module() { + let input = "@use \"sass:math\" with ($e: 2.7);"; + + assert_err!("Error: Built-in modules can't be configured.", input); +} + +#[test] +fn use_with_variable_never_used() { + let input = "@use \"use_with_variable_never_used\" with ($a: red);"; + tempfile!("use_with_variable_never_used.scss", ""); + + assert_err!( + "Error: This variable was not declared with !default in the @used module.", + input + ); +} + +#[test] +fn use_with_same_variable_multiple_times() { + let input = "@use \"use_with_same_variable_multiple_times\" as foo with ($a: b, $a: c);"; + tempfile!("use_with_same_variable_multiple_times.scss", ""); + + assert_err!( + "Error: The same variable may only be configured once.", + input + ); +} + +#[test] +fn use_variable_redeclaration_var_dne() { + let input = "@use \"use_variable_redeclaration_var_dne\" as mod;\nmod.$a: red;"; + tempfile!("use_variable_redeclaration_var_dne.scss", ""); + + assert_err!("Error: Undefined variable.", input); +} + +#[test] +fn use_variable_redeclaration_global() { + let input = "@use \"use_variable_redeclaration_global\" as mod;\nmod.$a: red !global;"; + tempfile!("use_variable_redeclaration_global.scss", "$a: green;"); + + assert_err!( + "Error: !global isn't allowed for variables in other modules.", + input + ); +} + +#[test] +fn use_variable_redeclaration_simple() { + let input = + "@use \"use_variable_redeclaration_simple\" as mod;\nmod.$a: red; a { color: mod.$a; }"; + tempfile!("use_variable_redeclaration_simple.scss", "$a: green;"); + + assert_eq!( + "a {\n color: red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_variable_redeclaration_default() { + let input = "@use \"use_variable_redeclaration_default\" as mod;\nmod.$a: 1 % red !default; a { color: mod.$a; }"; + tempfile!("use_variable_redeclaration_default.scss", "$a: green;"); + + assert_eq!( + "a {\n color: green;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +} + +#[test] +fn use_variable_redeclaration_private() { + let input = "@use \"use_variable_redeclaration_private\" as mod;\nmod.$-a: red;"; + tempfile!("use_variable_redeclaration_private.scss", "$a: green;"); + + assert_err!( + "Error: Private members can't be accessed from outside their modules.", + input + ); +} + +#[test] +fn use_variable_redeclaration_builtin() { + let input = "@use \"sass:math\";\nmath.$e: red;"; + + assert_err!("Error: Cannot modify built-in variable.", input); +} + +#[test] +fn use_variable_declaration_between_use() { + let input = r#" + $a: red; + $b: green; + @use "sass:math"; + $b: red; + @use "sass:meta"; + a { + color: $a $b; + }"#; + + assert_eq!( + "a {\n color: red red;\n}\n", + &grass::from_string(input.to_string(), &grass::Options::default()).expect(input) + ); +}