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

View File

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

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)]
pub(crate) struct Mixin {
pub(crate) struct UserDefinedMixin {
pub args: FuncArgs,
pub body: Vec<Token>,
pub accepts_content_block: bool,
pub declared_at_root: bool,
}
impl Mixin {
impl UserDefinedMixin {
pub fn new(
args: FuncArgs,
body: Vec<Token>,
accepts_content_block: bool,
declared_at_root: bool,
) -> Self {
Mixin {
Self {
args,
body,
accepts_content_block,

View File

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

View File

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

View File

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

View File

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

View File

@ -15,7 +15,8 @@ use crate::{
macro_rules! opt_rgba {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? {
Value::Dimension(n, u, _) => Some(bound!($args, $arg, n, u, $low, $high)),
Value::Dimension(Some(n), u, _) => Some(bound!($args, $arg, n, u, $low, $high)),
Value::Dimension(None, ..) => todo!(),
Value::Null => None,
v => {
return Err((
@ -31,9 +32,10 @@ macro_rules! opt_rgba {
macro_rules! opt_hsl {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? {
Value::Dimension(n, u, _) => {
Value::Dimension(Some(n), u, _) => {
Some(bound!($args, $arg, n, u, $low, $high) / Number::from(100))
}
Value::Dimension(None, ..) => todo!(),
Value::Null => None,
v => {
return Err((
@ -46,7 +48,7 @@ macro_rules! opt_hsl {
};
}
fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
pub(crate) fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
if args.positional_arg(1).is_some() {
return Err((
"Only one positional argument is allowed. All other arguments must be passed by name.",
@ -81,7 +83,8 @@ fn change_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
}
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,
v => {
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")? {
Value::Color(c) => c,
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)? {
Value::Dimension(n, ..) => Some(n),
Value::Dimension(Some(n), ..) => Some(n),
Value::Dimension(None, ..) => todo!(),
Value::Null => None,
v => {
return Err((
@ -175,8 +179,8 @@ fn adjust_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value
#[allow(clippy::cognitive_complexity)]
// todo: refactor into rgb and hsl?
fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
fn scale(val: Number, by: Number, max: Number) -> Number {
pub(crate) fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
pub(crate) fn scale(val: Number, by: Number, max: Number) -> Number {
if by.is_zero() {
return val;
}
@ -198,9 +202,10 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
macro_rules! opt_scale_arg {
($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal) => {
let $name = match $args.default_named_arg($arg, Value::Null)? {
Value::Dimension(n, Unit::Percent, _) => {
Value::Dimension(Some(n), Unit::Percent, _) => {
Some(bound!($args, $arg, n, Unit::Percent, $low, $high) / Number::from(100))
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -288,7 +293,7 @@ fn scale_color(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
let color = match args.get_err(0, "color")? {
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() {
Some(Value::Dimension(n, Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
let green = channels.pop().unwrap();
let red = channels.pop().unwrap();
@ -67,10 +68,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
};
let green = match channels.pop() {
Some(Value::Dimension(n, Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
let string = match channels.pop() {
Some(red) => format!(
@ -95,10 +97,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
};
let red = match channels.pop() {
Some(Value::Dimension(n, Unit::None, _)) => n,
Some(Value::Dimension(n, Unit::Percent, _)) => {
Some(Value::Dimension(Some(n), Unit::None, _)) => n,
Some(Value::Dimension(Some(n), Unit::Percent, _)) => {
(n / Number::from(100)) * Number::from(255)
}
Some(Value::Dimension(None, ..)) => todo!(),
Some(v) if v.is_special_function() => {
return Ok(Value::String(
format!(
@ -148,8 +151,9 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
}
};
let alpha = match args.get_err(1, "alpha")? {
Value::Dimension(n, Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => n / Number::from(100),
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -184,8 +188,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
Ok(Value::Color(Box::new(color.with_alpha(alpha))))
} else {
let red = match args.get_err(0, "red")? {
Value::Dimension(n, Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255),
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -222,8 +229,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
}
};
let green = match args.get_err(1, "green")? {
Value::Dimension(n, Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255),
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -259,8 +269,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
}
};
let blue = match args.get_err(2, "blue")? {
Value::Dimension(n, Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => (n / Number::from(100)) * Number::from(255),
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => {
(n / Number::from(100)) * Number::from(255)
}
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -297,10 +310,11 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
let alpha = match args.default_arg(
3,
"alpha",
Value::Dimension(Number::one(), Unit::None, true),
Value::Dimension(Some(Number::one()), Unit::None, true),
)? {
Value::Dimension(n, Unit::None, _) => n,
Value::Dimension(n, Unit::Percent, _) => n / Number::from(100),
Value::Dimension(Some(n), Unit::None, _) => n,
Value::Dimension(Some(n), Unit::Percent, _) => n / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v @ Value::Dimension(..) => {
return Err((
format!(
@ -336,18 +350,18 @@ fn inner_rgb(name: &'static str, mut args: CallArgs, parser: &mut Parser<'_>) ->
}
}
fn rgb(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
pub(crate) fn rgb(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
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)
}
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)?;
match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None, true)),
Value::Color(c) => Ok(Value::Dimension(Some(c.red()), Unit::None, true)),
v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
@ -356,10 +370,10 @@ fn red(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None, true)),
Value::Color(c) => Ok(Value::Dimension(Some(c.green()), Unit::None, true)),
v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
@ -368,10 +382,10 @@ fn green(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
match args.get_err(0, "color")? {
Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None, true)),
Value::Color(c) => Ok(Value::Dimension(Some(c.blue()), Unit::None, true)),
v => Err((
format!("$color: {} is not a color.", v.inspect(args.span())?),
args.span(),
@ -380,7 +394,7 @@ fn blue(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
let color1 = match args.get_err(0, "color1")? {
Value::Color(c) => c,
@ -407,9 +421,10 @@ fn mix(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
let weight = match args.default_arg(
2,
"weight",
Value::Dimension(Number::from(50), Unit::None, true),
Value::Dimension(Some(Number::from(50)), Unit::None, true),
)? {
Value::Dimension(n, u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
Value::Dimension(Some(n), u, _) => bound!(args, "weight", n, u, 0, 100) / Number::from(100),
Value::Dimension(None, ..) => todo!(),
v => {
return Err((
format!(

View File

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

View File

@ -8,7 +8,7 @@ use crate::{
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)?;
let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? {
@ -26,7 +26,7 @@ fn map_get(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
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)?;
let key = args.get_err(1, "key")?;
let map = match args.get_err(0, "map")? {
@ -44,7 +44,7 @@ fn map_has_key(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value>
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)?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
@ -65,7 +65,7 @@ fn map_keys(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
let map = match args.get_err(0, "map")? {
Value::Map(m) => m,
@ -86,7 +86,7 @@ fn map_values(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
let mut map1 = match args.get_err(0, "map1")? {
Value::Map(m) => m,
@ -116,7 +116,7 @@ fn map_merge(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
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")? {
Value::Map(m) => m,
Value::List(v, ..) if v.is_empty() => SassMap::new(),

View File

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

View File

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

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,
};
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)?;
let parent_selector = args
.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)?;
// todo: Value::to_compound_selector
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)?;
Ok(args
.get_err(0, "selector")?
@ -59,7 +59,7 @@ fn selector_parse(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Val
.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 selectors = args.get_variadic()?;
if selectors.is_empty() {
@ -80,7 +80,7 @@ fn selector_nest(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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 selectors = args.get_variadic()?;
if selectors.is_empty() {
@ -138,7 +138,7 @@ fn selector_append(args: CallArgs, parser: &mut Parser<'_>) -> SassResult<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)?;
let selector = args
.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())
}
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)?;
let selector = args
.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())
}
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)?;
let selector1 = args
.get_err(0, "selector1")?

View File

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

View File

@ -1,60 +1,6 @@
// A reference to the parser is only necessary for some functions
#![allow(unused_variables)]
mod functions;
pub(crate) mod modules;
use std::{
collections::HashMap,
sync::atomic::{AtomicUsize, Ordering},
pub(crate) use functions::{
color, list, map, math, meta, selector, string, Builtin, GLOBAL_FUNCTIONS,
};
use once_cell::sync::Lazy;
use crate::{args::CallArgs, error::SassResult, parse::Parser, value::Value};
#[macro_use]
mod macros;
mod color;
mod list;
mod map;
mod math;
mod meta;
mod selector;
mod string;
pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>;
static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0);
// TODO: impl Fn
#[derive(Clone)]
pub(crate) struct Builtin(
pub fn(CallArgs, &mut Parser<'_>) -> SassResult<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);
}
let red = self.red() / Number::from(255);
let red: Number = self.red() / Number::from(255);
let green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
@ -291,7 +291,7 @@ impl Color {
return h.luminance() * Number::from(100);
}
let red = self.red() / Number::from(255);
let red: Number = self.red() / Number::from(255);
let green = self.green() / Number::from(255);
let blue = self.blue() / Number::from(255);
let min = min(&red, min(&green, &blue)).clone();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,60 +13,13 @@ use crate::{
use super::{Parser, Stmt};
/// Searches the current directory of the file then searches in `load_paths` directories
/// if the import has not yet been found.
/// <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> {
fn parse_single_import(&mut self, file_name: &str, span: Span) -> SassResult<Vec<Stmt>> {
let path: &Path = file_name.as_ref();
/// Searches the current directory of the file then searches in `load_paths` directories
/// if the import has not yet been found.
///
/// <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() {
// todo: test for absolute path imports
path.into()
@ -79,7 +32,59 @@ impl<'a> Parser<'a> {
let name = path_buf.file_name().unwrap_or_else(|| OsStr::new(".."));
if let Some(name) = find_import(&path_buf, name, &self.options.load_paths) {
let paths = [
path_buf.with_file_name(name).with_extension("scss"),
path_buf
.with_file_name(format!("_{}", name.to_str().unwrap()))
.with_extension("scss"),
path_buf.clone(),
path_buf.join("index.scss"),
path_buf.join("_index.scss"),
];
for name in &paths {
if name.is_file() {
return Some(name.to_path_buf());
}
}
for path in &self.options.load_paths {
let paths: Vec<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(
name.to_string_lossy().into(),
String::from_utf8(fs::read(&name)?)?,
@ -102,10 +107,11 @@ impl<'a> Parser<'a> {
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse();
}
self.whitespace();
Err(("Can't find stylesheet to import.", span).into())
}

View File

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

View File

@ -25,16 +25,6 @@ impl<'a> Parser<'a> {
Ok(false)
}
pub fn expect_char(&mut self, c: char) -> SassResult<()> {
if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c {
self.toks.next();
return Ok(());
}
}
Err((format!("expected \"{}\".", c), self.span_before).into())
}
pub fn scan_char(&mut self, c: char) -> bool {
if let Some(Token { kind, .. }) = self.toks.peek() {
if *kind == c {

View File

@ -6,7 +6,7 @@ use peekmore::PeekMore;
use crate::{
args::{CallArgs, FuncArgs},
atrule::{Content, Mixin},
atrule::mixin::{Content, Mixin, UserDefinedMixin},
error::SassResult,
scope::Scopes,
utils::read_until_closing_curly_brace,
@ -55,7 +55,7 @@ impl<'a> Parser<'a> {
// this is blocked on figuring out just how to check for this. presumably we could have a check
// not when parsing initially, but rather when `@include`ing to see if an `@content` was found.
let mixin = Mixin::new(args, body, false, self.at_root);
let mixin = Mixin::new_user_defined(args, body, false, self.at_root);
if self.at_root {
self.global_scope.insert_mixin(name, mixin);
@ -73,6 +73,19 @@ impl<'a> Parser<'a> {
self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into);
let mixin = if let Some(Token { kind: '.', .. }) = self.toks.peek() {
self.toks.next();
let module = name;
let name = self.parse_identifier()?.map_node(Into::into);
self.modules
.get(module.node, module.span)?
.get_mixin(name)?
} else {
self.scopes.get_mixin(name, self.global_scope)?
};
self.whitespace_or_comment();
let args = if let Some(Token { kind: '(', .. }) = self.toks.peek() {
@ -91,9 +104,7 @@ impl<'a> Parser<'a> {
ident.node.make_ascii_lowercase();
if ident.node == "using" {
self.whitespace_or_comment();
if !matches!(self.toks.next(), Some(Token { kind: '(', .. })) {
return Err(("expected \"(\".", ident.span).into());
}
self.expect_char('(')?;
Some(self.parse_func_args()?)
} else {
@ -125,12 +136,17 @@ impl<'a> Parser<'a> {
self.toks.next();
}
let Mixin {
let UserDefinedMixin {
body,
args: fn_args,
declared_at_root,
..
} = self.scopes.get_mixin(name, self.global_scope)?;
} = match mixin {
Mixin::UserDefined(u) => u,
Mixin::Builtin(b) => {
return b(args, self);
}
};
let scope = self.eval_args(fn_args, args)?;
@ -164,8 +180,10 @@ impl<'a> Parser<'a> {
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse()?;
.parse_stmt()?;
self.content.pop();
self.scopes.exit_scope();
@ -225,8 +243,10 @@ impl<'a> Parser<'a> {
extender: self.extender,
content_scopes: self.scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse()?
.parse_stmt()?
} else {
Vec::new()
};

View File

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

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 whitespace_after_property = self.whitespace();
if let Some(Token { kind: ':', .. }) = self.toks.peek() {
self.toks.next();
if let Some(Token { kind, .. }) = self.toks.peek() {
return Ok(match kind {
':' => {
if whitespace_after_property {
property.push(' ');
}
property.push(':');
SelectorOrStyle::Selector(property)
}
c if is_name(*c) => {
if let Some(toks) = self.parse_style_value_when_no_space_after_semicolon() {
let len = toks.len();
if let Ok(val) = self.parse_value_from_vec(toks, false) {
self.toks.take(len).for_each(drop);
return Ok(SelectorOrStyle::Style(
InternedString::get_or_intern(property),
Some(Box::new(val)),
));
match self.toks.peek() {
Some(Token { kind: ':', .. }) => {
self.toks.next();
if let Some(Token { kind, .. }) = self.toks.peek() {
return Ok(match kind {
':' => {
if whitespace_after_property {
property.push(' ');
}
property.push(':');
SelectorOrStyle::Selector(property)
}
c if is_name(*c) => {
if let Some(toks) =
self.parse_style_value_when_no_space_after_semicolon()
{
let len = toks.len();
if let Ok(val) = self.parse_value_from_vec(toks, false) {
self.toks.take(len).for_each(drop);
return Ok(SelectorOrStyle::Style(
InternedString::get_or_intern(property),
Some(Box::new(val)),
));
}
}
if whitespace_after_property {
property.push(' ');
if whitespace_after_property {
property.push(' ');
}
property.push(':');
return Ok(SelectorOrStyle::Selector(property));
}
property.push(':');
return Ok(SelectorOrStyle::Selector(property));
_ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None),
});
}
}
Some(Token { kind: '.', .. }) => {
if matches!(self.toks.peek_next(), Some(Token { kind: '$', .. })) {
self.toks.next();
self.toks.next();
return Ok(SelectorOrStyle::ModuleVariableRedeclaration(
property.into(),
));
} else {
if whitespace_after_property {
property.push(' ');
}
_ => SelectorOrStyle::Style(InternedString::get_or_intern(property), None),
});
return Ok(SelectorOrStyle::Selector(property));
}
}
} else {
if whitespace_after_property {
property.push(' ');
_ => {
if whitespace_after_property {
property.push(' ');
}
return Ok(SelectorOrStyle::Selector(property));
}
return Ok(SelectorOrStyle::Selector(property));
}
Err(("expected \"{\".", self.span_before).into())
}

View File

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

View File

@ -198,6 +198,8 @@ impl<'a> Parser<'a> {
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_value(in_paren, predicate)
}
@ -222,11 +224,56 @@ impl<'a> Parser<'a> {
extender: self.extender,
content_scopes: self.content_scopes,
options: self.options,
modules: self.modules,
module_config: self.module_config,
}
.parse_value(in_paren, &|_| false)
}
fn parse_ident_value(&mut self) -> SassResult<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()?;
self.span_before = span;
@ -247,68 +294,72 @@ impl<'a> Parser<'a> {
});
}
if let Some(Token { kind: '(', .. }) = self.toks.peek() {
self.toks.next();
match self.toks.peek() {
Some(Token { kind: '(', .. }) => {
self.toks.next();
if lower == "min" || lower == "max" {
match self.try_parse_min_max(&lower, true)? {
Some(val) => {
self.toks.truncate_iterator_to_cursor();
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
Value::String(val, QuoteKind::None),
))
.span(span));
if lower == "min" || lower == "max" {
match self.try_parse_min_max(&lower, true)? {
Some(val) => {
self.toks.truncate_iterator_to_cursor();
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
Value::String(val, QuoteKind::None),
))
.span(span));
}
None => {
self.toks.reset_cursor();
}
}
}
let as_ident = Identifier::from(&s);
let func = match self.scopes.get_fn(as_ident, self.global_scope) {
Some(f) => f,
None => {
self.toks.reset_cursor();
if let Some(f) = GLOBAL_FUNCTIONS.get(as_ident.as_str()) {
return Ok(IntermediateValue::Value(
HigherIntermediateValue::Function(
SassFunction::Builtin(f.clone(), as_ident),
self.parse_call_args()?,
),
)
.span(span));
} else {
// check for special cased CSS functions
match unvendor(&lower) {
"calc" | "element" | "expression" => {
s = lower;
self.parse_calc_args(&mut s)?;
}
"url" => match self.try_parse_url()? {
Some(val) => s = val,
None => s.push_str(&self.parse_call_args()?.to_css_string()?),
},
_ => s.push_str(&self.parse_call_args()?.to_css_string()?),
}
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
Value::String(s, QuoteKind::None),
))
.span(span));
}
}
};
let call_args = self.parse_call_args()?;
return Ok(IntermediateValue::Value(HigherIntermediateValue::Function(
func, call_args,
))
.span(span));
}
Some(Token { kind: '.', .. }) => {
if !predicate(self.toks) {
self.toks.next();
return self.parse_module_item(&s, span);
}
}
let as_ident = Identifier::from(&s);
let func = match self.scopes.get_fn(
Spanned {
node: as_ident,
span,
},
self.global_scope,
) {
Some(f) => f,
None => {
if let Some(f) = GLOBAL_FUNCTIONS.get(as_ident.as_str()) {
return Ok(IntermediateValue::Value(HigherIntermediateValue::Function(
SassFunction::Builtin(f.clone(), as_ident),
self.parse_call_args()?,
))
.span(span));
} else {
// check for special cased CSS functions
match unvendor(&lower) {
"calc" | "element" | "expression" => {
s = lower;
self.parse_calc_args(&mut s)?;
}
"url" => match self.try_parse_url()? {
Some(val) => s = val,
None => s.push_str(&self.parse_call_args()?.to_css_string()?),
},
_ => s.push_str(&self.parse_call_args()?.to_css_string()?),
}
return Ok(IntermediateValue::Value(HigherIntermediateValue::Literal(
Value::String(s, QuoteKind::None),
))
.span(span));
}
}
};
let call_args = self.parse_call_args()?;
return Ok(IntermediateValue::Value(HigherIntermediateValue::Function(
SassFunction::UserDefined(Box::new(func), as_ident),
call_args,
))
.span(span));
_ => {}
}
// check for named colors
@ -442,7 +493,7 @@ impl<'a> Parser<'a> {
|| (!kind.is_ascii() && !kind.is_control())
|| (kind == '-' && self.next_is_hypen()) =>
{
return Some(self.parse_ident_value());
return Some(self.parse_ident_value(predicate));
}
'0'..='9' | '.' => {
let Spanned {
@ -478,7 +529,7 @@ impl<'a> Parser<'a> {
let n = Rational64::new_raw(parse_i64(&val.num), 1);
return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension(
Number::new_small(n),
Some(Number::new_small(n)),
unit,
false,
)),
@ -491,7 +542,7 @@ impl<'a> Parser<'a> {
let n = Rational64::new(parse_i64(&val.num), pow(10, val.dec_len));
return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension(
Number::new_small(n),
Some(Number::new_small(n)),
unit,
false,
)),
@ -504,7 +555,7 @@ impl<'a> Parser<'a> {
if val.times_ten.is_empty() {
return Some(Ok(IntermediateValue::Value(
HigherIntermediateValue::Literal(Value::Dimension(
Number::new_big(n),
Some(Number::new_big(n)),
unit,
false,
)),
@ -533,7 +584,7 @@ impl<'a> Parser<'a> {
};
IntermediateValue::Value(HigherIntermediateValue::Literal(Value::Dimension(
Number::new_big(n * times_ten),
Some(Number::new_big(n * times_ten)),
unit,
false,
)))
@ -547,6 +598,7 @@ impl<'a> Parser<'a> {
};
// todo: the above shouldn't eat the closing paren
if let Some(last_tok) = inner.pop() {
// todo: we should remove this like we did for square braces
if last_tok.kind != ')' {
return Some(Err(("expected \")\".", span).into()));
}
@ -570,7 +622,7 @@ impl<'a> Parser<'a> {
if let Some(Token { kind: '{', pos }) = self.toks.peek_forward(1) {
self.span_before = *pos;
self.toks.reset_cursor();
return Some(self.parse_ident_value());
return Some(self.parse_ident_value(predicate));
}
self.toks.reset_cursor();
self.toks.next();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,6 +34,10 @@ impl SassMap {
SassMap(Vec::new())
}
pub const fn new_with(elements: Vec<(Value, Value)>) -> SassMap {
SassMap(elements)
}
/// We take by value here (consuming the map) in order to
/// save a clone of the value, since the only place this
/// should be called is in a builtin function, which throws

View File

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

View File

@ -8,7 +8,11 @@ use std::{
use num_bigint::BigInt;
use num_rational::{BigRational, Rational64};
use num_traits::{CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, Zero};
use num_traits::{
CheckedAdd, CheckedDiv, CheckedMul, CheckedSub, Num, One, Signed, ToPrimitive, Zero,
};
use crate::unit::{Unit, UNIT_CONVERSION_TABLE};
use integer::Integer;
@ -16,7 +20,7 @@ mod integer;
const PRECISION: usize = 10;
#[derive(Clone, Eq, PartialEq, Ord)]
#[derive(Clone, Eq, PartialEq)]
pub(crate) enum Number {
Small(Rational64),
Big(Box<BigRational>),
@ -106,6 +110,84 @@ impl Number {
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 {
@ -321,6 +403,30 @@ impl PartialOrd for Number {
}
}
impl Ord for Number {
fn cmp(&self, other: &Self) -> Ordering {
match self {
Self::Small(val1) => match other {
Self::Small(val2) => val1.cmp(val2),
Self::Big(val2) => {
let tuple: (i64, i64) = (*val1).into();
BigRational::new_raw(BigInt::from(tuple.0), BigInt::from(tuple.1)).cmp(val2)
}
},
Self::Big(val1) => match other {
Self::Small(val2) => {
let tuple: (i64, i64) = (*val2).into();
(**val1).cmp(&BigRational::new_raw(
BigInt::from(tuple.0),
BigInt::from(tuple.1),
))
}
Self::Big(val2) => val1.cmp(val2),
},
}
}
}
impl Add for Number {
type Output = Self;

View File

@ -169,3 +169,8 @@ test!(
"a {\n color: 1 + 3 / 4;\n}\n",
"a {\n color: 1.75;\n}\n"
);
test!(
zero_div_zero_is_nan,
"a {\n color: (0 / 0);\n}\n",
"a {\n color: NaN;\n}\n"
);

View File

@ -121,3 +121,8 @@ test!(
"a {\n color: call(call(get-function(get-function), darken), red, 10%);\n}\n",
"a {\n color: #cc0000;\n}\n"
);
test!(
get_function_of_module,
"@use 'sass:math';\na {\n color: call(get-function(cos, $module: math), 2);\n}\n",
"a {\n color: -0.4161468365;\n}\n"
);

View File

@ -1,43 +1,10 @@
#![cfg(test)]
use std::io::Write;
use tempfile::Builder;
#[macro_use]
mod macros;
/// Create a temporary file with the given name
/// and contents.
///
/// This must be a macro rather than a function
/// because the tempfile will be deleted when it
/// exits scope
macro_rules! tempfile {
($name:literal, $content:literal) => {
let mut f = Builder::new()
.rand_bytes(0)
.prefix("")
.suffix($name)
.tempfile_in("")
.unwrap();
write!(f, "{}", $content).unwrap();
};
($name:literal, $content:literal, dir=$dir:literal) => {
let _d = Builder::new()
.rand_bytes(0)
.prefix("")
.suffix($dir)
.tempdir_in("")
.unwrap();
let mut f = Builder::new()
.rand_bytes(0)
.prefix("")
.suffix($name)
.tempfile_in($dir)
.unwrap();
write!(f, "{}", $content).unwrap();
};
}
#[test]
fn imports_variable() {
let input = "@import \"imports_variable\";\na {\n color: $a;\n}";
@ -59,17 +26,8 @@ fn import_no_semicolon() {
fn import_no_quotes() {
let input = "@import import_no_quotes";
tempfile!("import_no_quotes", "$a: red;");
match grass::from_string(input.to_string(), &grass::Options::default()) {
Ok(..) => panic!("did not fail"),
Err(e) => assert_eq!(
"Error: Expected string.",
e.to_string()
.chars()
.take_while(|c| *c != '\n')
.collect::<String>()
.as_str()
),
}
assert_err!("Error: Expected string.", input);
}
#[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: number;\n}\n"
);
test!(
type_of_nan,
"a {\n color: type-of((0 / 0))\n}\n",
"a {\n color: number;\n}\n"
);
test!(
type_of_arglist,
"@mixin foo($a...) {color: type-of($a);}\na {@include foo(1, 2, 3, 4, 5);}",

View File

@ -63,3 +63,8 @@ test!(
"a {\n color: 0 < 1;\n}\n",
"a {\n color: true;\n}\n"
);
test!(
ord_the_same_as_partial_ord,
"a {\n color: 2in > 1cm;\n}\n",
"a {\n color: true;\n}\n"
);

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