implement @use and the module system

This is an MVP implementation of the module system. `@forward` is not yet supported and some aspects may be untested.
This commit is contained in:
Connor Skees 2020-08-07 02:36:07 -04:00 committed by GitHub
commit 484409761d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 3823 additions and 974 deletions

View File

@ -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 # 0.10.0
- bugfixes for `@media` query regressions - bugfixes for `@media` query regressions

View File

@ -25,7 +25,7 @@ The large features remaining are
``` ```
indented syntax indented syntax
css imports css imports
@use, @forward, and the module system @forward
compressed output compressed output
``` ```
@ -77,6 +77,13 @@ cargo b --release
These numbers come from a default run of the Sass specification as shown above. 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 2020-07-24
PASSING: 2935 PASSING: 2935

View File

@ -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<Vec<Stmt>>;
#[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<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Mixin::UserDefined(UserDefinedMixin::new(
args,
body,
accepts_content_block,
declared_at_root,
))
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct Mixin { pub(crate) struct UserDefinedMixin {
pub args: FuncArgs, pub args: FuncArgs,
pub body: Vec<Token>, pub body: Vec<Token>,
pub accepts_content_block: bool, pub accepts_content_block: bool,
pub declared_at_root: bool, pub declared_at_root: bool,
} }
impl Mixin { impl UserDefinedMixin {
pub fn new( pub fn new(
args: FuncArgs, args: FuncArgs,
body: Vec<Token>, body: Vec<Token>,
accepts_content_block: bool, accepts_content_block: bool,
declared_at_root: bool, declared_at_root: bool,
) -> Self { ) -> Self {
Mixin { Self {
args, args,
body, body,
accepts_content_block, accepts_content_block,

View File

@ -1,6 +1,5 @@
pub(crate) use function::Function; pub(crate) use function::Function;
pub(crate) use kind::AtRuleKind; pub(crate) use kind::AtRuleKind;
pub(crate) use mixin::{Content, Mixin};
pub(crate) use supports::SupportsRule; pub(crate) use supports::SupportsRule;
pub(crate) use unknown::UnknownAtRule; pub(crate) use unknown::UnknownAtRule;
@ -8,6 +7,6 @@ mod function;
pub mod keyframes; pub mod keyframes;
mod kind; mod kind;
pub mod media; pub mod media;
mod mixin; pub mod mixin;
mod supports; mod supports;
mod unknown; mod unknown;

View File

@ -35,7 +35,8 @@ fn inner_hsl(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
} }
let lightness = match channels.pop() { 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) => { Some(v) => {
return Err(( return Err((
format!("$lightness: {} is not a number.", v.inspect(args.span())?), 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() { 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) => { Some(v) => {
return Err(( return Err((
format!("$saturation: {} is not a number.", v.inspect(args.span())?), 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() { let hue = match channels.pop() {
Some(Value::Dimension(n, ..)) => n, Some(Value::Dimension(Some(n), ..)) => n,
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) => { Some(v) => {
return Err(( return Err((
format!("$hue: {} is not a number.", v.inspect(args.span())?), 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 { } else {
let hue = match args.get_err(0, "hue")? { 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() => { v if v.is_special_function() => {
let saturation = args.get_err(1, "saturation")?; let saturation = args.get_err(1, "saturation")?;
let lightness = args.get_err(2, "lightness")?; 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")? { 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() => { v if v.is_special_function() => {
let lightness = args.get_err(2, "lightness")?; let lightness = args.get_err(2, "lightness")?;
let mut string = format!( 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")? { 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() => { v if v.is_special_function() => {
let mut string = format!( 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( let alpha = match args.default_arg(
3, 3,
"alpha", "alpha",
Value::Dimension(Number::one(), Unit::None, true), Value::Dimension(Some(Number::one()), Unit::None, true),
)? { )? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( 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<Value> { pub(crate) fn hsl(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
inner_hsl("hsl", args, parser) inner_hsl("hsl", args, parser)
} }
fn hsla(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn hsla(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
inner_hsl("hsla", args, parser) inner_hsl("hsla", args, parser)
} }
fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -225,10 +232,10 @@ fn hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -237,10 +244,10 @@ fn saturation(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
} }
} }
fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -249,7 +256,7 @@ fn lightness(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -262,7 +269,8 @@ fn adjust_hue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
} }
}; };
let degrees = match args.get_err(1, "degrees")? { let degrees = match args.get_err(1, "degrees")? {
Value::Dimension(n, ..) => n, Value::Dimension(Some(n), ..) => n,
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!( format!(
@ -290,7 +298,8 @@ fn lighten(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!( format!(
@ -318,7 +327,8 @@ fn darken(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!( format!(
@ -346,7 +356,8 @@ fn saturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!( format!(
@ -360,7 +371,7 @@ fn saturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
}; };
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(n, u, _) => { Value::Dimension(Some(n), u, _) => {
return Ok(Value::String( return Ok(Value::String(
format!("saturate({}{})", n, u), format!("saturate({}{})", n, u),
QuoteKind::None, QuoteKind::None,
@ -390,7 +401,8 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!( format!(
@ -405,11 +417,11 @@ fn desaturate(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
Ok(Value::Color(Box::new(color.desaturate(amount)))) Ok(Value::Color(Box::new(color.desaturate(amount))))
} }
fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
Value::Dimension(n, u, _) => { Value::Dimension(Some(n), u, _) => {
return Ok(Value::String( return Ok(Value::String(
format!("grayscale({}{})", n, u), format!("grayscale({}{})", n, u),
QuoteKind::None, QuoteKind::None,
@ -426,7 +438,7 @@ fn grayscale(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::Color(Box::new(color.desaturate(Number::one())))) Ok(Value::Color(Box::new(color.desaturate(Number::one()))))
} }
fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -441,14 +453,15 @@ fn complement(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
Ok(Value::Color(Box::new(color.complement()))) Ok(Value::Color(Box::new(color.complement())))
} }
fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let weight = match args.default_arg( let weight = match args.default_arg(
1, 1,
"weight", "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 => { v => {
return Err(( return Err((
format!( format!(
@ -462,9 +475,10 @@ fn invert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
}; };
match args.get_err(0, "color")? { match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Color(Box::new(c.invert(weight)))), 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)) Ok(Value::String(format!("invert({}%)", n), QuoteKind::None))
} }
Value::Dimension(None, ..) => todo!(),
Value::Dimension(..) => Err(( Value::Dimension(..) => Err((
"Only one argument may be passed to the plain-CSS invert() function.", "Only one argument may be passed to the plain-CSS invert() function.",
args.span(), args.span(),

View File

@ -1,9 +1,9 @@
use super::{Builtin, GlobalFunctionMap}; use super::{Builtin, GlobalFunctionMap};
mod hsl; pub mod hsl;
mod opacity; pub mod opacity;
mod other; pub mod other;
mod rgb; pub mod rgb;
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {
hsl::declare(f); hsl::declare(f);

View File

@ -5,35 +5,18 @@ use crate::{
value::Value, value::Value,
}; };
/// Check if `s` matches the regex `^[a-zA-Z]+\s*=`
fn is_ms_filter(s: &str) -> bool { fn is_ms_filter(s: &str) -> bool {
let mut chars = s.chars(); let mut bytes = s.bytes();
if let Some(c) = chars.next() { if !bytes.next().map_or(false, |c| c.is_ascii_alphabetic()) {
if !matches!(c, 'a'..='z' | 'A'..='Z') {
return false;
}
} else {
return false; return false;
} }
for c in &mut chars { bytes
match c { .skip_while(u8::is_ascii_alphabetic)
' ' | '\t' | '\n' => break, .find(|c| !matches!(c, b' ' | b'\t' | b'\n'))
'a'..='z' | 'A'..='Z' => continue, == Some(b'=')
'=' => return true,
_ => return false,
}
}
for c in chars {
match c {
' ' | '\t' | '\n' => continue,
'=' => return true,
_ => return false,
}
}
false
} }
#[cfg(test)] #[cfg(test)]
@ -52,10 +35,10 @@ mod test {
} }
} }
fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
if args.len() <= 1 { if args.len() <= 1 {
match args.get_err(0, "color")? { 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) => { Value::String(s, QuoteKind::None) if is_ms_filter(&s) => {
Ok(Value::String(format!("alpha({})", s), QuoteKind::None)) Ok(Value::String(format!("alpha({})", s), QuoteKind::None))
} }
@ -86,14 +69,15 @@ fn alpha(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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::Dimension(num, unit, _) => Ok(Value::String( Value::Dimension(Some(num), unit, _) => Ok(Value::String(
format!("opacity({}{})", num, unit), format!("opacity({}{})", num, unit),
QuoteKind::None, QuoteKind::None,
)), )),
Value::Dimension(None, ..) => todo!(),
v => Err(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -102,6 +86,7 @@ fn opacity(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
// todo: unify `opacify` and `fade_in`
fn opacify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn opacify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
@ -115,7 +100,8 @@ fn opacify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?), format!("$amount: {} is not a number.", v.inspect(args.span())?),
@ -140,7 +126,8 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?), format!("$amount: {} is not a number.", v.inspect(args.span())?),
@ -152,6 +139,7 @@ fn fade_in(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::Color(Box::new(color.fade_in(amount)))) Ok(Value::Color(Box::new(color.fade_in(amount))))
} }
// todo: unify with `fade_out`
fn transparentize(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn transparentize(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
@ -165,7 +153,8 @@ fn transparentize(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Val
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?), format!("$amount: {} is not a number.", v.inspect(args.span())?),
@ -190,7 +179,8 @@ fn fade_out(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let amount = match args.get_err(1, "amount")? { 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 => { v => {
return Err(( return Err((
format!("$amount: {} is not a number.", v.inspect(args.span())?), format!("$amount: {} is not a number.", v.inspect(args.span())?),

View File

@ -15,7 +15,8 @@ use crate::{
macro_rules! opt_rgba { macro_rules! opt_rgba {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { 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, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -31,9 +32,10 @@ macro_rules! opt_rgba {
macro_rules! opt_hsl { macro_rules! opt_hsl {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { 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)) Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100))
} }
Value::Dimension(None, ..) => todo!(),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -46,7 +48,7 @@ macro_rules! opt_hsl {
}; };
} }
fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
if args.positional_arg(1).is_some() { if args.positional_arg(1).is_some() {
return Err(( return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.", "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<Value
} }
let hue = match args.default_named_arg("hue", Value::Null)? { let hue = match args.default_named_arg("hue", Value::Null)? {
Value::Dimension(n, ..) => Some(n), Value::Dimension(Some(n), ..) => Some(n),
Value::Dimension(None, ..) => todo!(),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -113,7 +116,7 @@ fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
})) }))
} }
fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,
v => { v => {
@ -140,7 +143,8 @@ fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
} }
let hue = match args.default_named_arg("hue", Value::Null)? { let hue = match args.default_named_arg("hue", Value::Null)? {
Value::Dimension(n, ..) => Some(n), Value::Dimension(Some(n), ..) => Some(n),
Value::Dimension(None, ..) => todo!(),
Value::Null => None, Value::Null => None,
v => { v => {
return Err(( return Err((
@ -175,8 +179,8 @@ fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
#[allow(clippy::cognitive_complexity)] #[allow(clippy::cognitive_complexity)]
// todo: refactor into rgb and hsl? // todo: refactor into rgb and hsl?
fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
fn scale(val: Number, by: Number, max: Number) -> Number { pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
if by.is_zero() { if by.is_zero() {
return val; return val;
} }
@ -198,9 +202,10 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
macro_rules! opt_scale_arg { macro_rules! opt_scale_arg {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? { 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)) Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100))
} }
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -288,7 +293,7 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
})) }))
} }
fn ie_hex_str(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn ie_hex_str(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let color = match args.get_err(0, "color")? { let color = match args.get_err(0, "color")? {
Value::Color(c) => c, Value::Color(c) => c,

View File

@ -38,10 +38,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
} }
let blue = match channels.pop() { let blue = match channels.pop() {
Some(Value::Dimension(n, Unit::None, _)) => n, Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => { Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255) (n / Number::from(100)) * Number::from(255)
} }
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => { Some(v) if v.is_special_function() => {
let green = channels.pop().unwrap(); let green = channels.pop().unwrap();
let red = 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() { let green = match channels.pop() {
Some(Value::Dimension(n, Unit::None, _)) => n, Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => { Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255) (n / Number::from(100)) * Number::from(255)
} }
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => { Some(v) if v.is_special_function() => {
let string = match channels.pop() { let string = match channels.pop() {
Some(red) => format!( Some(red) => format!(
@ -95,10 +97,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
}; };
let red = match channels.pop() { let red = match channels.pop() {
Some(Value::Dimension(n, Unit::None, _)) => n, Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => { Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255) (n / Number::from(100)) * Number::from(255)
} }
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => { Some(v) if v.is_special_function() => {
return Ok(Value::String( return Ok(Value::String(
format!( 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")? { let alpha = match args.get_err(1, "alpha")? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( 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)))) Ok(Value::Color(Box::new(color.with_alpha(alpha))))
} else { } else {
let red = match args.get_err(0, "red")? { let red = match args.get_err(0, "red")? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( 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")? { let green = match args.get_err(1, "green")? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( 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")? { let blue = match args.get_err(2, "blue")? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255), Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -297,10 +310,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
let alpha = match args.default_arg( let alpha = match args.default_arg(
3, 3,
"alpha", "alpha",
Value::Dimension(Number::one(), Unit::None, true), Value::Dimension(Some(Number::one()), Unit::None, true),
)? { )? {
Value::Dimension(n, Unit::None, _) => n, Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => n / Number::from(100), Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( 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<Value> { pub(crate) fn rgb(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
inner_rgb("rgb", args, parser) inner_rgb("rgb", args, parser)
} }
fn rgba(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn rgba(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
inner_rgb("rgba", args, parser) inner_rgb("rgba", args, parser)
} }
fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -356,10 +370,10 @@ fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -368,10 +382,10 @@ fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "color")? { 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(( v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?), format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(), args.span(),
@ -380,7 +394,7 @@ fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let color1 = match args.get_err(0, "color1")? { let color1 = match args.get_err(0, "color1")? {
Value::Color(c) => c, Value::Color(c) => c,
@ -407,9 +421,10 @@ fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let weight = match args.default_arg( let weight = match args.default_arg(
2, 2,
"weight", "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 => { v => {
return Err(( return Err((
format!( format!(

View File

@ -11,20 +11,21 @@ use crate::{
value::{Number, Value}, value::{Number, Value},
}; };
fn length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::Dimension( 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, Unit::None,
true, true,
)) ))
} }
fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let mut list = args.get_err(0, "list")?.as_list(); let mut list = args.get_err(0, "list")?.as_list();
let n = match args.get_err(1, "n")? { let n = match args.get_err(1, "n")? {
Value::Dimension(num, ..) => num, Value::Dimension(Some(num), ..) => num,
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!("$n: {} is not a number.", v.inspect(args.span())?), format!("$n: {} is not a number.", v.inspect(args.span())?),
@ -61,7 +62,7 @@ fn nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
})) }))
} }
fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::String( Ok(Value::String(
match args.get_err(0, "list")? { match args.get_err(0, "list")? {
@ -73,7 +74,7 @@ fn list_separator(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Val
)) ))
} }
fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? { let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b), Value::List(v, sep, b) => (v, sep, b),
@ -81,7 +82,8 @@ fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
v => (vec![v], ListSeparator::Space, Brackets::None), v => (vec![v], ListSeparator::Space, Brackets::None),
}; };
let n = match args.get_err(1, "n")? { let n = match args.get_err(1, "n")? {
Value::Dimension(num, ..) => num, Value::Dimension(Some(num), ..) => num,
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!("$n: {} is not a number.", v.inspect(args.span())?), format!("$n: {} is not a number.", v.inspect(args.span())?),
@ -120,7 +122,7 @@ fn set_nth(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::List(list, sep, brackets)) Ok(Value::List(list, sep, brackets))
} }
fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (mut list, sep, brackets) = match args.get_err(0, "list")? { let (mut list, sep, brackets) = match args.get_err(0, "list")? {
Value::List(v, sep, b) => (v, sep, b), Value::List(v, sep, b) => (v, sep, b),
@ -158,7 +160,7 @@ fn append(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::List(list, sep, brackets)) Ok(Value::List(list, sep, brackets))
} }
fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(4)?; args.max_args(4)?;
let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? {
Value::List(v, sep, brackets) => (v, sep, brackets), Value::List(v, sep, brackets) => (v, sep, brackets),
@ -225,7 +227,7 @@ fn join(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::List(list1, sep, brackets)) Ok(Value::List(list1, sep, brackets))
} }
fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::bool(match args.get_err(0, "list")? { Ok(Value::bool(match args.get_err(0, "list")? {
Value::List(.., brackets) => match brackets { Value::List(.., brackets) => match brackets {
@ -236,7 +238,7 @@ fn is_bracketed(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
})) }))
} }
fn index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let list = args.get_err(0, "list")?.as_list(); let list = args.get_err(0, "list")?.as_list();
let value = args.get_err(1, "value")?; let value = args.get_err(1, "value")?;
@ -244,10 +246,10 @@ fn index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Some(v) => Number::from(v + 1), Some(v) => Number::from(v + 1),
None => return Ok(Value::Null), 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<Value> { pub(crate) fn zip(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let lists = args let lists = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()

View File

@ -8,7 +8,7 @@ use crate::{
value::{SassMap, Value}, value::{SassMap, Value},
}; };
fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let key = args.get_err(1, "key")?; let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
@ -26,7 +26,7 @@ fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(map.get(&key)?.unwrap_or(Value::Null)) Ok(map.get(&key)?.unwrap_or(Value::Null))
} }
fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let key = args.get_err(1, "key")?; let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
@ -44,7 +44,7 @@ fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
Ok(Value::bool(map.get(&key)?.is_some())) Ok(Value::bool(map.get(&key)?.is_some()))
} }
fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
@ -65,7 +65,7 @@ fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
)) ))
} }
fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let map = match args.get_err(0, "map")? { let map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
@ -86,7 +86,7 @@ fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
)) ))
} }
fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let mut map1 = match args.get_err(0, "map1")? { let mut map1 = match args.get_err(0, "map1")? {
Value::Map(m) => m, Value::Map(m) => m,
@ -116,7 +116,7 @@ fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::Map(map1)) Ok(Value::Map(map1))
} }
fn map_remove(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn map_remove(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let mut map = match args.get_err(0, "map")? { let mut map = match args.get_err(0, "map")? {
Value::Map(m) => m, Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(), Value::List(v, ..) if v.is_empty() => SassMap::new(),

View File

@ -13,10 +13,11 @@ use crate::{
value::{Number, Value}, value::{Number, Value},
}; };
fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let num = match args.get_err(0, "number")? { 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(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -35,13 +36,14 @@ fn percentage(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
.into()) .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<Value> { pub(crate) fn round(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { 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(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -50,10 +52,11 @@ fn round(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { 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(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -62,10 +65,11 @@ fn ceil(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { 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(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -74,10 +78,11 @@ fn floor(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "number")? { 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(( v => Err((
format!("$number: {} is not a number.", v.inspect(args.span())?), format!("$number: {} is not a number.", v.inspect(args.span())?),
args.span(), args.span(),
@ -86,7 +91,7 @@ fn abs(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let unit1 = match args.get_err(0, "number1")? { let unit1 = match args.get_err(0, "number1")? {
Value::Dimension(_, u, _) => u, Value::Dimension(_, u, _) => u,
@ -114,14 +119,15 @@ fn comparable(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
// TODO: write tests for this // TODO: write tests for this
#[cfg(feature = "random")] #[cfg(feature = "random")]
fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let limit = match args.default_arg(0, "limit", Value::Null)? { 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 => { Value::Null => {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
return Ok(Value::Dimension( 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, Unit::None,
true, true,
)); ));
@ -136,7 +142,7 @@ fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
}; };
if limit.is_one() { 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() { if limit.is_decimal() {
@ -164,20 +170,21 @@ fn random(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let mut rng = rand::thread_rng(); let mut rng = rand::thread_rng();
Ok(Value::Dimension( Ok(Value::Dimension(
Number::from(rng.gen_range(0, limit) + 1), Some(Number::from(rng.gen_range(0, limit) + 1)),
Unit::None, Unit::None,
true, true,
)) ))
} }
fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.min_args(1)?; args.min_args(1)?;
let span = args.span(); let span = args.span();
let mut nums = args let mut nums = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()
.map(|val| match val.node { .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()), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
}) })
.collect::<SassResult<Vec<(Number, Unit)>>>()? .collect::<SassResult<Vec<(Number, Unit)>>>()?
@ -190,12 +197,12 @@ fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
if ValueVisitor::new(parser, span) if ValueVisitor::new(parser, span)
.less_than( .less_than(
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
num.0.clone(), Some(num.0.clone()),
num.1.clone(), num.1.clone(),
true, true,
)), )),
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
min.0.clone(), Some(min.0.clone()),
min.1.clone(), min.1.clone(),
true, true,
)), )),
@ -205,17 +212,18 @@ fn min(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
min = num; 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<Value> { pub(crate) fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.min_args(1)?; args.min_args(1)?;
let span = args.span(); let span = args.span();
let mut nums = args let mut nums = args
.get_variadic()? .get_variadic()?
.into_iter() .into_iter()
.map(|val| match val.node { .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()), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()),
}) })
.collect::<SassResult<Vec<(Number, Unit)>>>()? .collect::<SassResult<Vec<(Number, Unit)>>>()?
@ -228,12 +236,12 @@ fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
if ValueVisitor::new(parser, span) if ValueVisitor::new(parser, span)
.greater_than( .greater_than(
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
num.0.clone(), Some(num.0.clone()),
num.1.clone(), num.1.clone(),
true, true,
)), )),
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
max.0.clone(), Some(max.0.clone()),
max.1.clone(), max.1.clone(),
true, true,
)), )),
@ -243,7 +251,7 @@ fn max(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
max = num; 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) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {

View File

@ -7,7 +7,6 @@ use crate::{
common::{Identifier, QuoteKind}, common::{Identifier, QuoteKind},
error::SassResult, error::SassResult,
parse::Parser, parse::Parser,
unit::Unit,
value::{SassFunction, Value}, value::{SassFunction, Value},
}; };
@ -20,7 +19,7 @@ fn if_(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "feature")? { match args.get_err(0, "feature")? {
#[allow(clippy::match_same_arms)] #[allow(clippy::match_same_arms)]
@ -50,7 +49,7 @@ fn feature_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Val
} }
} }
fn unit(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn unit(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let unit = match args.get_err(0, "number")? { let unit = match args.get_err(0, "number")? {
Value::Dimension(_, u, _) => u.to_string(), Value::Dimension(_, u, _) => u.to_string(),
@ -65,23 +64,18 @@ fn unit(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
Ok(Value::String(unit, QuoteKind::Quoted)) Ok(Value::String(unit, QuoteKind::Quoted))
} }
fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn type_of(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
let value = args.get_err(0, "value")?; let value = args.get_err(0, "value")?;
Ok(Value::String(value.kind().to_owned(), QuoteKind::None)) Ok(Value::String(value.kind().to_owned(), QuoteKind::None))
} }
fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn unitless(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
#[allow(clippy::match_same_arms)] Ok(Value::bool(args.get_err(0, "number")?.unitless()))
Ok(match args.get_err(0, "number")? {
Value::Dimension(_, Unit::None, _) => Value::True,
Value::Dimension(..) => Value::False,
_ => Value::True,
})
} }
fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(Value::String( Ok(Value::String(
args.get_err(0, "value")?.inspect(args.span())?.into_owned(), args.get_err(0, "value")?.inspect(args.span())?.into_owned(),
@ -89,7 +83,7 @@ fn inspect(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
)) ))
} }
fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "name")? { match args.get_err(0, "name")? {
Value::String(s, _) => Ok(Value::bool( Value::String(s, _) => Ok(Value::bool(
@ -103,33 +97,81 @@ fn variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Va
} }
} }
fn global_variable_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn global_variable_exists(
args.max_args(1)?; mut args: CallArgs,
match args.get_err(0, "name")? { parser: &mut Parser<'_>,
Value::String(s, _) => Ok(Value::bool(parser.global_scope.var_exists(s.into()))), ) -> SassResult<Value> {
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<Value> {
args.max_args(2)?; args.max_args(2)?;
match args.get_err(0, "name")? {
Value::String(s, _) => Ok(Value::bool( let name: Identifier = match args.get_err(0, "name")? {
parser.scopes.mixin_exists(s.into(), parser.global_scope), Value::String(s, _) => s.into(),
)), v => {
v => Err(( return Err((
format!("$name: {} is not a string.", v.inspect(args.span())?), format!("$name: {} is not a string.", v.inspect(args.span())?),
args.span(), args.span(),
) )
.into()), .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<Value> { pub(crate) fn mixin_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
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<Value> {
args.max_args(2)?; args.max_args(2)?;
match args.get_err(0, "name")? { match args.get_err(0, "name")? {
Value::String(s, _) => Ok(Value::bool( Value::String(s, _) => Ok(Value::bool(
@ -143,7 +185,7 @@ fn function_exists(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Va
} }
} }
fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let name: Identifier = match args.get_err(0, "name")? { let name: Identifier = match args.get_err(0, "name")? {
Value::String(s, _) => s.into(), Value::String(s, _) => s.into(),
@ -168,22 +210,26 @@ fn get_function(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
} }
}; };
if module.is_some() && css { let func = match if let Some(module_name) = module {
return Err(( if css {
"$css and $module may not both be passed at once.", return Err((
args.span(), "$css and $module may not both be passed at once.",
) args.span(),
.into()); )
} .into());
}
let func = match parser.scopes.get_fn( parser
Spanned { .modules
node: name, .get(module_name.into(), args.span())?
span: args.span(), .get_fn(Spanned {
}, node: name,
parser.global_scope, span: args.span(),
) { })?
Some(f) => SassFunction::UserDefined(Box::new(f), name), } else {
parser.scopes.get_fn(name, parser.global_scope)
} {
Some(f) => f,
None => match GLOBAL_FUNCTIONS.get(name.as_str()) { None => match GLOBAL_FUNCTIONS.get(name.as_str()) {
Some(f) => SassFunction::Builtin(f.clone(), name), Some(f) => SassFunction::Builtin(f.clone(), name),
None => return Err((format!("Function not found: {}", name), args.span()).into()), 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<Value
Ok(Value::FunctionRef(func)) Ok(Value::FunctionRef(func))
} }
fn call(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn call(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let func = match args.get_err(0, "function")? { let func = match args.get_err(0, "function")? {
Value::FunctionRef(f) => f, Value::FunctionRef(f) => f,
v => { v => {
@ -211,7 +257,7 @@ fn call(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(0)?; args.max_args(0)?;
if !parser.flags.in_mixin() { if !parser.flags.in_mixin() {
return Err(( return Err((
@ -225,6 +271,13 @@ fn content_exists(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
)) ))
} }
#[allow(unused_variables)]
pub(crate) fn keywords(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?;
drop(args);
todo!("builtin function `keywords` blocked on better handling of call args")
}
pub(crate) fn declare(f: &mut GlobalFunctionMap) { pub(crate) fn declare(f: &mut GlobalFunctionMap) {
f.insert("if", Builtin::new(if_)); f.insert("if", Builtin::new(if_));
f.insert("feature-exists", Builtin::new(feature_exists)); 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("get-function", Builtin::new(get_function));
f.insert("call", Builtin::new(call)); f.insert("call", Builtin::new(call));
f.insert("content-exists", Builtin::new(content_exists)); f.insert("content-exists", Builtin::new(content_exists));
f.insert("keywords", Builtin::new(keywords));
} }

View File

@ -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<Value>,
usize,
);
impl Builtin {
pub fn new(body: fn(CallArgs, &mut Parser<'_>) -> SassResult<Value>) -> 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<GlobalFunctionMap> = 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
});

View File

@ -9,7 +9,7 @@ use crate::{
value::Value, value::Value,
}; };
fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let parent_selector = args let parent_selector = args
.get_err(0, "super")? .get_err(0, "super")?
@ -21,7 +21,7 @@ fn is_superselector(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<V
)) ))
} }
fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
// todo: Value::to_compound_selector // todo: Value::to_compound_selector
let selector = args let selector = args
@ -51,7 +51,7 @@ fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<V
)) ))
} }
fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
Ok(args Ok(args
.get_err(0, "selector")? .get_err(0, "selector")?
@ -59,7 +59,7 @@ fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Val
.into_value()) .into_value())
} }
fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let span = args.span(); let span = args.span();
let selectors = args.get_variadic()?; let selectors = args.get_variadic()?;
if selectors.is_empty() { if selectors.is_empty() {
@ -80,7 +80,7 @@ fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
.into_value()) .into_value())
} }
fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let span = args.span(); let span = args.span();
let selectors = args.get_variadic()?; let selectors = args.get_variadic()?;
if selectors.is_empty() { if selectors.is_empty() {
@ -138,7 +138,7 @@ fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
.into_value()) .into_value())
} }
fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let selector = args let selector = args
.get_err(0, "selector")? .get_err(0, "selector")?
@ -153,7 +153,7 @@ fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Va
Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list()) Ok(Extender::extend(selector.0, source.0, target.0, args.span())?.to_sass_list())
} }
fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let selector = args let selector = args
.get_err(0, "selector")? .get_err(0, "selector")?
@ -167,7 +167,7 @@ fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<V
Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list()) Ok(Extender::replace(selector.0, source.0, target.0, args.span())?.to_sass_list())
} }
fn selector_unify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn selector_unify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let selector1 = args let selector1 = args
.get_err(0, "selector1")? .get_err(0, "selector1")?

View File

@ -15,7 +15,7 @@ use crate::{
value::{Number, Value}, value::{Number, Value},
}; };
fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(mut i, q) => { Value::String(mut i, q) => {
@ -30,7 +30,7 @@ fn to_upper_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Valu
} }
} }
fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(mut i, q) => { Value::String(mut i, q) => {
@ -45,11 +45,11 @@ fn to_lower_case(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Valu
} }
} }
fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(i, _) => Ok(Value::Dimension( Value::String(i, _) => Ok(Value::Dimension(
Number::from(i.chars().count()), Some(Number::from(i.chars().count())),
Unit::None, Unit::None,
true, true,
)), )),
@ -61,7 +61,7 @@ fn str_length(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
} }
} }
fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)), Value::String(i, _) => Ok(Value::String(i, QuoteKind::Quoted)),
@ -73,7 +73,7 @@ fn quote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(1)?; args.max_args(1)?;
match args.get_err(0, "string")? { match args.get_err(0, "string")? {
i @ Value::String(..) => Ok(i.unquote()), i @ Value::String(..) => Ok(i.unquote()),
@ -85,7 +85,7 @@ fn unquote(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (string, quotes) = match args.get_err(0, "string")? { let (string, quotes) = match args.get_err(0, "string")? {
Value::String(s, q) => (s, q), Value::String(s, q) => (s, q),
@ -99,17 +99,18 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
}; };
let str_len = string.chars().count(); let str_len = string.chars().count();
let start = match args.get_err(1, "start-at")? { 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()) 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) n.to_integer().to_usize().unwrap_or(str_len + 1)
} }
Value::Dimension(n, Unit::None, _) if n.is_zero() => 1_usize, Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 1_usize,
Value::Dimension(n, Unit::None, _) if n < -Number::from(str_len) => 1_usize, Value::Dimension(Some(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, _) => (n.to_integer() + BigInt::from(str_len + 1))
.to_usize() .to_usize()
.unwrap(), .unwrap(),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -129,17 +130,18 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
}; };
let mut end = match args.default_arg(2, "end-at", Value::Null)? { 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()) 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) n.to_integer().to_usize().unwrap_or(str_len + 1)
} }
Value::Dimension(n, Unit::None, _) if n.is_zero() => 0_usize, Value::Dimension(Some(n), Unit::None, _) if n.is_zero() => 0_usize,
Value::Dimension(n, Unit::None, _) if n < -Number::from(str_len) => 0_usize, Value::Dimension(Some(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, _) => (n.to_integer() + BigInt::from(str_len + 1))
.to_usize() .to_usize()
.unwrap_or(str_len + 1), .unwrap_or(str_len + 1),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -178,7 +180,7 @@ fn str_slice(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
} }
} }
fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let s1 = match args.get_err(0, "string")? { let s1 = match args.get_err(0, "string")? {
Value::String(i, _) => i, Value::String(i, _) => i,
@ -203,12 +205,12 @@ fn str_index(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
}; };
Ok(match s1.find(&substr) { 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, None => Value::Null,
}) })
} }
fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(3)?; args.max_args(3)?;
let (s1, quotes) = match args.get_err(0, "string")? { let (s1, quotes) = match args.get_err(0, "string")? {
Value::String(i, q) => (i, q), Value::String(i, q) => (i, q),
@ -233,10 +235,11 @@ fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
}; };
let index = match args.get_err(2, "index")? { 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()) 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(..) => { v @ Value::Dimension(..) => {
return Err(( return Err((
format!( format!(
@ -305,7 +308,7 @@ fn str_insert(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
#[cfg(feature = "random")] #[cfg(feature = "random")]
#[allow(clippy::needless_pass_by_value)] #[allow(clippy::needless_pass_by_value)]
fn unique_id(args: CallArgs, _: &mut Parser<'_>) -> SassResult<Value> { pub(crate) fn unique_id(args: CallArgs, _: &mut Parser<'_>) -> SassResult<Value> {
args.max_args(0)?; args.max_args(0)?;
let mut rng = thread_rng(); let mut rng = thread_rng();
let string = std::iter::repeat(()) let string = std::iter::repeat(())

View File

@ -1,60 +1,6 @@
// A reference to the parser is only necessary for some functions mod functions;
#![allow(unused_variables)] pub(crate) mod modules;
use std::{ pub(crate) use functions::{
collections::HashMap, color, list, map, math, meta, selector, string, Builtin, GLOBAL_FUNCTIONS,
sync::atomic::{AtomicUsize, Ordering},
}; };
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<Value>,
usize,
);
impl Builtin {
pub fn new(body: fn(CallArgs, &mut Parser<'_>) -> SassResult<Value>) -> 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<GlobalFunctionMap> = 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
});

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

622
src/builtin/modules/math.rs Normal file
View File

@ -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<Value> {
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<Value> {
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<Option<Number>> {
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::<SassResult<Option<Vec<Number>>>>()?;
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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<Value> {
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),
);
}

127
src/builtin/modules/meta.rs Normal file
View File

@ -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<Vec<Stmt>> {
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<Value> {
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<Value> {
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);
}

282
src/builtin/modules/mod.rs Normal file
View File

@ -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<Identifier, Module>);
#[derive(Debug, Default)]
pub(crate) struct ModuleConfig(BTreeMap<Identifier, Value>);
impl ModuleConfig {
/// Removes and returns element with name
pub fn get(&mut self, name: Identifier) -> Option<Value> {
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<Identifier>, value: Spanned<Value>) -> 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<Identifier>) -> 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<Identifier>, 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<Identifier>) -> SassResult<Mixin> {
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<Identifier>) -> SassResult<Option<SassFunction>> {
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<Value>,
) {
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::<Vec<(Value, Value)>>(),
)
}
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::<Vec<(Value, Value)>>(),
)
}
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
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -264,7 +264,7 @@ impl Color {
return h.saturation() * Number::from(100); 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 green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255);
@ -291,7 +291,7 @@ impl Color {
return h.luminance() * Number::from(100); 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 green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255); let blue = self.blue() / Number::from(255);
let min = min(&red, min(&green, &blue)).clone(); let min = min(&red, min(&green, &blue)).clone();

View File

@ -1,11 +1,11 @@
/*! # grass /*! # grass
An implementation of the Sass specification in pure rust. 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 | | Passing | Failing | Total |
|---------|---------|-------| |---------|---------|-------|
| 2935 | 2158 | 5093 | | 3375 | 1718 | 5093 |
## Use as library ## Use as library
``` ```
@ -50,6 +50,7 @@ grass input.scss
clippy::unknown_clippy_lints, clippy::unknown_clippy_lints,
clippy::replace_consts, clippy::replace_consts,
clippy::single_match, clippy::single_match,
clippy::float_arithmetic,
// temporarily allowed while under heavy development. // temporarily allowed while under heavy development.
// eventually these allows should be refactored away // 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 use crate::error::{SassError as Error, SassResult as Result};
pub(crate) use crate::token::Token; pub(crate) use crate::token::Token;
use crate::{ use crate::{
builtin::modules::{ModuleConfig, Modules},
lexer::Lexer, lexer::Lexer,
output::Css, output::Css,
parse::{ parse::{
@ -292,6 +294,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options, options,
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; .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<String> {
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options, options,
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; .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<String, JsValue> {
extender: &mut Extender::new(empty_span), extender: &mut Extender::new(empty_span),
content_scopes: &mut Scopes::new(), content_scopes: &mut Scopes::new(),
options: &Options::default(), options: &Options::default(),
modules: &mut Modules::default(),
module_config: &mut ModuleConfig::default(),
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?; .map_err(|e| raw_to_parse_error(&map, *e, true).to_string())?;

View File

@ -1,6 +1,6 @@
use std::{collections::HashMap, mem}; use std::{collections::HashMap, mem};
use codemap::{Span, Spanned}; use codemap::Span;
use crate::{ use crate::{
args::{CallArg, CallArgs, FuncArg, FuncArgs}, args::{CallArg, CallArgs, FuncArg, FuncArgs},
@ -72,19 +72,12 @@ impl<'a> Parser<'a> {
} }
} }
'.' => { '.' => {
let next = self.toks.next().ok_or(("expected \".\".", span))?; self.expect_char('.')?;
if next.kind != '.' { self.expect_char('.')?;
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.whitespace_or_comment(); self.whitespace_or_comment();
let next = self.toks.next().ok_or(("expected \")\".", next.pos()))?;
if next.kind != ')' { self.expect_char(')')?;
return Err(("expected \")\".", next.pos()).into());
}
is_variadic = true; is_variadic = true;
@ -119,6 +112,7 @@ impl<'a> Parser<'a> {
} }
self.whitespace_or_comment(); self.whitespace_or_comment();
// TODO: this should NOT eat the opening curly brace // TODO: this should NOT eat the opening curly brace
// todo: self.expect_char('{')?;
match self.toks.next() { match self.toks.next() {
Some(v) if v.kind == '{' => {} Some(v) if v.kind == '{' => {}
Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()), Some(..) | None => return Err(("expected \"{\".", close_paren_span).into()),
@ -225,11 +219,7 @@ impl<'a> Parser<'a> {
return Err(("expected \")\".", pos).into()); return Err(("expected \")\".", pos).into());
} }
self.toks.next(); self.toks.next();
if let Some(Token { kind: '.', .. }) = self.toks.peek() { self.expect_char('.')?;
self.toks.next();
} else {
return Err(("expected \".\".", pos).into());
}
} else { } else {
return Err(("expected \")\".", pos).into()); return Err(("expected \")\".", pos).into());
} }
@ -323,23 +313,16 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment(); self.whitespace_or_comment();
continue; continue;
} }
Some(Token { kind: '.', pos }) => { Some(Token { kind: '.', .. }) => {
let pos = *pos;
self.toks.next(); self.toks.next();
if let Some(Token { kind: '.', pos }) = self.toks.peek().cloned() { self.expect_char('.')?;
if !name.is_empty() {
return Err(("expected \")\".", pos).into()); if !name.is_empty() {
} return Err(("expected \")\".", self.span_before).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('.')?;
} }
Some(Token { pos, .. }) => { Some(Token { pos, .. }) => {
return Err(("expected \")\".", *pos).into()); return Err(("expected \")\".", *pos).into());
@ -367,15 +350,8 @@ impl<'a> Parser<'a> {
self.scopes.enter_new_scope(); self.scopes.enter_new_scope();
for (idx, mut arg) in fn_args.0.into_iter().enumerate() { for (idx, mut arg) in fn_args.0.into_iter().enumerate() {
if arg.is_variadic { if arg.is_variadic {
let span = args.span();
let arg_list = Value::ArgList(args.get_variadic()?); let arg_list = Value::ArgList(args.get_variadic()?);
scope.insert_var( scope.insert_var(arg.name, arg_list);
arg.name,
Spanned {
node: arg_list,
span,
},
);
break; break;
} }
let val = match args.get(idx, arg.name) { 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()); self.scopes.insert_var(arg.name, val.clone());
scope.insert_var(arg.name, val); scope.insert_var(arg.name, val);
} }

View File

@ -2,7 +2,7 @@ use std::ops::{BitAnd, BitOr};
use codemap::Spanned; use codemap::Spanned;
use crate::{interner::InternedString, value::Value}; use crate::{common::Identifier, interner::InternedString, value::Value};
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct NeverEmptyVec<T> { pub(crate) struct NeverEmptyVec<T> {
@ -42,6 +42,7 @@ impl<T> NeverEmptyVec<T> {
pub(super) enum SelectorOrStyle { pub(super) enum SelectorOrStyle {
Selector(String), Selector(String),
Style(InternedString, Option<Box<Spanned<Value>>>), Style(InternedString, Option<Box<Spanned<Value>>>),
ModuleVariableRedeclaration(Identifier),
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]

View File

@ -24,14 +24,10 @@ impl<'a> Parser<'a> {
let init_cond = self.parse_value(true, &|_| false)?.node; let init_cond = self.parse_value(true, &|_| false)?.node;
// consume the open curly brace self.expect_char('{')?;
let span_before = match self.toks.next() {
Some(Token { kind: '{', pos }) => pos,
Some(..) | None => return Err(("expected \"{\".", self.span_before).into()),
};
if self.toks.peek().is_none() { if self.toks.peek().is_none() {
return Err(("expected \"}\".", span_before).into()); return Err(("expected \"}\".", self.span_before).into());
} }
self.whitespace_or_comment(); self.whitespace_or_comment();
@ -53,6 +49,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
} else { } else {
@ -87,12 +85,7 @@ impl<'a> Parser<'a> {
false false
} else { } else {
let v = self.parse_value(true, &|_| false)?.node.is_true(); let v = self.parse_value(true, &|_| false)?.node.is_true();
match self.toks.next() { self.expect_char('{')?;
Some(Token { kind: '{', .. }) => {}
Some(..) | None => {
return Err(("expected \"{\".", self.span_before).into())
}
}
v v
}; };
if cond { if cond {
@ -112,6 +105,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;
} else { } else {
@ -140,6 +135,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt(); .parse_stmt();
} }
@ -158,17 +155,15 @@ impl<'a> Parser<'a> {
} }
pub(super) fn parse_for(&mut self) -> SassResult<Vec<Stmt>> { pub(super) fn parse_for(&mut self) -> SassResult<Vec<Stmt>> {
// todo: whitespace or comment
self.whitespace(); self.whitespace();
let next = self // todo: test for error here
.toks self.expect_char('$')?;
.next()
.ok_or(("expected \"$\".", self.span_before))?; let var = self
let var: Spanned<Identifier> = match next.kind { .parse_identifier_no_interpolation(false)?
'$' => self .map_node(|n| n.into());
.parse_identifier_no_interpolation(false)?
.map_node(|i| i.into()),
_ => return Err(("expected \"$\".", self.span_before).into()),
};
self.whitespace(); self.whitespace();
self.span_before = match self.toks.peek() { self.span_before = match self.toks.peek() {
Some(tok) => tok.pos, Some(tok) => tok.pos,
@ -242,10 +237,11 @@ impl<'a> Parser<'a> {
self.whitespace(); self.whitespace();
let from_val = self.parse_value_from_vec(from_toks, true)?; let from_val = self.parse_value_from_vec(from_toks, true)?;
let from = match from_val.node { 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, Some(v) => v,
None => return Err((format!("{} is not a int.", n), from_val.span).into()), None => return Err((format!("{} is not a int.", n), from_val.span).into()),
}, },
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!("{} is not an integer.", v.inspect(from_val.span)?), 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_val = self.parse_value(true, &|_| false)?;
let to = match to_val.node { 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, Some(v) => v,
None => return Err((format!("{} is not a int.", n), to_val.span).into()), None => return Err((format!("{} is not a int.", n), to_val.span).into()),
}, },
Value::Dimension(None, ..) => todo!(),
v => { v => {
return Err(( return Err((
format!("{} is not an integer.", v.to_css_string(to_val.span)?), 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 self.expect_char('{')?;
match self.toks.next() {
Some(Token { kind: '{', pos }) => pos,
Some(..) | None => return Err(("expected \"{\".", to_val.span).into()),
};
let body = read_until_closing_curly_brace(self.toks)?; let body = read_until_closing_curly_brace(self.toks)?;
self.toks.next(); self.toks.next();
@ -299,10 +292,7 @@ impl<'a> Parser<'a> {
for i in iter { for i in iter {
self.scopes.insert_var_last( self.scopes.insert_var_last(
var.node, var.node,
Spanned { Value::Dimension(Some(Number::from(i)), Unit::None, true),
node: Value::Dimension(Number::from(i), Unit::None, true),
span: var.span,
},
); );
if self.flags.in_function() { if self.flags.in_function() {
let these_stmts = Parser { let these_stmts = Parser {
@ -320,8 +310,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
@ -342,8 +334,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, 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, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
@ -414,8 +410,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?, .parse_stmt()?,
); );
} }
val = self.parse_value_from_vec(cond.clone(), true)?; val = self.parse_value_from_vec(cond.clone(), true)?;
@ -430,15 +428,11 @@ impl<'a> Parser<'a> {
let mut vars: Vec<Spanned<Identifier>> = Vec::new(); let mut vars: Vec<Spanned<Identifier>> = Vec::new();
loop { loop {
let next = self self.expect_char('$')?;
.toks
.next()
.ok_or(("expected \"$\".", self.span_before))?;
match next.kind { vars.push(self.parse_identifier()?.map_node(|i| i.into()));
'$' => vars.push(self.parse_identifier()?.map_node(|i| i.into())),
_ => return Err(("expected \"$\".", next.pos()).into()), // todo: whitespace or comment
}
self.whitespace(); self.whitespace();
if self if self
.toks .toks
@ -478,26 +472,14 @@ impl<'a> Parser<'a> {
for row in iter { for row in iter {
if vars.len() == 1 { if vars.len() == 1 {
self.scopes.insert_var_last( self.scopes.insert_var_last(vars[0].node, row);
vars[0].node,
Spanned {
node: row,
span: vars[0].span,
},
);
} else { } else {
for (var, val) in vars.iter().zip( for (var, val) in vars.iter().zip(
row.as_list() row.as_list()
.into_iter() .into_iter()
.chain(std::iter::once(Value::Null).cycle()), .chain(std::iter::once(Value::Null).cycle()),
) { ) {
self.scopes.insert_var_last( self.scopes.insert_var_last(var.node, val);
var.node,
Spanned {
node: val,
span: var.span,
},
);
} }
} }
@ -517,8 +499,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?; .parse_stmt()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
return Ok(these_stmts); return Ok(these_stmts);
} }
@ -539,8 +523,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?, .parse_stmt()?,
); );
} }
} }

View File

@ -4,11 +4,11 @@ use peekmore::PeekMore;
use crate::{ use crate::{
args::CallArgs, args::CallArgs,
atrule::Function, atrule::Function,
common::unvendor, common::{unvendor, Identifier},
error::SassResult, error::SassResult,
scope::Scopes, scope::Scopes,
utils::{read_until_closing_curly_brace, read_until_semicolon_or_closing_curly_brace}, utils::{read_until_closing_curly_brace, read_until_semicolon_or_closing_curly_brace},
value::Value, value::{SassFunction, Value},
Token, Token,
}; };
@ -40,11 +40,9 @@ impl<'a> Parser<'a> {
} }
self.whitespace_or_comment(); self.whitespace_or_comment();
let args = match self.toks.next() { self.expect_char('(')?;
Some(Token { kind: '(', .. }) => self.parse_func_args()?,
Some(Token { pos, .. }) => return Err(("expected \"(\".", pos).into()), let args = self.parse_func_args()?;
None => return Err(("expected \"(\".", span).into()),
};
self.whitespace(); self.whitespace();
@ -57,10 +55,18 @@ impl<'a> Parser<'a> {
let function = Function::new(args, body, self.at_root, span); let function = Function::new(args, body, self.at_root, span);
let name_as_ident = Identifier::from(name);
if self.at_root { 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 { } else {
self.scopes.insert_fn(name.into(), function); self.scopes.insert_fn(
name_as_ident,
SassFunction::UserDefined(Box::new(function), name_as_ident),
);
} }
Ok(()) Ok(())
} }
@ -112,8 +118,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?; .parse_stmt()?;
if entered_scope { if entered_scope {
self.scopes.exit_scope(); self.scopes.exit_scope();

View File

@ -13,60 +13,13 @@ use crate::{
use super::{Parser, Stmt}; 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.
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
fn find_import(file_path: &PathBuf, name: &OsStr, load_paths: &[&Path]) -> Option<PathBuf> {
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<PathBuf> = 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> { impl<'a> Parser<'a> {
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> { /// Searches the current directory of the file then searches in `load_paths` directories
let path: &Path = file_name.as_ref(); /// if the import has not yet been found.
///
/// <https://sass-lang.com/documentation/at-rules/import#finding-the-file>
/// <https://sass-lang.com/documentation/at-rules/import#load-paths>
pub(super) fn find_import(&self, path: &Path) -> Option<PathBuf> {
let path_buf = if path.is_absolute() { let path_buf = if path.is_absolute() {
// todo: test for absolute path imports // todo: test for absolute path imports
path.into() path.into()
@ -79,7 +32,59 @@ impl<'a> Parser<'a> {
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new("..")); 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<PathBuf> = 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<Vec<Stmt>> {
let path: &Path = file_name.as_ref();
if let Some(name) = self.find_import(path) {
let file = self.map.add_file( let file = self.map.add_file(
name.to_string_lossy().into(), name.to_string_lossy().into(),
String::from_utf8(fs::read(&name)?)?, String::from_utf8(fs::read(&name)?)?,
@ -102,10 +107,11 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse(); .parse();
} }
self.whitespace();
Err(("Can't find stylesheet to import.", span).into()) Err(("Can't find stylesheet to import.", span).into())
} }

View File

@ -63,9 +63,8 @@ impl<'a, 'b> KeyframesSelectorParser<'a, 'b> {
num.push_str(&eat_whole_number(self.parser.toks)); num.push_str(&eat_whole_number(self.parser.toks));
} }
if !matches!(self.parser.toks.next(), Some(Token { kind: '%', .. })) { self.parser.expect_char('%')?;
return Err(("expected \"%\".", tok.pos).into());
}
selectors.push(KeyframesSelector::Percent(num.into_boxed_str())); selectors.push(KeyframesSelector::Percent(num.into_boxed_str()));
} }
'{' => break, '{' => break,
@ -173,6 +172,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
}) })
.parse_keyframes_selector()?; .parse_keyframes_selector()?;
@ -208,6 +209,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_stmt()?; .parse_stmt()?;

View File

@ -25,16 +25,6 @@ impl<'a> Parser<'a> {
Ok(false) 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 { pub fn scan_char(&mut self, c: char) -> bool {
if let Some(Token { kind, .. }) = self.toks.peek() { if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c { if *kind == c {

View File

@ -6,7 +6,7 @@ use peekmore::PeekMore;
use crate::{ use crate::{
args::{CallArgs, FuncArgs}, args::{CallArgs, FuncArgs},
atrule::{Content, Mixin}, atrule::mixin::{Content, Mixin, UserDefinedMixin},
error::SassResult, error::SassResult,
scope::Scopes, scope::Scopes,
utils::read_until_closing_curly_brace, 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 // 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. // 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 { if self.at_root {
self.global_scope.insert_mixin(name, mixin); self.global_scope.insert_mixin(name, mixin);
@ -73,6 +73,19 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment(); self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into); 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(); self.whitespace_or_comment();
let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() { let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() {
@ -91,9 +104,7 @@ impl<'a> Parser<'a> {
ident.node.make_ascii_lowercase(); ident.node.make_ascii_lowercase();
if ident.node == "using" { if ident.node == "using" {
self.whitespace_or_comment(); self.whitespace_or_comment();
if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) { self.expect_char('(')?;
return Err(("expected \"(\".", ident.span).into());
}
Some(self.parse_func_args()?) Some(self.parse_func_args()?)
} else { } else {
@ -125,12 +136,17 @@ impl<'a> Parser<'a> {
self.toks.next(); self.toks.next();
} }
let Mixin { let UserDefinedMixin {
body, body,
args: fn_args, args: fn_args,
declared_at_root, 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)?; let scope = self.eval_args(fn_args, args)?;
@ -164,8 +180,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()?; .parse_stmt()?;
self.content.pop(); self.content.pop();
self.scopes.exit_scope(); self.scopes.exit_scope();
@ -225,8 +243,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.scopes, content_scopes: self.scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()? .parse_stmt()?
} else { } else {
Vec::new() Vec::new()
}; };

View File

@ -7,8 +7,10 @@ use crate::{
atrule::{ atrule::{
keyframes::{Keyframes, KeyframesRuleSet}, keyframes::{Keyframes, KeyframesRuleSet},
media::MediaRule, media::MediaRule,
AtRuleKind, Content, SupportsRule, UnknownAtRule, mixin::Content,
AtRuleKind, SupportsRule, UnknownAtRule,
}, },
builtin::modules::{ModuleConfig, Modules},
error::SassResult, error::SassResult,
scope::{Scope, Scopes}, scope::{Scope, Scopes},
selector::{ selector::{
@ -25,6 +27,7 @@ use crate::{
use common::{Comment, ContextFlags, NeverEmptyVec, SelectorOrStyle}; use common::{Comment, ContextFlags, NeverEmptyVec, SelectorOrStyle};
pub(crate) use value::{HigherIntermediateValue, ValueVisitor}; pub(crate) use value::{HigherIntermediateValue, ValueVisitor};
use variable::VariableValue;
mod args; mod args;
pub mod common; pub mod common;
@ -35,6 +38,7 @@ mod import;
mod keyframes; mod keyframes;
mod media; mod media;
mod mixin; mod mixin;
mod module;
mod style; mod style;
mod throw_away; mod throw_away;
mod value; mod value;
@ -86,11 +90,18 @@ pub(crate) struct Parser<'a> {
pub extender: &'a mut Extender, pub extender: &'a mut Extender,
pub options: &'a Options<'a>, pub options: &'a Options<'a>,
pub modules: &'a mut Modules,
pub module_config: &'a mut ModuleConfig,
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
pub fn parse(&mut self) -> SassResult<Vec<Stmt>> { pub fn parse(&mut self) -> SassResult<Vec<Stmt>> {
let mut stmts = Vec::new(); let mut stmts = Vec::new();
self.whitespace();
stmts.append(&mut self.load_modules()?);
while self.toks.peek().is_some() { while self.toks.peek().is_some() {
stmts.append(&mut self.parse_stmt()?); stmts.append(&mut self.parse_stmt()?);
if self.flags.in_function() && !stmts.is_empty() { if self.flags.in_function() && !stmts.is_empty() {
@ -101,6 +112,26 @@ impl<'a> Parser<'a> {
Ok(stmts) 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<Vec<Stmt>> { fn parse_stmt(&mut self) -> SassResult<Vec<Stmt>> {
let mut stmts = Vec::new(); let mut stmts = Vec::new();
while let Some(Token { kind, pos }) = self.toks.peek() { while let Some(Token { kind, pos }) = self.toks.peek() {
@ -212,7 +243,13 @@ impl<'a> Parser<'a> {
AtRuleKind::Unknown(_) => { AtRuleKind::Unknown(_) => {
stmts.push(self.parse_unknown_at_rule(kind_string.node)?) 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::Forward => todo!("@forward not yet implemented"),
AtRuleKind::Extend => self.parse_extend()?, AtRuleKind::Extend => self.parse_extend()?,
AtRuleKind::Supports => stmts.push(self.parse_supports()?), AtRuleKind::Supports => stmts.push(self.parse_supports()?),
@ -258,6 +295,9 @@ impl<'a> Parser<'a> {
} }
if self.flags.in_keyframes() { if self.flags.in_keyframes() {
match self.is_selector_or_style()? { match self.is_selector_or_style()? {
SelectorOrStyle::ModuleVariableRedeclaration(module) => {
self.parse_module_variable_redeclaration(module)?
}
SelectorOrStyle::Style(property, value) => { SelectorOrStyle::Style(property, value) => {
if let Some(value) = value { if let Some(value) = value {
stmts.push(Stmt::Style(Style { property, value })); stmts.push(Stmt::Style(Style { property, value }));
@ -285,6 +325,9 @@ impl<'a> Parser<'a> {
} }
match self.is_selector_or_style()? { match self.is_selector_or_style()? {
SelectorOrStyle::ModuleVariableRedeclaration(module) => {
self.parse_module_variable_redeclaration(module)?
}
SelectorOrStyle::Style(property, value) => { SelectorOrStyle::Style(property, value) => {
if let Some(value) = value { if let Some(value) = value {
stmts.push(Stmt::Style(Style { property, value })); stmts.push(Stmt::Style(Style { property, value }));
@ -412,6 +455,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
}, },
allows_parent, allows_parent,
true, true,
@ -470,10 +515,11 @@ impl<'a> Parser<'a> {
pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> { pub fn parse_interpolation(&mut self) -> SassResult<Spanned<Value>> {
let val = self.parse_value(true, &|_| false)?; let val = self.parse_value(true, &|_| false)?;
match self.toks.next() {
Some(Token { kind: '}', .. }) => {} self.span_before = val.span;
Some(..) | None => return Err(("expected \"}\".", val.span).into()),
} self.expect_char('}')?;
Ok(val.map_node(Value::unquote)) Ok(val.map_node(Value::unquote))
} }
@ -640,9 +686,7 @@ impl<'a> Parser<'a> {
self.whitespace(); self.whitespace();
if !matches!(self.toks.next(), Some(Token { kind: '{', .. })) { self.expect_char('{')?;
return Err(("expected \"{\".", self.span_before).into());
}
let raw_body = self.parse_stmt()?; let raw_body = self.parse_stmt()?;
@ -711,8 +755,10 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse()? .parse_stmt()?
.into_iter() .into_iter()
.filter_map(|s| match s { .filter_map(|s| match s {
Stmt::Style(..) => { Stmt::Style(..) => {
@ -755,14 +801,15 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_selector(false, true, String::new())?; .parse_selector(false, true, String::new())?;
// todo: this might be superfluous
self.whitespace(); self.whitespace();
if let Some(Token { kind: ';', .. }) = self.toks.peek() { self.consume_char_if_exists(';');
self.toks.next();
}
let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before);

291
src/parse/module.rs Normal file
View File

@ -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<Option<String>> {
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<ModuleConfig> {
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<Stmt>)> {
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::<Vec<Token>>()
.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<Vec<Stmt>> {
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(())
}
}

View File

@ -111,43 +111,62 @@ impl<'a> Parser<'a> {
let mut property = self.parse_identifier()?.node; let mut property = self.parse_identifier()?.node;
let whitespace_after_property = self.whitespace(); let whitespace_after_property = self.whitespace();
if let Some(Token { kind: ':', .. }) = self.toks.peek() { match self.toks.peek() {
self.toks.next(); Some(Token { kind: ':', .. }) => {
if let Some(Token { kind, .. }) = self.toks.peek() { self.toks.next();
return Ok(match kind { if let Some(Token { kind, .. }) = self.toks.peek() {
':' => { return Ok(match kind {
if whitespace_after_property { ':' => {
property.push(' '); 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)),
));
} }
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 { if whitespace_after_property {
property.push(' '); property.push(' ');
}
property.push(':');
return Ok(SelectorOrStyle::Selector(property));
} }
property.push(':'); _ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None),
return Ok(SelectorOrStyle::Selector(property)); });
}
}
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 { if whitespace_after_property {
property.push(' '); property.push(' ');
}
return Ok(SelectorOrStyle::Selector(property));
} }
return Ok(SelectorOrStyle::Selector(property));
} }
Err(("expected \"{\".", self.span_before).into()) Err(("expected \"{\".", self.span_before).into())
} }

View File

@ -3,12 +3,13 @@
use std::cmp::Ordering; use std::cmp::Ordering;
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use num_traits::Zero;
use crate::{ use crate::{
args::CallArgs, args::CallArgs,
common::{Op, QuoteKind}, common::{Op, QuoteKind},
error::SassResult, error::SassResult,
unit::{Unit, UNIT_CONVERSION_TABLE}, unit::Unit,
value::{SassFunction, Value}, value::{SassFunction, Value},
}; };
@ -119,7 +120,11 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
fn unary_minus(&self, val: Value) -> SassResult<Value> { fn unary_minus(&self, val: Value) -> SassResult<Value> {
Ok(match val { 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), 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, QuoteKind::None,
), ),
}, },
Value::Dimension(num, unit, _) => match right { v @ Value::Dimension(None, ..) => v,
Value::Dimension(num2, unit2, _) => { Value::Dimension(Some(num), unit, _) => match right {
v @ Value::Dimension(None, ..) => v,
Value::Dimension(Some(num2), unit2, _) => {
if !unit.comparable(&unit2) { if !unit.comparable(&unit2) {
return Err(( return Err((
format!("Incompatible units {} and {}.", unit2, unit), format!("Incompatible units {} and {}.", unit2, unit),
@ -215,20 +222,13 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
.into()); .into());
} }
if unit == unit2 { if unit == unit2 {
Value::Dimension(num + num2, unit, true) Value::Dimension(Some(num + num2), unit, true)
} else if unit == Unit::None { } else if unit == Unit::None {
Value::Dimension(num + num2, unit2, true) Value::Dimension(Some(num + num2), unit2, true)
} else if unit2 == Unit::None { } else if unit2 == Unit::None {
Value::Dimension(num + num2, unit, true) Value::Dimension(Some(num + num2), unit, true)
} else { } else {
Value::Dimension( Value::Dimension(Some(num + num2.convert(&unit2, &unit)), unit, true)
num + num2
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone(),
unit,
true,
)
} }
} }
Value::String(s, q) => Value::String(format!("{}{}{}", num, unit, s), q), 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)?), format!("-{}", right.to_css_string(self.span)?),
QuoteKind::None, QuoteKind::None,
), ),
Value::Dimension(num, unit, _) => match right { Value::Dimension(None, ..) => todo!(),
Value::Dimension(num2, unit2, _) => { Value::Dimension(Some(num), unit, _) => match right {
Value::Dimension(None, ..) => todo!(),
Value::Dimension(Some(num2), unit2, _) => {
if !unit.comparable(&unit2) { if !unit.comparable(&unit2) {
return Err(( return Err((
format!("Incompatible units {} and {}.", unit2, unit), format!("Incompatible units {} and {}.", unit2, unit),
@ -324,20 +326,13 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
.into()); .into());
} }
if unit == unit2 { if unit == unit2 {
Value::Dimension(num - num2, unit, true) Value::Dimension(Some(num - num2), unit, true)
} else if unit == Unit::None { } else if unit == Unit::None {
Value::Dimension(num - num2, unit2, true) Value::Dimension(Some(num - num2), unit2, true)
} else if unit2 == Unit::None { } else if unit2 == Unit::None {
Value::Dimension(num - num2, unit, true) Value::Dimension(Some(num - num2), unit, true)
} else { } else {
Value::Dimension( Value::Dimension(Some(num - num2.convert(&unit2, &unit)), unit, true)
num - num2
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone(),
unit,
true,
)
} }
} }
Value::List(..) Value::List(..)
@ -434,14 +429,16 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
v => panic!("{:?}", v), v => panic!("{:?}", v),
}; };
Ok(match left { Ok(match left {
Value::Dimension(num, unit, _) => match right { Value::Dimension(None, ..) => todo!(),
Value::Dimension(num2, unit2, _) => { Value::Dimension(Some(num), unit, _) => match right {
Value::Dimension(None, ..) => todo!(),
Value::Dimension(Some(num2), unit2, _) => {
if unit == Unit::None { if unit == Unit::None {
Value::Dimension(num * num2, unit2, true) Value::Dimension(Some(num * num2), unit2, true)
} else if unit2 == Unit::None { } else if unit2 == Unit::None {
Value::Dimension(num * num2, unit, true) Value::Dimension(Some(num * num2), unit, true)
} else { } 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)?), format!("/{}", right.to_css_string(self.span)?),
QuoteKind::None, QuoteKind::None,
), ),
Value::Dimension(num, unit, should_divide1) => match right { Value::Dimension(None, ..) => todo!(),
Value::Dimension(num2, unit2, should_divide2) => { 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 should_divide1 || should_divide2 || in_parens {
if num.is_zero() && num2.is_zero() {
return Ok(Value::Dimension(None, Unit::None, true));
}
// `unit(1em / 1em)` => `""` // `unit(1em / 1em)` => `""`
if unit == unit2 { if unit == unit2 {
Value::Dimension(num / num2, Unit::None, true) Value::Dimension(Some(num / num2), Unit::None, true)
// `unit(1 / 1em)` => `"em^-1"` // `unit(1 / 1em)` => `"em^-1"`
} else if unit == Unit::None { } 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"` // `unit(1em / 1)` => `"em"`
} else if unit2 == Unit::None { } else if unit2 == Unit::None {
Value::Dimension(num / num2, unit, true) Value::Dimension(Some(num / num2), unit, true)
// `unit(1in / 1px)` => `""` // `unit(1in / 1px)` => `""`
} else if unit.comparable(&unit2) { } else if unit.comparable(&unit2) {
Value::Dimension( Value::Dimension(
num / (num2 Some(num / num2.convert(&unit2, &unit)),
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone()),
Unit::None, Unit::None,
true, true,
) )
@ -630,28 +630,30 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
v => panic!("{:?}", v), v => panic!("{:?}", v),
}; };
Ok(match left { Ok(match left {
Value::Dimension(n, u, _) => match right { Value::Dimension(None, ..) => todo!(),
Value::Dimension(n2, u2, _) => { Value::Dimension(Some(n), u, _) => match right {
Value::Dimension(None, ..) => todo!(),
Value::Dimension(Some(n2), u2, _) => {
if !u.comparable(&u2) { if !u.comparable(&u2) {
return Err( return Err(
(format!("Incompatible units {} and {}.", u2, u), self.span).into() (format!("Incompatible units {} and {}.", u2, u), self.span).into()
); );
} }
if u == u2 { if u == u2 {
Value::Dimension(n % n2, u, true) Value::Dimension(Some(n % n2), u, true)
} else if u == Unit::None { } else if u == Unit::None {
Value::Dimension(n % n2, u2, true) Value::Dimension(Some(n % n2), u2, true)
} else if u2 == Unit::None { } else if u2 == Unit::None {
Value::Dimension(n % n2, u, true) Value::Dimension(Some(n % n2), u, true)
} else { } else {
Value::Dimension(n, u, true) Value::Dimension(Some(n), u, true)
} }
} }
_ => { _ => {
return Err(( return Err((
format!( format!(
"Undefined operation \"{} % {}\".", "Undefined operation \"{} % {}\".",
Value::Dimension(n, u, true).inspect(self.span)?, Value::Dimension(Some(n), u, true).inspect(self.span)?,
right.inspect(self.span)? right.inspect(self.span)?
), ),
self.span, self.span,
@ -735,53 +737,9 @@ impl<'a, 'b: 'a> ValueVisitor<'a, 'b> {
HigherIntermediateValue::Literal(v) => v, HigherIntermediateValue::Literal(v) => v,
v => panic!("{:?}", v), v => panic!("{:?}", v),
}; };
let ordering = match left {
Value::Dimension(num, unit, _) => match &right { let ordering = left.cmp(&right, self.span, op)?;
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())
}
};
Ok(match op { Ok(match op {
Op::GreaterThan => match ordering { Op::GreaterThan => match ordering {
Ordering::Greater => Value::True, Ordering::Greater => Value::True,

View File

@ -198,6 +198,8 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_value(in_paren, predicate) .parse_value(in_paren, predicate)
} }
@ -222,11 +224,56 @@ impl<'a> Parser<'a> {
extender: self.extender, extender: self.extender,
content_scopes: self.content_scopes, content_scopes: self.content_scopes,
options: self.options, options: self.options,
modules: self.modules,
module_config: self.module_config,
} }
.parse_value(in_paren, &|_| false) .parse_value(in_paren, &|_| false)
} }
fn parse_ident_value(&mut self) -> SassResult<Spanned<IntermediateValue>> { #[allow(clippy::eval_order_dependence)]
fn parse_module_item(
&mut self,
module: &str,
mut module_span: Span,
) -> SassResult<Spanned<IntermediateValue>> {
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<IntoIter<Token>>) -> bool,
) -> SassResult<Spanned<IntermediateValue>> {
let Spanned { node: mut s, span } = self.parse_identifier()?; let Spanned { node: mut s, span } = self.parse_identifier()?;
self.span_before = span; self.span_before = span;
@ -247,68 +294,72 @@ impl<'a> Parser<'a> {
}); });
} }
if let Some(Token { kind: '(', .. }) = self.toks.peek() { match self.toks.peek() {
self.toks.next(); Some(Token { kind: '(', .. }) => {
self.toks.next();
if lower == "min" || lower == "max" { if lower == "min" || lower == "max" {
match self.try_parse_min_max(&lower, true)? { match self.try_parse_min_max(&lower, true)? {
Some(val) => { Some(val) => {
self.toks.truncate_iterator_to_cursor(); self.toks.truncate_iterator_to_cursor();
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal( return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
Value::String(val, QuoteKind::None), Value::String(val, QuoteKind::None),
)) ))
.span(span)); .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 => { 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 // check for named colors
@ -442,7 +493,7 @@ impl<'a> Parser<'a> {
|| (!kind.is_ascii() && !kind.is_control()) || (!kind.is_ascii() && !kind.is_control())
|| (kind == '-' && self.next_is_hypen()) => || (kind == '-' && self.next_is_hypen()) =>
{ {
return Some(self.parse_ident_value()); return Some(self.parse_ident_value(predicate));
} }
'0'..='9' | '.' => { '0'..='9' | '.' => {
let Spanned { let Spanned {
@ -478,7 +529,7 @@ impl<'a> Parser<'a> {
let n = Rational64::new_raw(parse_i64(&val.num), 1); let n = Rational64::new_raw(parse_i64(&val.num), 1);
return Some(Ok(IntermediateValue::Value( return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
Number::new_small(n), Some(Number::new_small(n)),
unit, unit,
false, false,
)), )),
@ -491,7 +542,7 @@ impl<'a> Parser<'a> {
let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len)); let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len));
return Some(Ok(IntermediateValue::Value( return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
Number::new_small(n), Some(Number::new_small(n)),
unit, unit,
false, false,
)), )),
@ -504,7 +555,7 @@ impl<'a> Parser<'a> {
if val.times_ten.is_empty() { if val.times_ten.is_empty() {
return Some(Ok(IntermediateValue::Value( return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension( HigherIntermediateValue::Literal(Value::Dimension(
Number::new_big(n), Some(Number::new_big(n)),
unit, unit,
false, false,
)), )),
@ -533,7 +584,7 @@ impl<'a> Parser<'a> {
}; };
IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension( IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension(
Number::new_big(n * times_ten), Some(Number::new_big(n * times_ten)),
unit, unit,
false, false,
))) )))
@ -547,6 +598,7 @@ impl<'a> Parser<'a> {
}; };
// todo: the above shouldn't eat the closing paren // todo: the above shouldn't eat the closing paren
if let Some(last_tok) = inner.pop() { if let Some(last_tok) = inner.pop() {
// todo: we should remove this like we did for square braces
if last_tok.kind != ')' { if last_tok.kind != ')' {
return Some(Err(("expected \")\".", span).into())); 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) { if let Some(Token { kind: '{', pos }) = self.toks.peek_forward(1) {
self.span_before = *pos; self.span_before = *pos;
self.toks.reset_cursor(); self.toks.reset_cursor();
return Some(self.parse_ident_value()); return Some(self.parse_ident_value(predicate));
} }
self.toks.reset_cursor(); self.toks.reset_cursor();
self.toks.next(); self.toks.next();

View File

@ -8,10 +8,10 @@ use crate::{
use super::Parser; use super::Parser;
#[derive(Debug)] #[derive(Debug)]
struct VariableValue { pub(crate) struct VariableValue {
val_toks: Vec<Token>, pub val_toks: Vec<Token>,
global: bool, pub global: bool,
default: bool, pub default: bool,
} }
impl VariableValue { impl VariableValue {
@ -29,9 +29,9 @@ impl<'a> Parser<'a> {
assert!(matches!(self.toks.next(), Some(Token { kind: '$', .. }))); assert!(matches!(self.toks.next(), Some(Token { kind: '$', .. })));
let ident: Identifier = self.parse_identifier_no_interpolation(false)?.node.into(); let ident: Identifier = self.parse_identifier_no_interpolation(false)?.node.into();
self.whitespace(); self.whitespace();
if !matches!(self.toks.next(), Some(Token { kind: ':', .. })) {
return Err(("expected \":\".", self.span_before).into()); self.expect_char(':')?;
}
let VariableValue { let VariableValue {
val_toks, val_toks,
global, global,
@ -39,13 +39,24 @@ impl<'a> Parser<'a> {
} = self.parse_variable_value()?; } = self.parse_variable_value()?;
if default { if default {
let config_val = self.module_config.get(ident);
if self.at_root && !self.flags.in_control_flow() { if self.at_root && !self.flags.in_control_flow() {
if !self.global_scope.var_exists(ident) { 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); self.global_scope.insert_var(ident, value);
} }
} else { } 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) { if global && !self.global_scope.var_exists(ident) {
self.global_scope.insert_var(ident, value.clone()); self.global_scope.insert_var(ident, value.clone());
} }
@ -55,7 +66,7 @@ impl<'a> Parser<'a> {
return Ok(()); 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 { if global {
self.global_scope.insert_var(ident, value.clone()); self.global_scope.insert_var(ident, value.clone());
@ -77,7 +88,7 @@ impl<'a> Parser<'a> {
Ok(()) Ok(())
} }
fn parse_variable_value(&mut self) -> SassResult<VariableValue> { pub(super) fn parse_variable_value(&mut self) -> SassResult<VariableValue> {
let mut default = false; let mut default = false;
let mut global = false; let mut global = false;

View File

@ -3,18 +3,18 @@ use std::collections::BTreeMap;
use codemap::Spanned; use codemap::Spanned;
use crate::{ use crate::{
atrule::{Function, Mixin}, atrule::mixin::Mixin,
builtin::GLOBAL_FUNCTIONS, builtin::{modules::Module, GLOBAL_FUNCTIONS},
common::Identifier, common::Identifier,
error::SassResult, error::SassResult,
value::Value, value::{SassFunction, Value},
}; };
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub(crate) struct Scope { pub(crate) struct Scope {
vars: BTreeMap<Identifier, Spanned<Value>>, pub vars: BTreeMap<Identifier, Value>,
mixins: BTreeMap<Identifier, Mixin>, pub mixins: BTreeMap<Identifier, Mixin>,
functions: BTreeMap<Identifier, Function>, pub functions: BTreeMap<Identifier, SassFunction>,
} }
impl Scope { impl Scope {
@ -31,12 +31,12 @@ impl Scope {
fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> { fn get_var(&self, name: Spanned<Identifier>) -> SassResult<&Value> {
match self.vars.get(&name.node) { match self.vars.get(&name.node) {
Some(v) => Ok(&v.node), Some(v) => Ok(v),
None => Err(("Undefined variable.", name.span).into()), None => Err(("Undefined variable.", name.span).into()),
} }
} }
pub fn insert_var(&mut self, s: Identifier, v: Spanned<Value>) -> Option<Spanned<Value>> { pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
self.vars.insert(s, v) self.vars.insert(s, v)
} }
@ -55,16 +55,16 @@ impl Scope {
self.mixins.insert(s.into(), v) 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) self.mixins.contains_key(&name)
} }
fn get_fn(&self, name: Identifier) -> Option<Function> { fn get_fn(&self, name: Identifier) -> Option<SassFunction> {
self.functions.get(&name).cloned() self.functions.get(&name).cloned()
} }
pub fn insert_fn<T: Into<Identifier>>(&mut self, s: T, v: Function) -> Option<Function> { pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
self.functions.insert(s.into(), v) self.functions.insert(s, v)
} }
fn fn_exists(&self, name: Identifier) -> bool { fn fn_exists(&self, name: Identifier) -> bool {
@ -73,6 +73,16 @@ impl Scope {
} }
self.functions.contains_key(&name) 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)] #[derive(Debug, Default)]
@ -111,7 +121,7 @@ impl Scopes {
/// Variables /// Variables
impl Scopes { impl Scopes {
pub fn insert_var(&mut self, s: Identifier, v: Spanned<Value>) -> Option<Spanned<Value>> { pub fn insert_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
for scope in self.0.iter_mut().rev() { for scope in self.0.iter_mut().rev() {
if scope.var_exists(s) { if scope.var_exists(s) {
return scope.insert_var(s, v); return scope.insert_var(s, v);
@ -130,7 +140,7 @@ impl Scopes {
/// Always insert this variable into the innermost scope /// Always insert this variable into the innermost scope
/// ///
/// Used, for example, for variables from `@each` and `@for` /// Used, for example, for variables from `@each` and `@for`
pub fn insert_var_last(&mut self, s: Identifier, v: Spanned<Value>) -> Option<Spanned<Value>> { pub fn insert_var_last(&mut self, s: Identifier, v: Value) -> Option<Value> {
if let Some(scope) = self.0.last_mut() { if let Some(scope) = self.0.last_mut() {
scope.insert_var(s, v) scope.insert_var(s, v)
} else { } else {
@ -141,11 +151,7 @@ impl Scopes {
} }
} }
pub fn insert_default_var( pub fn insert_default_var(&mut self, s: Identifier, v: Value) -> Option<Value> {
&mut self,
s: Identifier,
v: Spanned<Value>,
) -> Option<Spanned<Value>> {
if let Some(scope) = self.0.last_mut() { if let Some(scope) = self.0.last_mut() {
if scope.var_exists(s) { if scope.var_exists(s) {
None None
@ -218,7 +224,7 @@ impl Scopes {
/// Functions /// Functions
impl Scopes { impl Scopes {
pub fn insert_fn(&mut self, s: Identifier, v: Function) -> Option<Function> { pub fn insert_fn(&mut self, s: Identifier, v: SassFunction) -> Option<SassFunction> {
if let Some(scope) = self.0.last_mut() { if let Some(scope) = self.0.last_mut() {
scope.insert_fn(s, v) scope.insert_fn(s, v)
} else { } else {
@ -229,17 +235,13 @@ impl Scopes {
} }
} }
pub fn get_fn<'a>( pub fn get_fn<'a>(&'a self, name: Identifier, global_scope: &'a Scope) -> Option<SassFunction> {
&'a self,
name: Spanned<Identifier>,
global_scope: &'a Scope,
) -> Option<Function> {
for scope in self.0.iter().rev() { for scope in self.0.iter().rev() {
if scope.fn_exists(name.node) { if scope.fn_exists(name) {
return scope.get_fn(name.node); 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 { pub fn fn_exists(&self, name: Identifier, global_scope: &Scope) -> bool {

View File

@ -5,7 +5,9 @@ use std::{
use codemap::Span; 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}; use super::{Namespace, QualifiedName};
@ -41,13 +43,8 @@ impl Hash for Attribute {
fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult<QualifiedName> { fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult<QualifiedName> {
let next = parser.toks.peek().ok_or(("Expected identifier.", start))?; let next = parser.toks.peek().ok_or(("Expected identifier.", start))?;
if next.kind == '*' { if next.kind == '*' {
let pos = next.pos;
parser.toks.next(); parser.toks.next();
if parser.toks.peek().ok_or(("expected \"|\".", pos))?.kind != '|' { parser.expect_char('|')?;
return Err(("expected \"|\".", pos).into());
}
parser.span_before = parser.toks.next().unwrap().pos();
let ident = parser.parse_identifier()?.node; let ident = parser.parse_identifier()?.node;
return Ok(QualifiedName { return Ok(QualifiedName {
@ -89,19 +86,18 @@ fn attribute_name(parser: &mut Parser<'_>, start: Span) -> SassResult<QualifiedN
} }
fn attribute_operator(parser: &mut Parser<'_>) -> SassResult<AttributeOp> { fn attribute_operator(parser: &mut Parser<'_>) -> SassResult<AttributeOp> {
let start = parser.span_before; let op = match parser.toks.next() {
let op = match parser.toks.next().ok_or(("Expected \"]\".", start))?.kind { Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals),
'=' => return Ok(AttributeOp::Equals), Some(Token { kind: '~', .. }) => AttributeOp::Include,
'~' => AttributeOp::Include, Some(Token { kind: '|', .. }) => AttributeOp::Dash,
'|' => AttributeOp::Dash, Some(Token { kind: '^', .. }) => AttributeOp::Prefix,
'^' => AttributeOp::Prefix, Some(Token { kind: '$', .. }) => AttributeOp::Suffix,
'$' => AttributeOp::Suffix, Some(Token { kind: '*', .. }) => AttributeOp::Contains,
'*' => AttributeOp::Contains, Some(..) | None => return Err(("Expected \"]\".", parser.span_before).into()),
_ => return Err(("Expected \"]\".", start).into()),
}; };
if parser.toks.next().ok_or(("expected \"=\".", start))?.kind != '=' {
return Err(("expected \"=\".", start).into()); parser.expect_char('=')?;
}
Ok(op) Ok(op)
} }
impl Attribute { impl Attribute {
@ -145,25 +141,23 @@ impl Attribute {
}; };
parser.whitespace(); parser.whitespace();
let peek = parser.toks.peek().ok_or(("expected more input.", start))?; let modifier = match parser.toks.peek().cloned() {
Some(Token {
let modifier = match peek.kind { kind: c @ 'a'..='z',
c if c.is_alphabetic() => Some(c), ..
})
| Some(Token {
kind: c @ 'A'..='Z',
..
}) => {
parser.toks.next();
parser.whitespace();
Some(c)
}
_ => None, _ => None,
}; };
let pos = peek.pos(); parser.expect_char(']')?;
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();
Ok(Attribute { Ok(Attribute {
op, op,

View File

@ -317,14 +317,14 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) {
selector = Some(Box::new(self.parse_selector_list()?)); selector = Some(Box::new(self.parse_selector_list()?));
self.parser.whitespace(); self.parser.whitespace();
self.expect_closing_paren()?; self.parser.expect_char(')')?;
} else { } else {
argument = Some(self.declaration_value()?.into_boxed_str()); argument = Some(self.declaration_value()?.into_boxed_str());
} }
} else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) {
selector = Some(Box::new(self.parse_selector_list()?)); selector = Some(Box::new(self.parse_selector_list()?));
self.parser.whitespace(); self.parser.whitespace();
self.expect_closing_paren()?; self.parser.expect_char(')')?;
} else if unvendored == "nth-child" || unvendored == "nth-last-child" { } else if unvendored == "nth-child" || unvendored == "nth-last-child" {
let mut this_arg = self.parse_a_n_plus_b()?; let mut this_arg = self.parse_a_n_plus_b()?;
let found_whitespace = self.parser.whitespace(); 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()); argument = Some(this_arg.into_boxed_str());
} else { } else {
argument = Some( argument = Some(
@ -541,14 +541,6 @@ impl<'a, 'b> SelectorParser<'a, 'b> {
Err((format!("Expected \"{}\".", s), self.span).into()) 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 /// Returns whether `c` can start a simple selector other than a type

View File

@ -7,152 +7,151 @@ use std::{collections::HashMap, f64::consts::PI};
use num_traits::One; use num_traits::One;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use crate::value::Number; use crate::{unit::Unit, value::Number};
pub(crate) static UNIT_CONVERSION_TABLE: Lazy< pub(crate) static UNIT_CONVERSION_TABLE: Lazy<HashMap<Unit, HashMap<Unit, Number>>> =
HashMap<&'static str, HashMap<&'static str, Number>>, Lazy::new(|| {
> = Lazy::new(|| { let mut from_in = HashMap::new();
let mut from_in = HashMap::new(); from_in.insert(Unit::In, Number::one());
from_in.insert("in", Number::one()); from_in.insert(Unit::Cm, Number::one() / Number::from(2.54));
from_in.insert("cm", Number::one() / Number::from(2.54)); from_in.insert(Unit::Pc, Number::small_ratio(1, 6));
from_in.insert("pc", Number::small_ratio(1, 6)); from_in.insert(Unit::Mm, Number::one() / Number::from(25.4));
from_in.insert("mm", Number::one() / Number::from(25.4)); from_in.insert(Unit::Q, Number::one() / Number::from(101.6));
from_in.insert("q", Number::one() / Number::from(101.6)); from_in.insert(Unit::Pt, Number::small_ratio(1, 72));
from_in.insert("pt", Number::small_ratio(1, 72)); from_in.insert(Unit::Px, Number::small_ratio(1, 96));
from_in.insert("px", Number::small_ratio(1, 96));
let mut from_cm = HashMap::new(); let mut from_cm = HashMap::new();
from_cm.insert("in", Number::from(2.54)); from_cm.insert(Unit::In, Number::from(2.54));
from_cm.insert("cm", Number::one()); from_cm.insert(Unit::Cm, Number::one());
from_cm.insert("pc", Number::from(2.54) / Number::from(6)); from_cm.insert(Unit::Pc, Number::from(2.54) / Number::from(6));
from_cm.insert("mm", Number::small_ratio(1, 10)); from_cm.insert(Unit::Mm, Number::small_ratio(1, 10));
from_cm.insert("q", Number::small_ratio(1, 40)); from_cm.insert(Unit::Q, Number::small_ratio(1, 40));
from_cm.insert("pt", Number::from(2.54) / Number::from(72)); from_cm.insert(Unit::Pt, Number::from(2.54) / Number::from(72));
from_cm.insert("px", Number::from(2.54) / Number::from(96)); from_cm.insert(Unit::Px, Number::from(2.54) / Number::from(96));
let mut from_pc = HashMap::new(); let mut from_pc = HashMap::new();
from_pc.insert("in", Number::from(6)); from_pc.insert(Unit::In, Number::from(6));
from_pc.insert("cm", Number::from(6) / Number::from(2.54)); from_pc.insert(Unit::Cm, Number::from(6) / Number::from(2.54));
from_pc.insert("pc", Number::one()); from_pc.insert(Unit::Pc, Number::one());
from_pc.insert("mm", Number::from(6) / Number::from(25.4)); from_pc.insert(Unit::Mm, Number::from(6) / Number::from(25.4));
from_pc.insert("q", Number::from(6) / Number::from(101.6)); from_pc.insert(Unit::Q, Number::from(6) / Number::from(101.6));
from_pc.insert("pt", Number::small_ratio(1, 12)); from_pc.insert(Unit::Pt, Number::small_ratio(1, 12));
from_pc.insert("px", Number::small_ratio(1, 16)); from_pc.insert(Unit::Px, Number::small_ratio(1, 16));
let mut from_mm = HashMap::new(); let mut from_mm = HashMap::new();
from_mm.insert("in", Number::from(25.4)); from_mm.insert(Unit::In, Number::from(25.4));
from_mm.insert("cm", Number::from(10)); from_mm.insert(Unit::Cm, Number::from(10));
from_mm.insert("pc", Number::from(25.4) / Number::from(6)); from_mm.insert(Unit::Pc, Number::from(25.4) / Number::from(6));
from_mm.insert("mm", Number::one()); from_mm.insert(Unit::Mm, Number::one());
from_mm.insert("q", Number::small_ratio(1, 4)); from_mm.insert(Unit::Q, Number::small_ratio(1, 4));
from_mm.insert("pt", Number::from(25.4) / Number::from(72)); from_mm.insert(Unit::Pt, Number::from(25.4) / Number::from(72));
from_mm.insert("px", Number::from(25.4) / Number::from(96)); from_mm.insert(Unit::Px, Number::from(25.4) / Number::from(96));
let mut from_q = HashMap::new(); let mut from_q = HashMap::new();
from_q.insert("in", Number::from(101.6)); from_q.insert(Unit::In, Number::from(101.6));
from_q.insert("cm", Number::from(40)); from_q.insert(Unit::Cm, Number::from(40));
from_q.insert("pc", Number::from(101.6) / Number::from(6)); from_q.insert(Unit::Pc, Number::from(101.6) / Number::from(6));
from_q.insert("mm", Number::from(4)); from_q.insert(Unit::Mm, Number::from(4));
from_q.insert("q", Number::one()); from_q.insert(Unit::Q, Number::one());
from_q.insert("pt", Number::from(101.6) / Number::from(72)); from_q.insert(Unit::Pt, Number::from(101.6) / Number::from(72));
from_q.insert("px", Number::from(101.6) / Number::from(96)); from_q.insert(Unit::Px, Number::from(101.6) / Number::from(96));
let mut from_pt = HashMap::new(); let mut from_pt = HashMap::new();
from_pt.insert("in", Number::from(72)); from_pt.insert(Unit::In, Number::from(72));
from_pt.insert("cm", Number::from(72) / Number::from(2.54)); from_pt.insert(Unit::Cm, Number::from(72) / Number::from(2.54));
from_pt.insert("pc", Number::from(12)); from_pt.insert(Unit::Pc, Number::from(12));
from_pt.insert("mm", Number::from(72) / Number::from(25.4)); from_pt.insert(Unit::Mm, Number::from(72) / Number::from(25.4));
from_pt.insert("q", Number::from(72) / Number::from(101.6)); from_pt.insert(Unit::Q, Number::from(72) / Number::from(101.6));
from_pt.insert("pt", Number::one()); from_pt.insert(Unit::Pt, Number::one());
from_pt.insert("px", Number::small_ratio(3, 4)); from_pt.insert(Unit::Px, Number::small_ratio(3, 4));
let mut from_px = HashMap::new(); let mut from_px = HashMap::new();
from_px.insert("in", Number::from(96)); from_px.insert(Unit::In, Number::from(96));
from_px.insert("cm", Number::from(96) / Number::from(2.54)); from_px.insert(Unit::Cm, Number::from(96) / Number::from(2.54));
from_px.insert("pc", Number::from(16)); from_px.insert(Unit::Pc, Number::from(16));
from_px.insert("mm", Number::from(96) / Number::from(25.4)); from_px.insert(Unit::Mm, Number::from(96) / Number::from(25.4));
from_px.insert("q", Number::from(96) / Number::from(101.6)); from_px.insert(Unit::Q, Number::from(96) / Number::from(101.6));
from_px.insert("pt", Number::small_ratio(4, 3)); from_px.insert(Unit::Pt, Number::small_ratio(4, 3));
from_px.insert("px", Number::one()); from_px.insert(Unit::Px, Number::one());
let mut from_deg = HashMap::new(); let mut from_deg = HashMap::new();
from_deg.insert("deg", Number::one()); from_deg.insert(Unit::Deg, Number::one());
from_deg.insert("grad", Number::small_ratio(9, 10)); from_deg.insert(Unit::Grad, Number::small_ratio(9, 10));
from_deg.insert("rad", Number::from(180) / Number::from(PI)); from_deg.insert(Unit::Rad, Number::from(180) / Number::from(PI));
from_deg.insert("turn", Number::from(360)); from_deg.insert(Unit::Turn, Number::from(360));
let mut from_grad = HashMap::new(); let mut from_grad = HashMap::new();
from_grad.insert("deg", Number::small_ratio(10, 9)); from_grad.insert(Unit::Deg, Number::small_ratio(10, 9));
from_grad.insert("grad", Number::one()); from_grad.insert(Unit::Grad, Number::one());
from_grad.insert("rad", Number::from(200) / Number::from(PI)); from_grad.insert(Unit::Rad, Number::from(200) / Number::from(PI));
from_grad.insert("turn", Number::from(400)); from_grad.insert(Unit::Turn, Number::from(400));
let mut from_rad = HashMap::new(); let mut from_rad = HashMap::new();
from_rad.insert("deg", Number::from(PI) / Number::from(180)); from_rad.insert(Unit::Deg, Number::from(PI) / Number::from(180));
from_rad.insert("grad", Number::from(PI) / Number::from(200)); from_rad.insert(Unit::Grad, Number::from(PI) / Number::from(200));
from_rad.insert("rad", Number::one()); from_rad.insert(Unit::Rad, Number::one());
from_rad.insert("turn", Number::from(2.0 * PI)); from_rad.insert(Unit::Turn, Number::from(2.0 * PI));
let mut from_turn = HashMap::new(); let mut from_turn = HashMap::new();
from_turn.insert("deg", Number::small_ratio(1, 360)); from_turn.insert(Unit::Deg, Number::small_ratio(1, 360));
from_turn.insert("grad", Number::small_ratio(1, 400)); from_turn.insert(Unit::Grad, Number::small_ratio(1, 400));
from_turn.insert("rad", Number::one() / Number::from(2.0 * PI)); from_turn.insert(Unit::Rad, Number::one() / Number::from(2.0 * PI));
from_turn.insert("turn", Number::one()); from_turn.insert(Unit::Turn, Number::one());
let mut from_s = HashMap::new(); let mut from_s = HashMap::new();
from_s.insert("s", Number::one()); from_s.insert(Unit::S, Number::one());
from_s.insert("ms", Number::small_ratio(1, 1000)); from_s.insert(Unit::Ms, Number::small_ratio(1, 1000));
let mut from_ms = HashMap::new(); let mut from_ms = HashMap::new();
from_ms.insert("s", Number::from(1000)); from_ms.insert(Unit::S, Number::from(1000));
from_ms.insert("ms", Number::one()); from_ms.insert(Unit::Ms, Number::one());
let mut from_hz = HashMap::new(); let mut from_hz = HashMap::new();
from_hz.insert("Hz", Number::one()); from_hz.insert(Unit::Hz, Number::one());
from_hz.insert("kHz", Number::from(1000)); from_hz.insert(Unit::Khz, Number::from(1000));
let mut from_khz = HashMap::new(); let mut from_khz = HashMap::new();
from_khz.insert("Hz", Number::small_ratio(1, 1000)); from_khz.insert(Unit::Hz, Number::small_ratio(1, 1000));
from_khz.insert("kHz", Number::one()); from_khz.insert(Unit::Khz, Number::one());
let mut from_dpi = HashMap::new(); let mut from_dpi = HashMap::new();
from_dpi.insert("dpi", Number::one()); from_dpi.insert(Unit::Dpi, Number::one());
from_dpi.insert("dpcm", Number::from(2.54)); from_dpi.insert(Unit::Dpcm, Number::from(2.54));
from_dpi.insert("dppx", Number::from(96)); from_dpi.insert(Unit::Dppx, Number::from(96));
let mut from_dpcm = HashMap::new(); let mut from_dpcm = HashMap::new();
from_dpcm.insert("dpi", Number::one() / Number::from(2.54)); from_dpcm.insert(Unit::Dpi, Number::one() / Number::from(2.54));
from_dpcm.insert("dpcm", Number::one()); from_dpcm.insert(Unit::Dpcm, Number::one());
from_dpcm.insert("dppx", Number::from(96) / Number::from(2.54)); from_dpcm.insert(Unit::Dppx, Number::from(96) / Number::from(2.54));
let mut from_dppx = HashMap::new(); let mut from_dppx = HashMap::new();
from_dppx.insert("dpi", Number::small_ratio(1, 96)); from_dppx.insert(Unit::Dpi, Number::small_ratio(1, 96));
from_dppx.insert("dpcm", Number::from(2.54) / Number::from(96)); from_dppx.insert(Unit::Dpcm, Number::from(2.54) / Number::from(96));
from_dppx.insert("dppx", Number::one()); from_dppx.insert(Unit::Dppx, Number::one());
let mut m = HashMap::new(); let mut m = HashMap::new();
m.insert("in", from_in); m.insert(Unit::In, from_in);
m.insert("cm", from_cm); m.insert(Unit::Cm, from_cm);
m.insert("pc", from_pc); m.insert(Unit::Pc, from_pc);
m.insert("mm", from_mm); m.insert(Unit::Mm, from_mm);
m.insert("q", from_q); m.insert(Unit::Q, from_q);
m.insert("pt", from_pt); m.insert(Unit::Pt, from_pt);
m.insert("px", from_px); m.insert(Unit::Px, from_px);
m.insert("deg", from_deg); m.insert(Unit::Deg, from_deg);
m.insert("grad", from_grad); m.insert(Unit::Grad, from_grad);
m.insert("rad", from_rad); m.insert(Unit::Rad, from_rad);
m.insert("turn", from_turn); m.insert(Unit::Turn, from_turn);
m.insert("s", from_s); m.insert(Unit::S, from_s);
m.insert("ms", from_ms); m.insert(Unit::Ms, from_ms);
m.insert("Hz", from_hz); m.insert(Unit::Hz, from_hz);
m.insert("kHz", from_khz); m.insert(Unit::Khz, from_khz);
m.insert("dpi", from_dpi); m.insert(Unit::Dpi, from_dpi);
m.insert("dpcm", from_dpcm); m.insert(Unit::Dpcm, from_dpcm);
m.insert("dppx", from_dppx); m.insert(Unit::Dppx, from_dppx);
m m
}); });

View File

@ -34,6 +34,10 @@ impl SassMap {
SassMap(Vec::new()) 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 /// We take by value here (consuming the map) in order to
/// save a clone of the value, since the only place this /// save a clone of the value, since the only place this
/// should be called is in a builtin function, which throws /// should be called is in a builtin function, which throws

View File

@ -1,14 +1,16 @@
use std::cmp::Ordering;
use peekmore::PeekMore; use peekmore::PeekMore;
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use crate::{ use crate::{
color::Color, color::Color,
common::{Brackets, ListSeparator, QuoteKind}, common::{Brackets, ListSeparator, Op, QuoteKind},
error::SassResult, error::SassResult,
parse::Parser, parse::Parser,
selector::Selector, selector::Selector,
unit::{Unit, UNIT_CONVERSION_TABLE}, unit::Unit,
utils::hex_char_for, utils::hex_char_for,
{Cow, Token}, {Cow, Token},
}; };
@ -29,7 +31,8 @@ pub(crate) enum Value {
True, True,
False, False,
Null, Null,
Dimension(Number, Unit, bool), /// A `None` value for `Number` indicates a `NaN` value
Dimension(Option<Number>, Unit, bool),
List(Vec<Value>, ListSeparator, Brackets), List(Vec<Value>, ListSeparator, Brackets),
Color(Box<Color>), Color(Box<Color>),
String(String, QuoteKind), String(String, QuoteKind),
@ -46,8 +49,8 @@ impl PartialEq for Value {
Value::String(s2, ..) => s1 == s2, Value::String(s2, ..) => s1 == s2,
_ => false, _ => false,
}, },
Value::Dimension(n, unit, _) => match other { Value::Dimension(Some(n), unit, _) => match other {
Value::Dimension(n2, unit2, _) => { Value::Dimension(Some(n2), unit2, _) => {
if !unit.comparable(unit2) { if !unit.comparable(unit2) {
false false
} else if unit == unit2 { } else if unit == unit2 {
@ -55,14 +58,12 @@ impl PartialEq for Value {
} else if unit == &Unit::None || unit2 == &Unit::None { } else if unit == &Unit::None || unit2 == &Unit::None {
false false
} else { } else {
n == &(n2.clone() n == &n2.clone().convert(unit2, unit)
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone())
} }
} }
_ => false, _ => false,
}, },
Value::Dimension(None, ..) => false,
Value::List(list1, sep1, brackets1) => match other { Value::List(list1, sep1, brackets1) => match other {
Value::List(list2, sep2, brackets2) => { Value::List(list2, sep2, brackets2) => {
if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() {
@ -200,9 +201,21 @@ impl Value {
Value::Important => Cow::const_str("!important"), Value::Important => Cow::const_str("!important"),
Value::Dimension(num, unit, _) => match unit { Value::Dimension(num, unit, _) => match unit {
Unit::Mul(..) | Unit::Div(..) => { 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(..) => { Value::Map(..) | Value::FunctionRef(..) => {
return Err(( return Err((
@ -322,14 +335,68 @@ impl Value {
} }
} }
pub fn cmp(&self, other: &Self, span: Span, op: Op) -> SassResult<Ordering> {
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 { pub fn not_equals(&self, other: &Self) -> bool {
match self { match self {
Value::String(s1, ..) => match other { Value::String(s1, ..) => match other {
Value::String(s2, ..) => s1 != s2, Value::String(s2, ..) => s1 != s2,
_ => true, _ => true,
}, },
Value::Dimension(n, unit, _) => match other { Value::Dimension(Some(n), unit, _) => match other {
Value::Dimension(n2, unit2, _) => { Value::Dimension(Some(n2), unit2, _) => {
if !unit.comparable(unit2) { if !unit.comparable(unit2) {
true true
} else if unit == unit2 { } else if unit == unit2 {
@ -337,10 +404,7 @@ impl Value {
} else if unit == &Unit::None || unit2 == &Unit::None { } else if unit == &Unit::None || unit2 == &Unit::None {
true true
} else { } else {
n != &(n2.clone() n != &n2.clone().convert(unit2, unit)
* UNIT_CONVERSION_TABLE[unit.to_string().as_str()]
[unit2.to_string().as_str()]
.clone())
} }
} }
_ => true, _ => true,
@ -405,7 +469,8 @@ impl Value {
.collect::<SassResult<Vec<String>>>()? .collect::<SassResult<Vec<String>>>()?
.join(", ") .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.is_empty() => Cow::const_str("()"),
Value::ArgList(args) if args.len() == 1 => Cow::owned(format!( Value::ArgList(args) if args.len() == 1 => Cow::owned(format!(
"({},)", "({},)",
@ -477,6 +542,8 @@ impl Value {
extender: parser.extender, extender: parser.extender,
content_scopes: parser.content_scopes, content_scopes: parser.content_scopes,
options: parser.options, options: parser.options,
modules: parser.modules,
module_config: parser.module_config,
} }
.parse_selector(allows_parent, true, String::new())? .parse_selector(allows_parent, true, String::new())?
.0) .0)

View File

@ -8,7 +8,11 @@ use std::{
use num_bigint::BigInt; use num_bigint::BigInt;
use num_rational::{BigRational, Rational64}; 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; use integer::Integer;
@ -16,7 +20,7 @@ mod integer;
const PRECISION: usize = 10; const PRECISION: usize = 10;
#[derive(Clone, Eq, PartialEq, Ord)] #[derive(Clone, Eq, PartialEq)]
pub(crate) enum Number { pub(crate) enum Number {
Small(Rational64), Small(Rational64),
Big(Box<BigRational>), Big(Box<BigRational>),
@ -106,6 +110,84 @@ impl Number {
self self
} }
#[allow(clippy::cast_precision_loss)]
fn as_float(self) -> Option<f64> {
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<Self> {
Some(Number::Big(Box::new(BigRational::from_float(
self.as_float()?.sqrt(),
)?)))
}
pub fn ln(self) -> Option<Self> {
Some(Number::Big(Box::new(BigRational::from_float(
self.as_float()?.ln(),
)?)))
}
pub fn pow(self, exponent: Self) -> Option<Self> {
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<Self> {
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<Self> {
Some(Number::Big(Box::new(BigRational::from_float(
self.as_float()?.$name(),
)?)))
}
pub fn $name_deg(self) -> Option<Self> {
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<Self> {
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 { 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 { impl Add for Number {
type Output = Self; type Output = Self;

View File

@ -169,3 +169,8 @@ test!(
"a {\n color: 1 + 3 / 4;\n}\n", "a {\n color: 1 + 3 / 4;\n}\n",
"a {\n color: 1.75;\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"
);

View File

@ -121,3 +121,8 @@ test!(
"a {\n color: call(call(get-function(get-function), darken), red, 10%);\n}\n", "a {\n color: call(call(get-function(get-function), darken), red, 10%);\n}\n",
"a {\n color: #cc0000;\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"
);

View File

@ -1,43 +1,10 @@
#![cfg(test)] #![cfg(test)]
use std::io::Write; use std::io::Write;
use tempfile::Builder;
#[macro_use] #[macro_use]
mod macros; 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] #[test]
fn imports_variable() { fn imports_variable() {
let input = "@import \"imports_variable\";\na {\n color: $a;\n}"; let input = "@import \"imports_variable\";\na {\n color: $a;\n}";
@ -59,17 +26,8 @@ fn import_no_semicolon() {
fn import_no_quotes() { fn import_no_quotes() {
let input = "@import import_no_quotes"; let input = "@import import_no_quotes";
tempfile!("import_no_quotes", "$a: red;"); tempfile!("import_no_quotes", "$a: red;");
match grass::from_string(input.to_string(), &grass::Options::default()) {
Ok(..) => panic!("did not fail"), assert_err!("Error: Expected string.", input);
Err(e) => assert_eq!(
"Error: Expected string.",
e.to_string()
.chars()
.take_while(|c| *c != '\n')
.collect::<String>()
.as_str()
),
}
} }
#[test] #[test]

View File

@ -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::<String>()
.as_str()
),
}
};
}

592
tests/math-module.rs Normal file
View File

@ -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"
);

65
tests/meta-module.rs Normal file
View File

@ -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);
}

View File

@ -195,6 +195,11 @@ test!(
"a {\n color: type-of(- 2)\n}\n", "a {\n color: type-of(- 2)\n}\n",
"a {\n color: number;\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!( test!(
type_of_arglist, type_of_arglist,
"@mixin foo($a...) {color: type-of($a);}\na {@include foo(1, 2, 3, 4, 5);}", "@mixin foo($a...) {color: type-of($a);}\na {@include foo(1, 2, 3, 4, 5);}",

View File

@ -63,3 +63,8 @@ test!(
"a {\n color: 0 < 1;\n}\n", "a {\n color: 0 < 1;\n}\n",
"a {\n color: true;\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"
);

333
tests/use.rs Normal file
View File

@ -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)
);
}