diff --git a/src/args.rs b/src/args.rs index 8dfcc14..efa2cd0 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,7 +1,8 @@ use std::collections::HashMap; use std::iter::Peekable; -use crate::common::Pos; +use codemap::{Span, Spanned}; + use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -29,7 +30,7 @@ impl FuncArgs { } #[derive(Debug, Clone)] -pub(crate) struct CallArgs(HashMap>); +pub(crate) struct CallArgs(HashMap>, Span); #[derive(Debug, Clone, Hash, Eq, PartialEq)] enum CallArg { @@ -38,9 +39,9 @@ enum CallArg { } impl CallArg { - pub fn position(&self) -> SassResult { + pub fn position(&self) -> Result { match self { - Self::Named(..) => Err("found named".into()), + Self::Named(ref name) => Err(name.clone()), Self::Positional(p) => Ok(*p), } } @@ -54,26 +55,45 @@ impl CallArg { } impl CallArgs { - pub fn new() -> Self { - CallArgs(HashMap::new()) + pub fn new(span: Span) -> Self { + CallArgs(HashMap::new(), span) } - pub fn to_css_string(self, scope: &Scope, super_selector: &Selector) -> SassResult { + pub fn to_css_string( + self, + scope: &Scope, + super_selector: &Selector, + ) -> SassResult> { let mut string = String::with_capacity(2 + self.len() * 10); string.push('('); + let mut span = self.1; + + if self.is_empty() { + return Ok(Spanned { + node: "()".to_string(), + span, + }); + } + let args = match self.get_variadic(scope, super_selector) { Ok(v) => v, - Err(..) => return Err("Plain CSS functions don't support keyword arguments.".into()), + Err(..) => { + return Err(("Plain CSS functions don't support keyword arguments.", span).into()) + } }; + string.push_str( &args .iter() - .map(std::string::ToString::to_string) - .collect::>() + .map(|a| { + span = span.merge(a.span); + Ok(a.node.to_css_string(a.span)?) + }) + .collect::>>()? .join(", "), ); string.push(')'); - Ok(string) + Ok(Spanned { node: string, span }) } /// Get argument by name @@ -84,7 +104,7 @@ impl CallArgs { val: String, scope: &Scope, super_selector: &Selector, - ) -> Option> { + ) -> Option>> { match self.0.remove(&CallArg::Named(val)) { Some(v) => Some(Value::from_vec(v, scope, super_selector)), None => None, @@ -99,20 +119,28 @@ impl CallArgs { val: usize, scope: &Scope, super_selector: &Selector, - ) -> Option> { + ) -> Option>> { match self.0.remove(&CallArg::Positional(val)) { Some(v) => Some(Value::from_vec(v, scope, super_selector)), None => None, } } - pub fn get_variadic(self, scope: &Scope, super_selector: &Selector) -> SassResult> { + pub fn get_variadic( + self, + scope: &Scope, + super_selector: &Selector, + ) -> SassResult>> { let mut vals = Vec::new(); - let mut args = self + let mut args = match self .0 .into_iter() .map(|(a, v)| Ok((a.position()?, v))) - .collect::)>>>()?; + .collect::)>, String>>() + { + Ok(v) => v, + Err(e) => return Err((format!("No argument named ${}.", e), self.1).into()), + }; args.sort_by(|(a1, _), (a2, _)| a1.cmp(a2)); for arg in args { vals.push(Value::from_vec(arg.1, scope, super_selector)?); @@ -126,15 +154,20 @@ impl CallArgs { .into_iter() .map(|(k, v)| (k.decrement(), v)) .collect(), + self.1, ) } + pub fn span(&self) -> Span { + self.1 + } + pub fn len(&self) -> usize { self.0.len() } pub fn is_empty(&self) -> bool { - self.0.len() == 0 + self.0.is_empty() } } @@ -155,8 +188,8 @@ pub(crate) fn eat_func_args>( let mut default: Vec = Vec::new(); let mut is_variadic = false; devour_whitespace(toks); - let kind = match toks.next() { - Some(Token { kind, .. }) => kind, + let (kind, span) = match toks.next() { + Some(Token { kind, pos }) => (kind, pos), _ => todo!("unexpected eof"), }; match kind { @@ -189,15 +222,18 @@ pub(crate) fn eat_func_args>( } } '.' => { - if toks.next().ok_or("expected \".\".")?.kind != '.' { - return Err("expected \".\".".into()); + let next = toks.next().ok_or(("expected \".\".", span))?; + if next.kind != '.' { + return Err(("expected \".\".", next.pos()).into()); } - if toks.next().ok_or("expected \".\".")?.kind != '.' { - return Err("expected \".\".".into()); + let next = toks.next().ok_or(("expected \".\".", next.pos()))?; + if next.kind != '.' { + return Err(("expected \".\".", next.pos()).into()); } devour_whitespace(toks); - if toks.next().ok_or("expected \")\".")?.kind != ')' { - return Err("expected \")\".".into()); + let next = toks.next().ok_or(("expected \")\".", next.pos()))?; + if next.kind != ')' { + return Err(("expected \")\".", next.pos()).into()); } is_variadic = true; @@ -247,24 +283,31 @@ pub(crate) fn eat_call_args>( devour_whitespace_or_comment(toks)?; let mut name = String::new(); let mut val: Vec = Vec::new(); + let span = toks.peek().unwrap().pos(); loop { match toks.peek().unwrap().kind { '$' => { - toks.next(); + let Token { pos, .. } = toks.next().unwrap(); let v = eat_ident(toks, scope, super_selector)?; devour_whitespace_or_comment(toks)?; if toks.peek().unwrap().kind == ':' { toks.next(); - name = v; + name = v.node; } else { - val.push(Token::new(Pos::new(), '$')); - val.extend(v.chars().map(|x| Token::new(Pos::new(), x))); + val.push(Token::new(pos, '$')); + let mut current_pos = 0; + val.extend(v.chars().map(|x| { + let len = x.len_utf8() as u64; + let tok = Token::new(v.span.subspan(current_pos, current_pos + len), x); + current_pos += len; + tok + })); name.clear(); } } ')' => { toks.next(); - return Ok(CallArgs(args)); + return Ok(CallArgs(args, span)); } _ => name.clear(), } @@ -281,7 +324,7 @@ pub(crate) fn eat_call_args>( }, val, ); - return Ok(CallArgs(args)); + return Ok(CallArgs(args, span)); } ',' => break, '[' => { @@ -312,7 +355,7 @@ pub(crate) fn eat_call_args>( devour_whitespace(toks); if toks.peek().is_none() { - return Ok(CallArgs(args)); + return Ok(CallArgs(args, span)); } } } diff --git a/src/atrule/for_rule.rs b/src/atrule/for_rule.rs index cba6650..d2077d9 100644 --- a/src/atrule/for_rule.rs +++ b/src/atrule/for_rule.rs @@ -1,5 +1,7 @@ use std::iter::Peekable; +use codemap::{Span, Spanned}; + use num_traits::cast::ToPrimitive; use super::parse::eat_stmts; @@ -19,18 +21,19 @@ pub(crate) fn parse_for>( toks: &mut Peekable, scope: &mut Scope, super_selector: &Selector, + span: Span, ) -> SassResult { let mut stmts = Vec::new(); devour_whitespace(toks); - let var = match toks.next().ok_or("expected \"$\".")?.kind { + let var = match toks.next().ok_or(("expected \"$\".", span))?.kind { '$' => eat_ident(toks, scope, super_selector)?, - _ => return Err("expected \"$\".".into()), + _ => return Err(("expected \"$\".", span).into()), }; devour_whitespace(toks); if toks.peek().is_none() || eat_ident(toks, scope, super_selector)?.to_ascii_lowercase() != "from" { - return Err("Expected \"from\".".into()); + return Err(("Expected \"from\".", var.span).into()); } devour_whitespace(toks); let mut from_toks = Vec::new(); @@ -93,27 +96,41 @@ pub(crate) fn parse_for>( } } '{' => { - return Err("Expected \"to\" or \"through\".".into()); + return Err(("Expected \"to\" or \"through\".", tok.pos()).into()); } _ => from_toks.extend(these_toks), } } - let from = match Value::from_vec(from_toks, scope, super_selector)? { + let from_val = Value::from_vec(from_toks, scope, super_selector)?; + let from = match from_val.node { Value::Dimension(n, _) => match n.to_integer().to_usize() { Some(v) => v, - None => return Err(format!("{} is not a int.", n).into()), + None => return Err((format!("{} is not a int.", n), from_val.span).into()), }, - v => return Err(format!("{} is not an integer.", v).into()), + v => { + return Err(( + format!("{} is not an integer.", v.to_css_string(from_val.span)?), + from_val.span, + ) + .into()) + } }; devour_whitespace(toks); let to_toks = read_until_open_curly_brace(toks); toks.next(); - let to = match Value::from_vec(to_toks, scope, super_selector)? { + let to_val = Value::from_vec(to_toks, scope, super_selector)?; + let to = match to_val.node { Value::Dimension(n, _) => match n.to_integer().to_usize() { Some(v) => v, - None => return Err(format!("{} is not a int.", n).into()), + None => return Err((format!("{} is not a int.", n), to_val.span).into()), }, - v => return Err(format!("{} is not an integer.", v).into()), + v => { + return Err(( + format!("{} is not an integer.", v.to_css_string(to_val.span)?), + to_val.span, + ) + .into()) + } }; let body = read_until_closing_curly_brace(toks); toks.next(); @@ -130,7 +147,13 @@ pub(crate) fn parse_for>( }; for i in iter { - scope.insert_var(&var, Value::Dimension(Number::from(i), Unit::None))?; + scope.insert_var( + &var, + Spanned { + node: Value::Dimension(Number::from(i), Unit::None), + span: var.span, + }, + )?; stmts.extend(eat_stmts( &mut body.clone().into_iter().peekable(), scope, diff --git a/src/atrule/function.rs b/src/atrule/function.rs index 5789069..d495f32 100644 --- a/src/atrule/function.rs +++ b/src/atrule/function.rs @@ -2,9 +2,10 @@ use std::iter::Peekable; use super::eat_stmts; +use codemap::{Span, Spanned}; + use crate::args::{eat_func_args, CallArgs, FuncArgs}; use crate::atrule::AtRule; -use crate::common::Pos; use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -16,8 +17,8 @@ use crate::{Stmt, Token}; pub(crate) struct Function { scope: Scope, args: FuncArgs, - body: Vec, - pos: Pos, + body: Vec>, + pos: Span, } impl PartialEq for Function { @@ -29,7 +30,7 @@ impl PartialEq for Function { impl Eq for Function {} impl Function { - pub fn new(scope: Scope, args: FuncArgs, body: Vec, pos: Pos) -> Self { + pub fn new(scope: Scope, args: FuncArgs, body: Vec>, pos: Span) -> Self { Function { scope, args, @@ -43,12 +44,12 @@ impl Function { scope: Scope, super_selector: &Selector, ) -> SassResult<(String, Function)> { - let pos = toks.peek().unwrap().pos; - let name = eat_ident(toks, &scope, super_selector)?; + let Spanned { node: name, span } = eat_ident(toks, &scope, super_selector)?; devour_whitespace(toks); let args = match toks.next() { Some(Token { kind: '(', .. }) => eat_func_args(toks, &scope, super_selector)?, - _ => return Err("expected \"(\".".into()), + Some(Token { pos, .. }) => return Err(("expected \"(\".", pos).into()), + None => return Err(("expected \"(\".", span).into()), }; devour_whitespace(toks); @@ -56,7 +57,7 @@ impl Function { let body = eat_stmts(toks, &mut scope.clone(), super_selector)?; devour_whitespace(toks); - Ok((name, Function::new(scope, args, body, pos))) + Ok((name, Function::new(scope, args, body, span))) } pub fn args( @@ -67,9 +68,13 @@ impl Function { ) -> SassResult { for (idx, arg) in self.args.0.iter().enumerate() { if arg.is_variadic { + let span = args.span(); self.scope.insert_var( &arg.name, - Value::ArgList(args.get_variadic(scope, super_selector)?), + Spanned { + node: Value::ArgList(args.get_variadic(scope, super_selector)?), + span, + }, )?; break; } @@ -83,7 +88,11 @@ impl Function { scope, super_selector, )?, - None => return Err(format!("Missing argument ${}.", &arg.name).into()), + None => { + return Err( + (format!("Missing argument ${}.", &arg.name), args.span()).into() + ) + } }, }, }; @@ -92,19 +101,20 @@ impl Function { Ok(self) } - pub fn body(&self) -> Vec { + pub fn body(&self) -> Vec> { self.body.clone() } - pub fn call(&self, super_selector: &Selector, stmts: Vec) -> SassResult { + pub fn call(&self, super_selector: &Selector, stmts: Vec>) -> SassResult { for stmt in stmts { - match stmt { + match stmt.node { Stmt::AtRule(AtRule::Return(toks)) => { - return Value::from_tokens( + return Ok(Value::from_tokens( &mut toks.into_iter().peekable(), &self.scope, super_selector, - ) + )? + .node) } Stmt::AtRule(AtRule::For(..)) => todo!("@for in function"), Stmt::AtRule(AtRule::If(i)) => { @@ -115,9 +125,9 @@ impl Function { return Ok(v); } } - _ => return Err("This at-rule is not allowed here.".into()), + _ => return Err(("This at-rule is not allowed here.", stmt.span).into()), } } - Err("Function finished without @return.".into()) + Err(("Function finished without @return.", self.pos).into()) } } diff --git a/src/atrule/if_rule.rs b/src/atrule/if_rule.rs index 25f2fc0..05d94ca 100644 --- a/src/atrule/if_rule.rs +++ b/src/atrule/if_rule.rs @@ -1,5 +1,7 @@ use std::iter::Peekable; +use codemap::Spanned; + use super::{eat_stmts, AtRule}; use crate::error::SassResult; @@ -72,7 +74,7 @@ impl If { break; } _ => { - return Err("expected \"{\".".into()); + return Err(("expected \"{\".", tok.pos()).into()); } } } else { @@ -90,12 +92,17 @@ impl If { Ok(If { branches, else_ }) } - pub fn eval(self, scope: &mut Scope, super_selector: &Selector) -> SassResult> { + pub fn eval( + self, + scope: &mut Scope, + super_selector: &Selector, + ) -> SassResult>> { let mut stmts = Vec::new(); let mut toks = Vec::new(); let mut found_true = false; for branch in self.branches { - if Value::from_vec(branch.cond, scope, super_selector)?.is_true()? { + let val = Value::from_vec(branch.cond, scope, super_selector)?; + if val.node.is_true(val.span)? { toks = branch.toks; found_true = true; break; @@ -105,10 +112,10 @@ impl If { toks = self.else_; } for stmt in eat_stmts(&mut toks.into_iter().peekable(), scope, super_selector)? { - match stmt { + match stmt.node { Stmt::AtRule(AtRule::If(i)) => stmts.extend(i.eval(scope, super_selector)?), Stmt::RuleSet(r) if r.selector.is_empty() => stmts.extend(r.rules), - v => stmts.push(v), + _ => stmts.push(stmt), } } Ok(stmts) diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs index b924b26..502d646 100644 --- a/src/atrule/mixin.rs +++ b/src/atrule/mixin.rs @@ -1,6 +1,8 @@ use std::iter::Peekable; use std::vec::IntoIter; +use codemap::Spanned; + use super::eat_stmts; use crate::args::{eat_call_args, eat_func_args, CallArgs, FuncArgs}; @@ -19,11 +21,16 @@ pub(crate) struct Mixin { scope: Scope, args: FuncArgs, body: Peekable>, - content: Vec, + content: Vec>, } impl Mixin { - pub fn new(scope: Scope, args: FuncArgs, body: Vec, content: Vec) -> Self { + pub fn new( + scope: Scope, + args: FuncArgs, + body: Vec, + content: Vec>, + ) -> Self { let body = body.into_iter().peekable(); Mixin { scope, @@ -37,14 +44,15 @@ impl Mixin { toks: &mut Peekable, scope: &Scope, super_selector: &Selector, - ) -> SassResult<(String, Mixin)> { + ) -> SassResult> { devour_whitespace(toks); - let name = eat_ident(toks, scope, super_selector)?; + let Spanned { node: name, span } = eat_ident(toks, scope, super_selector)?; devour_whitespace(toks); let args = match toks.next() { Some(Token { kind: '(', .. }) => eat_func_args(toks, scope, super_selector)?, Some(Token { kind: '{', .. }) => FuncArgs::new(), - _ => return Err("expected \"{\".".into()), + Some(t) => return Err(("expected \"{\".", t.pos()).into()), + None => return Err(("expected \"{\".", span).into()), }; devour_whitespace(toks); @@ -52,10 +60,13 @@ impl Mixin { let mut body = read_until_closing_curly_brace(toks); body.push(toks.next().unwrap()); - Ok((name, Mixin::new(scope.clone(), args, body, Vec::new()))) + Ok(Spanned { + node: (name, Mixin::new(scope.clone(), args, body, Vec::new())), + span, + }) } - pub fn content(mut self, content: Vec) -> Mixin { + pub fn content(mut self, content: Vec>) -> Mixin { self.content = content; self } @@ -68,9 +79,13 @@ impl Mixin { ) -> SassResult { for (idx, arg) in self.args.0.iter().enumerate() { if arg.is_variadic { + let span = args.span(); self.scope.insert_var( &arg.name, - Value::ArgList(args.get_variadic(scope, super_selector)?), + Spanned { + node: Value::ArgList(args.get_variadic(scope, super_selector)?), + span, + }, )?; break; } @@ -84,7 +99,11 @@ impl Mixin { scope, super_selector, )?, - None => return Err(format!("Missing argument ${}.", &arg.name).into()), + None => { + return Err( + (format!("Missing argument ${}.", &arg.name), args.span()).into() + ) + } }, }, }; @@ -93,43 +112,64 @@ impl Mixin { Ok(self) } - pub fn call(mut self, super_selector: &Selector) -> SassResult> { + pub fn call(mut self, super_selector: &Selector) -> SassResult>> { self.eval(super_selector) } - fn eval(&mut self, super_selector: &Selector) -> SassResult> { + fn eval(&mut self, super_selector: &Selector) -> SassResult>> { let mut stmts = Vec::new(); while let Some(expr) = eat_expr(&mut self.body, &mut self.scope, super_selector)? { - match expr { + let span = expr.span; + match expr.node { Expr::AtRule(a) => match a { - AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => stmts.extend(s), + AtRule::Include(s) | AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => { + stmts.extend(s) + } AtRule::If(i) => stmts.extend(i.eval(&mut self.scope.clone(), super_selector)?), AtRule::Content => stmts.extend(self.content.clone()), - AtRule::Return(..) => return Err("This at-rule is not allowed here.".into()), - r => stmts.push(Stmt::AtRule(r)), + AtRule::Return(..) => { + return Err(("This at-rule is not allowed here.", span).into()) + } + AtRule::Debug(..) | AtRule::Warn(..) => todo!(), + r => stmts.push(Spanned { + node: Stmt::AtRule(r), + span, + }), }, - Expr::Style(s) => stmts.push(Stmt::Style(s)), - Expr::Styles(s) => stmts.extend(s.into_iter().map(Box::new).map(Stmt::Style)), - Expr::Include(s) => stmts.extend(s), + Expr::Style(s) => stmts.push(Spanned { + node: Stmt::Style(s), + span, + }), + Expr::Styles(s) => stmts.extend( + s.into_iter() + .map(Box::new) + .map(Stmt::Style) + .map(|style| Spanned { node: style, span }), + ), Expr::FunctionDecl(..) => { - return Err("Mixins may not contain function declarations.".into()) + return Err(("Mixins may not contain function declarations.", span).into()) } Expr::MixinDecl(..) => { - return Err("Mixins may not contain mixin declarations.".into()) + return Err(("Mixins may not contain mixin declarations.", span).into()) } - Expr::Debug(..) | Expr::Warn(..) => todo!(), Expr::Selector(selector) => { let rules = self.eval(&super_selector.zip(&selector))?; - stmts.push(Stmt::RuleSet(RuleSet { - super_selector: super_selector.clone(), - selector, - rules, - })); + stmts.push(Spanned { + node: Stmt::RuleSet(RuleSet { + super_selector: super_selector.clone(), + selector, + rules, + }), + span, + }); } Expr::VariableDecl(name, val) => { self.scope.insert_var(&name, *val)?; } - Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s)), + Expr::MultilineComment(s) => stmts.push(Spanned { + node: Stmt::MultilineComment(s), + span, + }), } } Ok(stmts) @@ -140,37 +180,37 @@ pub(crate) fn eat_include>( toks: &mut Peekable, scope: &Scope, super_selector: &Selector, -) -> SassResult> { +) -> SassResult>> { devour_whitespace_or_comment(toks)?; let name = eat_ident(toks, scope, super_selector)?; devour_whitespace_or_comment(toks)?; - let mut has_include = false; + let mut has_content = false; let args = if let Some(tok) = toks.next() { match tok.kind { - ';' => CallArgs::new(), + ';' => CallArgs::new(name.span), '(' => { let tmp = eat_call_args(toks, scope, super_selector)?; devour_whitespace_or_comment(toks)?; if let Some(tok) = toks.next() { match tok.kind { ';' => {} - '{' => has_include = true, + '{' => has_content = true, _ => todo!(), } } tmp } '{' => { - has_include = true; - CallArgs::new() + has_content = true; + CallArgs::new(name.span) } - _ => return Err("expected \"{\".".into()), + _ => return Err(("expected \"{\".", tok.pos()).into()), } } else { - return Err("unexpected EOF".into()); + return Err(("unexpected EOF", name.span).into()); }; devour_whitespace(toks); @@ -179,7 +219,7 @@ pub(crate) fn eat_include>( if tok.kind == '{' { toks.next(); eat_stmts(toks, &mut scope.clone(), super_selector)? - } else if has_include { + } else if has_content { eat_stmts(toks, &mut scope.clone(), super_selector)? } else { Vec::new() @@ -188,7 +228,7 @@ pub(crate) fn eat_include>( Vec::new() }; - let mixin = scope.get_mixin(&name)?.clone(); + let mixin = scope.get_mixin(name)?.clone(); let rules = mixin .args(args, scope, super_selector)? diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 72ec88f..64aac68 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -1,6 +1,8 @@ use std::iter::Peekable; -use crate::common::{Brackets, ListSeparator, Pos}; +use codemap::{Span, Spanned}; + +use crate::common::{Brackets, ListSeparator}; use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -28,71 +30,98 @@ mod unknown; #[derive(Debug, Clone)] pub(crate) enum AtRule { - Warn(Pos, String), - Debug(Pos, String), + Warn(String), + Debug(String), Mixin(String, Box), Function(String, Box), Return(Vec), Charset, Content, Unknown(UnknownAtRule), - For(Vec), - Each(Vec), - While(Vec), + For(Vec>), + Each(Vec>), + While(Vec>), + Include(Vec>), If(If), - AtRoot(Vec), + AtRoot(Vec>), } impl AtRule { pub fn from_tokens>( rule: &AtRuleKind, - pos: Pos, + kind_span: Span, toks: &mut Peekable, scope: &mut Scope, super_selector: &Selector, - ) -> SassResult { + ) -> SassResult> { devour_whitespace(toks); Ok(match rule { AtRuleKind::Error => { - let message = Value::from_vec( + let Spanned { + node: message, + span, + } = Value::from_vec( read_until_semicolon_or_closing_curly_brace(toks), scope, super_selector, )?; - return Err(message.to_string().into()); + return Err((message.to_css_string(span)?, span.merge(kind_span)).into()); } AtRuleKind::Warn => { - let message = Value::from_vec( + let Spanned { + node: message, + span, + } = Value::from_vec( read_until_semicolon_or_closing_curly_brace(toks), scope, super_selector, )?; + span.merge(kind_span); if toks.peek().unwrap().kind == ';' { - toks.next(); + kind_span.merge(toks.next().unwrap().pos()); } devour_whitespace(toks); - AtRule::Warn(pos, message.to_string()) + Spanned { + node: AtRule::Warn(message.to_css_string(span)?), + span, + } } AtRuleKind::Debug => { - let message = Value::from_vec( + let Spanned { + node: message, + span, + } = Value::from_vec( read_until_semicolon_or_closing_curly_brace(toks), scope, super_selector, )?; + span.merge(kind_span); if toks.peek().unwrap().kind == ';' { - toks.next(); + kind_span.merge(toks.next().unwrap().pos()); } devour_whitespace(toks); - AtRule::Debug(pos, message.inspect()) + Spanned { + node: AtRule::Debug(message.inspect(span)?), + span, + } } AtRuleKind::Mixin => { - let (name, mixin) = Mixin::decl_from_tokens(toks, scope, super_selector)?; - AtRule::Mixin(name, Box::new(mixin)) + let Spanned { + node: (name, mixin), + span, + } = Mixin::decl_from_tokens(toks, scope, super_selector)?; + Spanned { + node: AtRule::Mixin(name, Box::new(mixin)), + span, + } } AtRuleKind::Function => { let (name, func) = Function::decl_from_tokens(toks, scope.clone(), super_selector)?; - AtRule::Function(name, Box::new(func)) + Spanned { + node: AtRule::Function(name, Box::new(func)), + span: kind_span, + } } AtRuleKind::Return => { let v = read_until_semicolon_or_closing_curly_brace(toks); @@ -100,7 +129,10 @@ impl AtRule { toks.next(); } devour_whitespace(toks); - AtRule::Return(v) + Spanned { + node: AtRule::Return(v), + span: kind_span, + } } AtRuleKind::Use => todo!("@use not yet implemented"), AtRuleKind::Annotation => todo!("@annotation not yet implemented"), @@ -132,21 +164,27 @@ impl AtRule { is_some, )? .into_iter() - .filter_map(|s| match s { + .filter_map(|s| match s.node { Stmt::Style(..) => { styles.push(s); None } _ => Some(s), }) - .collect::>(); - let mut stmts = vec![Stmt::RuleSet(RuleSet { - selector: selector.clone(), - rules: styles, - super_selector: Selector::new(), - })]; + .collect::>>(); + let mut stmts = vec![Spanned { + node: Stmt::RuleSet(RuleSet { + selector: selector.clone(), + rules: styles, + super_selector: Selector::new(), + }), + span: kind_span, + }]; stmts.extend(raw_stmts); - AtRule::AtRoot(stmts) + Spanned { + node: AtRule::AtRoot(stmts), + span: kind_span, + } } AtRuleKind::Charset => { read_until_semicolon_or_closing_curly_brace(toks); @@ -154,36 +192,51 @@ impl AtRule { toks.next(); } devour_whitespace(toks); - AtRule::Charset + Spanned { + node: AtRule::Charset, + span: kind_span, + } } AtRuleKind::Each => { let mut stmts = Vec::new(); devour_whitespace(toks); let mut vars = Vec::new(); + let mut span = kind_span; loop { - match toks.next().ok_or("expected \"$\".")?.kind { + let next = toks.next().ok_or(("expected \"$\".", span))?; + span = next.pos(); + match next.kind { '$' => vars.push(eat_ident(toks, scope, super_selector)?), - _ => return Err("expected \"$\".".into()), + _ => return Err(("expected \"$\".", next.pos()).into()), } devour_whitespace(toks); - if toks.peek().ok_or("expected \"$\".")?.kind == ',' { + if toks + .peek() + .ok_or(("expected \"$\".", vars[vars.len() - 1].span))? + .kind + == ',' + { toks.next(); devour_whitespace(toks); } else { break; } } - if toks.peek().is_none() - || eat_ident(toks, scope, super_selector)?.to_ascii_lowercase() != "in" - { - return Err("Expected \"in\".".into()); + if toks.peek().is_none() { + todo!() + } + let i = eat_ident(toks, scope, super_selector)?; + if i.node.to_ascii_lowercase() != "in" { + return Err(("Expected \"in\".", i.span).into()); } devour_whitespace(toks); let iterator = match Value::from_vec( read_until_open_curly_brace(toks), scope, super_selector, - )? { + )? + .node + { Value::List(v, ..) => v, Value::Map(m) => m .into_iter() @@ -212,7 +265,14 @@ impl AtRule { if vars.len() == 1 { scope.insert_var( &vars[0], - Value::List(this_iterator, ListSeparator::Space, Brackets::None), + Spanned { + node: Value::List( + this_iterator, + ListSeparator::Space, + Brackets::None, + ), + span: vars[0].span, + }, )?; } else { for (var, val) in vars.clone().into_iter().zip( @@ -220,7 +280,7 @@ impl AtRule { .into_iter() .chain(std::iter::once(Value::Null).cycle()), ) { - scope.insert_var(&var, val)?; + scope.insert_var(&var, Spanned { node: val, span })?; } } @@ -230,19 +290,28 @@ impl AtRule { super_selector, )?); } - AtRule::Each(stmts) + Spanned { + node: AtRule::Each(stmts), + span: kind_span, + } } AtRuleKind::Extend => todo!("@extend not yet implemented"), - AtRuleKind::If => AtRule::If(If::from_tokens(toks)?), + AtRuleKind::If => Spanned { + node: AtRule::If(If::from_tokens(toks)?), + span: kind_span, + }, AtRuleKind::Else => todo!("@else not yet implemented"), - AtRuleKind::For => for_rule::parse_for(toks, scope, super_selector)?, + AtRuleKind::For => Spanned { + node: for_rule::parse_for(toks, scope, super_selector, kind_span)?, + span: kind_span, + }, AtRuleKind::While => { let mut stmts = Vec::new(); devour_whitespace(toks); let cond = read_until_open_curly_brace(toks); if cond.is_empty() { - return Err("Expected expression.".into()); + return Err(("Expected expression.", kind_span).into()); } toks.next(); @@ -252,23 +321,39 @@ impl AtRule { devour_whitespace(toks); - while Value::from_vec(cond.clone(), scope, super_selector)?.is_true()? { + let mut val = Value::from_vec(cond.clone(), scope, super_selector)?; + while val.node.is_true(val.span)? { stmts.extend(eat_stmts( &mut body.clone().into_iter().peekable(), scope, super_selector, )?); + val = Value::from_vec(cond.clone(), scope, super_selector)?; + } + Spanned { + node: AtRule::While(stmts), + span: kind_span, } - AtRule::While(stmts) } AtRuleKind::Keyframes => todo!("@keyframes not yet implemented"), - AtRuleKind::Unknown(name) => AtRule::Unknown(UnknownAtRule::from_tokens( - toks, - name, - scope, - super_selector, - )?), - AtRuleKind::Content => AtRule::Content, + AtRuleKind::Unknown(name) => Spanned { + node: AtRule::Unknown(UnknownAtRule::from_tokens( + toks, + name, + scope, + super_selector, + kind_span, + )?), + span: kind_span, + }, + AtRuleKind::Content => Spanned { + node: AtRule::Content, + span: kind_span, + }, + AtRuleKind::Include => Spanned { + node: AtRule::Include(eat_include(toks, scope, super_selector)?), + span: kind_span, + }, _ => todo!("encountered unimplemented at rule"), }) } diff --git a/src/atrule/parse.rs b/src/atrule/parse.rs index a5f15ce..b1c9899 100644 --- a/src/atrule/parse.rs +++ b/src/atrule/parse.rs @@ -1,5 +1,7 @@ use std::iter::Peekable; +use codemap::Spanned; + use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -9,29 +11,35 @@ pub(crate) fn eat_stmts>( toks: &mut Peekable, scope: &mut Scope, super_selector: &Selector, -) -> SassResult> { +) -> SassResult>> { let mut stmts = Vec::new(); while let Some(expr) = eat_expr(toks, scope, super_selector)? { - match expr { - Expr::AtRule(a) => stmts.push(Stmt::AtRule(a)), - Expr::Style(s) => stmts.push(Stmt::Style(s)), - Expr::Styles(s) => stmts.extend(s.into_iter().map(Box::new).map(Stmt::Style)), - Expr::Include(s) => stmts.extend(s), - Expr::MixinDecl(..) | Expr::FunctionDecl(..) | Expr::Debug(..) | Expr::Warn(..) => { - todo!() - } + let span = expr.span; + match expr.node { + Expr::AtRule(a) => stmts.push(Stmt::AtRule(a).span(span)), + Expr::Style(s) => stmts.push(Stmt::Style(s).span(span)), + Expr::Styles(s) => stmts.extend( + s.into_iter() + .map(Box::new) + .map(Stmt::Style) + .map(|style| Spanned { node: style, span }), + ), + Expr::MixinDecl(..) | Expr::FunctionDecl(..) => todo!(), Expr::Selector(selector) => { let rules = eat_stmts(toks, scope, &super_selector.zip(&selector))?; - stmts.push(Stmt::RuleSet(RuleSet { - super_selector: super_selector.clone(), - selector, - rules, - })); + stmts.push( + Stmt::RuleSet(RuleSet { + super_selector: super_selector.clone(), + selector, + rules, + }) + .span(span), + ); } Expr::VariableDecl(name, val) => { scope.insert_var(&name, *val)?; } - Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s)), + Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s).span(span)), } } Ok(stmts) @@ -43,17 +51,20 @@ pub(crate) fn eat_stmts_at_root>( super_selector: &Selector, mut nesting: usize, is_some: bool, -) -> SassResult> { +) -> SassResult>> { let mut stmts = Vec::new(); while let Some(expr) = eat_expr(toks, scope, super_selector)? { - match expr { - Expr::AtRule(a) => stmts.push(Stmt::AtRule(a)), - Expr::Style(s) => stmts.push(Stmt::Style(s)), - Expr::Styles(s) => stmts.extend(s.into_iter().map(Box::new).map(Stmt::Style)), - Expr::Include(s) => stmts.extend(s), - Expr::MixinDecl(..) | Expr::FunctionDecl(..) | Expr::Debug(..) | Expr::Warn(..) => { - todo!() - } + let span = expr.span; + match expr.node { + Expr::AtRule(a) => stmts.push(Stmt::AtRule(a).span(span)), + Expr::Style(s) => stmts.push(Stmt::Style(s).span(span)), + Expr::Styles(s) => stmts.extend( + s.into_iter() + .map(Box::new) + .map(Stmt::Style) + .map(|style| Spanned { node: style, span }), + ), + Expr::MixinDecl(..) | Expr::FunctionDecl(..) => todo!(), Expr::Selector(mut selector) => { if nesting > 1 || is_some { selector = super_selector.zip(&selector); @@ -63,20 +74,23 @@ pub(crate) fn eat_stmts_at_root>( nesting += 1; let rules = eat_stmts_at_root(toks, scope, &selector, nesting, true)?; nesting -= 1; - stmts.push(Stmt::RuleSet(RuleSet { - super_selector: if nesting > 1 { - super_selector.clone() - } else { - Selector::new() - }, - selector, - rules, - })); + stmts.push( + Stmt::RuleSet(RuleSet { + super_selector: if nesting > 1 { + super_selector.clone() + } else { + Selector::new() + }, + selector, + rules, + }) + .span(span), + ); } Expr::VariableDecl(name, val) => { scope.insert_var(&name, *val)?; } - Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s)), + Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s).span(span)), } } Ok(stmts) diff --git a/src/atrule/unknown.rs b/src/atrule/unknown.rs index 53be3e4..088b18c 100644 --- a/src/atrule/unknown.rs +++ b/src/atrule/unknown.rs @@ -1,5 +1,7 @@ use std::iter::Peekable; +use codemap::{Span, Spanned}; + use super::parse::eat_stmts; use crate::error::SassResult; use crate::scope::Scope; @@ -12,7 +14,7 @@ pub(crate) struct UnknownAtRule { pub name: String, pub super_selector: Selector, pub params: String, - pub body: Vec, + pub body: Vec>, } impl UnknownAtRule { @@ -21,6 +23,7 @@ impl UnknownAtRule { name: &str, scope: &mut Scope, super_selector: &Selector, + kind_span: Span, ) -> SassResult { let mut params = String::new(); while let Some(tok) = toks.next() { @@ -29,9 +32,8 @@ impl UnknownAtRule { '#' => { if toks.peek().unwrap().kind == '{' { toks.next(); - params.push_str( - &parse_interpolation(toks, scope, super_selector)?.to_string(), - ); + let interpolation = parse_interpolation(toks, scope, super_selector)?; + params.push_str(&interpolation.node.to_css_string(interpolation.span)?); continue; } else { params.push(tok.kind); @@ -49,20 +51,26 @@ impl UnknownAtRule { let raw_body = eat_stmts(toks, scope, super_selector)?; let mut body = Vec::with_capacity(raw_body.len()); - body.push(Stmt::RuleSet(RuleSet::new())); + body.push(Spanned { + node: Stmt::RuleSet(RuleSet::new()), + span: kind_span, + }); let mut rules = Vec::new(); for stmt in raw_body { - match stmt { - s @ Stmt::Style(..) => rules.push(s), - s => body.push(s), + match stmt.node { + Stmt::Style(..) => rules.push(stmt), + _ => body.push(stmt), } } - body[0] = Stmt::RuleSet(RuleSet { - selector: super_selector.clone(), - rules, - super_selector: Selector::new(), - }); + body[0] = Spanned { + node: Stmt::RuleSet(RuleSet { + selector: super_selector.clone(), + rules, + super_selector: Selector::new(), + }), + span: kind_span, + }; Ok(UnknownAtRule { name: name.to_owned(), diff --git a/src/builtin/color/hsl.rs b/src/builtin/color/hsl.rs index e5be15f..d7614e2 100644 --- a/src/builtin/color/hsl.rs +++ b/src/builtin/color/hsl.rs @@ -13,39 +13,66 @@ pub(crate) fn register(f: &mut HashMap) { "hsl".to_owned(), Builtin::new(|mut args, scope, super_selector| { if args.is_empty() { - return Err("Missing argument $channels.".into()); + return Err(("Missing argument $channels.", args.span()).into()); } if args.len() == 1 { let mut channels = match arg!(args, scope, super_selector, 0, "channels") { Value::List(v, ..) => v, - _ => return Err("Missing argument $channels.".into()), + _ => return Err(("Missing argument $channels.", args.span()).into()), }; if channels.len() > 3 { - return Err(format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), ) - .into()); + .into()); } let lightness = match channels.pop() { Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => return Err(format!("$lightness: {} is not a number.", v).into()), - None => return Err("Missing element $lightness.".into()), + Some(v) => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $lightness.", args.span()).into()), }; let saturation = match channels.pop() { Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => return Err(format!("$saturation: {} is not a number.", v).into()), - None => return Err("Missing element $saturation.".into()), + Some(v) => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $saturation.", args.span()).into()), }; let hue = match channels.pop() { Some(Value::Dimension(n, _)) => n, - Some(v) => return Err(format!("$hue: {} is not a number.", v).into()), - None => return Err("Missing element $hue.".into()), + Some(v) => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $hue.", args.span()).into()), }; Ok(Value::Color(Color::from_hsla( @@ -60,48 +87,90 @@ pub(crate) fn register(f: &mut HashMap) { v if v.is_special_function() => { let saturation = arg!(args, scope, super_selector, 1, "saturation"); let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!("hsl({}, {}, {}", v, saturation, lightness); + let mut string = format!( + "hsl({}, {}, {}", + v.to_css_string(args.span())?, + saturation.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$hue: {} is not a number.", v).into()), + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let saturation = match arg!(args, scope, super_selector, 1, "saturation") { Value::Dimension(n, _) => n / Number::from(100), v if v.is_special_function() => { let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!("hsl({}, {}, {}", hue, v, lightness); + let mut string = format!( + "hsl({}, {}, {}", + hue, + v.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$saturation: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let lightness = match arg!(args, scope, super_selector, 2, "lightness") { Value::Dimension(n, _) => n / Number::from(100), v if v.is_special_function() => { - let mut string = format!("hsl({}, {}, {}", hue, saturation, v); + let mut string = format!( + "hsl({}, {}, {}", + hue, + saturation, + v.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$lightness: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let alpha = match arg!( args, @@ -113,17 +182,34 @@ pub(crate) fn register(f: &mut HashMap) { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { return Ok(Value::Ident( - format!("hsl({}, {}, {}, {})", hue, saturation, lightness, v), + format!( + "hsl({}, {}, {}, {})", + hue, + saturation, + lightness, + v.to_css_string(args.span())? + ), QuoteKind::None, )); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(Color::from_hsla( hue, saturation, lightness, alpha, @@ -135,39 +221,66 @@ pub(crate) fn register(f: &mut HashMap) { "hsla".to_owned(), Builtin::new(|mut args, scope, super_selector| { if args.is_empty() { - return Err("Missing argument $channels.".into()); + return Err(("Missing argument $channels.", args.span()).into()); } if args.len() == 1 { let mut channels = match arg!(args, scope, super_selector, 0, "channels") { Value::List(v, ..) => v, - _ => return Err("Missing argument $channels.".into()), + _ => return Err(("Missing argument $channels.", args.span()).into()), }; if channels.len() > 3 { - return Err(format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), ) - .into()); + .into()); } let lightness = match channels.pop() { Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => return Err(format!("$lightness: {} is not a number.", v).into()), - None => return Err("Missing element $lightness.".into()), + Some(v) => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $lightness.", args.span()).into()), }; let saturation = match channels.pop() { Some(Value::Dimension(n, _)) => n / Number::from(100), - Some(v) => return Err(format!("$saturation: {} is not a number.", v).into()), - None => return Err("Missing element $saturation.".into()), + Some(v) => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $saturation.", args.span()).into()), }; let hue = match channels.pop() { Some(Value::Dimension(n, _)) => n, - Some(v) => return Err(format!("$hue: {} is not a number.", v).into()), - None => return Err("Missing element $hue.".into()), + Some(v) => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $hue.", args.span()).into()), }; Ok(Value::Color(Color::from_hsla( @@ -182,48 +295,90 @@ pub(crate) fn register(f: &mut HashMap) { v if v.is_special_function() => { let saturation = arg!(args, scope, super_selector, 1, "saturation"); let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!("hsla({}, {}, {}", v, saturation, lightness); + let mut string = format!( + "hsla({}, {}, {}", + v.to_css_string(args.span())?, + saturation.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$hue: {} is not a number.", v).into()), + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let saturation = match arg!(args, scope, super_selector, 1, "saturation") { Value::Dimension(n, _) => n / Number::from(100), v if v.is_special_function() => { let lightness = arg!(args, scope, super_selector, 2, "lightness"); - let mut string = format!("hsla({}, {}, {}", hue, v, lightness); + let mut string = format!( + "hsla({}, {}, {}", + hue, + v.to_css_string(args.span())?, + lightness.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$saturation: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$saturation: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let lightness = match arg!(args, scope, super_selector, 2, "lightness") { Value::Dimension(n, _) => n / Number::from(100), v if v.is_special_function() => { - let mut string = format!("hsla({}, {}, {}", hue, saturation, v); + let mut string = format!( + "hsla({}, {}, {}", + hue, + saturation, + v.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$lightness: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$lightness: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let alpha = match arg!( args, @@ -235,17 +390,34 @@ pub(crate) fn register(f: &mut HashMap) { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { return Ok(Value::Ident( - format!("hsl({}, {}, {}, {})", hue, saturation, lightness, v), + format!( + "hsl({}, {}, {}, {})", + hue, + saturation, + lightness, + v.to_css_string(args.span())? + ), QuoteKind::None, )); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(Color::from_hsla( hue, saturation, lightness, alpha, @@ -259,7 +431,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.hue(), Unit::Deg)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -269,7 +445,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.saturation(), Unit::Percent)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -279,7 +459,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.lightness(), Unit::Percent)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -289,11 +473,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let degrees = match arg!(args, scope, super_selector, 1, "degrees") { Value::Dimension(n, _) => n, - v => return Err(format!("$degrees: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$degrees: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.adjust_hue(degrees))) }), @@ -304,11 +503,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.lighten(amount))) }), @@ -319,11 +533,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.darken(amount))) }), @@ -337,14 +566,24 @@ pub(crate) fn register(f: &mut HashMap) { format!( "saturate({})", arg!(args, scope, super_selector, 0, "amount") + .to_css_string(args.span())? ), QuoteKind::None, )); } let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, @@ -354,7 +593,13 @@ pub(crate) fn register(f: &mut HashMap) { QuoteKind::None, )) } - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.saturate(amount))) }), @@ -365,11 +610,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.desaturate(amount))) }), @@ -386,7 +646,13 @@ pub(crate) fn register(f: &mut HashMap) { QuoteKind::None, )) } - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.desaturate(Number::one()))) }), @@ -397,7 +663,13 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.complement())) }), @@ -413,18 +685,33 @@ pub(crate) fn register(f: &mut HashMap) { 1, "weight" = Value::Dimension(Number::from(100), Unit::Percent) ) { - Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$weight: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$weight: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Color(c.invert(weight))), Value::Dimension(n, Unit::Percent) => { Ok(Value::Ident(format!("invert({}%)", n), QuoteKind::None)) } - Value::Dimension(..) => Err( - "Only one argument may be passed to the plain-CSS invert() function.".into(), - ), - v => Err(format!("$color: {} is not a color.", v).into()), + Value::Dimension(..) => Err(( + "Only one argument may be passed to the plain-CSS invert() function.", + args.span(), + ) + .into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); diff --git a/src/builtin/color/opacity.rs b/src/builtin/color/opacity.rs index f21fe94..636f6c1 100644 --- a/src/builtin/color/opacity.rs +++ b/src/builtin/color/opacity.rs @@ -13,7 +13,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.alpha(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -27,7 +31,11 @@ pub(crate) fn register(f: &mut HashMap) { format!("opacity({}{})", num, unit), QuoteKind::None, )), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -37,11 +45,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.fade_in(amount))) }), @@ -52,11 +75,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.fade_in(amount))) }), @@ -67,11 +105,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.fade_out(amount))) }), @@ -82,11 +135,26 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let amount = match arg!(args, scope, super_selector, 1, "amount") { - Value::Dimension(n, u) => bound!("amount", n, u, 0, 1), - v => return Err(format!("$amount: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "amount", n, u, 0, 1), + v => { + return Err(( + format!( + "$amount: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.fade_out(amount))) }), diff --git a/src/builtin/color/other.rs b/src/builtin/color/other.rs index bd14852..cb46d92 100644 --- a/src/builtin/color/other.rs +++ b/src/builtin/color/other.rs @@ -12,9 +12,19 @@ macro_rules! opt_rgba { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal, $scope:ident, $super_selector:ident) => { let x = $low; let $name = match named_arg!($args, $scope, $super_selector, $arg = Value::Null) { - Value::Dimension(n, u) => Some(bound!($arg, n, u, x, $high)), + Value::Dimension(n, u) => Some(bound!($args, $arg, n, u, x, $high)), Value::Null => None, - v => return Err(format!("${}: {} is not a number.", $arg, v).into()), + v => { + return Err(( + format!( + "${}: {} is not a number.", + $arg, + v.to_css_string($args.span())? + ), + $args.span(), + ) + .into()) + } }; }; } @@ -23,9 +33,19 @@ macro_rules! opt_hsl { ($args:ident, $name:ident, $arg:literal, $low:literal, $high:literal, $scope:ident, $super_selector:ident) => { let x = $low; let $name = match named_arg!($args, $scope, $super_selector, $arg = Value::Null) { - Value::Dimension(n, u) => Some(bound!($arg, n, u, x, $high) / Number::from(100)), + Value::Dimension(n, u) => Some(bound!($args, $arg, n, u, x, $high) / Number::from(100)), Value::Null => None, - v => return Err(format!("${}: {} is not a number.", $arg, v).into()), + v => { + return Err(( + format!( + "${}: {} is not a number.", + $arg, + v.to_css_string($args.span())? + ), + $args.span(), + ) + .into()) + } }; }; } @@ -33,12 +53,12 @@ macro_rules! opt_hsl { pub(crate) fn register(f: &mut HashMap) { f.insert("change-color".to_owned(), Builtin::new(|mut args, scope, super_selector| { if args.get_positional(1, scope, super_selector).is_some() { - return Err("Only one positional argument is allowed. All other arguments must be passed by name.".into()); + return Err(("Only one positional argument is allowed. All other arguments must be passed by name.", args.span()).into()); } let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => return Err((format!("$color: {} is not a color.", v.to_css_string(args.span())?), args.span()).into()), }; opt_rgba!(args, alpha, "alpha", 0, 1, scope, super_selector); @@ -53,7 +73,7 @@ pub(crate) fn register(f: &mut HashMap) { let hue = match named_arg!(args, scope, super_selector, "hue"=Value::Null) { Value::Dimension(n, _) => Some(n), Value::Null => None, - v => return Err(format!("$hue: {} is not a number.", v).into()), + v => return Err((format!("$hue: {} is not a number.", v.to_css_string(args.span())?), args.span()).into()), }; opt_hsl!(args, saturation, "saturation", 0, 100, scope, super_selector); @@ -76,7 +96,13 @@ pub(crate) fn register(f: &mut HashMap) { Builtin::new(|mut args, scope, super_selector| { let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; opt_rgba!(args, alpha, "alpha", -1, 1, scope, super_selector); @@ -96,7 +122,13 @@ pub(crate) fn register(f: &mut HashMap) { let hue = match named_arg!(args, scope, super_selector, "hue" = Value::Null) { Value::Dimension(n, _) => Some(n), Value::Null => None, - v => return Err(format!("$hue: {} is not a number.", v).into()), + v => { + return Err(( + format!("$hue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; opt_hsl!( @@ -142,7 +174,7 @@ pub(crate) fn register(f: &mut HashMap) { Builtin::new(|mut args, scope, super_selector| { let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => return Err((format!("$color: {} is not a color.", v.to_css_string(args.span())?), args.span()).into()), }; macro_rules! opt_scale_arg { @@ -150,15 +182,15 @@ pub(crate) fn register(f: &mut HashMap) { let x = $low; let $name = match named_arg!($args, $scope, $super_selector, $arg = Value::Null) { Value::Dimension(n, Unit::Percent) => { - Some(bound!($arg, n, Unit::Percent, x, $high) / Number::from(100)) + Some(bound!($args, $arg, n, Unit::Percent, x, $high) / Number::from(100)) } v @ Value::Dimension(..) => { return Err( - format!("${}: Expected {} to have unit \"%\".", $arg, v).into() + (format!("${}: Expected {} to have unit \"%\".", $arg, v.to_css_string($args.span())?), $args.span()).into() ) } Value::Null => None, - v => return Err(format!("${}: {} is not a number.", $arg, v).into()), + v => return Err((format!("${}: {} is not a number.", $arg, v.to_css_string($args.span())?), $args.span()).into()), }; }; } @@ -229,7 +261,13 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); let color = match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => c, - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Ident(color.to_ie_hex_str(), QuoteKind::None)) }), diff --git a/src/builtin/color/rgb.rs b/src/builtin/color/rgb.rs index 6655aba..72bbfe9 100644 --- a/src/builtin/color/rgb.rs +++ b/src/builtin/color/rgb.rs @@ -13,21 +13,24 @@ pub(crate) fn register(f: &mut HashMap) { "rgb".to_owned(), Builtin::new(|mut args, scope, super_selector| { if args.is_empty() { - return Err("Missing argument $channels.".into()); + return Err(("Missing argument $channels.", args.span()).into()); } if args.len() == 1 { let mut channels = match arg!(args, scope, super_selector, 0, "channels") { Value::List(v, ..) => v, - _ => return Err("Missing argument $channels.".into()), + _ => return Err(("Missing argument $channels.", args.span()).into()), }; if channels.len() > 3 { - return Err(format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), ) - .into()); + .into()); } let blue = match channels.pop() { @@ -39,12 +42,23 @@ pub(crate) fn register(f: &mut HashMap) { let green = channels.pop().unwrap(); let red = channels.pop().unwrap(); return Ok(Value::Ident( - format!("rgb({}, {}, {})", red, green, v), + format!( + "rgb({}, {}, {})", + red.to_css_string(args.span())?, + green.to_css_string(args.span())?, + v.to_css_string(args.span())? + ), QuoteKind::None, )); } - Some(v) => return Err(format!("$blue: {} is not a number.", v).into()), - None => return Err("Missing element $blue.".into()), + Some(v) => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $blue.", args.span()).into()), }; let green = match channels.pop() { @@ -54,13 +68,24 @@ pub(crate) fn register(f: &mut HashMap) { } Some(v) if v.is_special_function() => { let string = match channels.pop() { - Some(red) => format!("rgb({}, {}, {})", red, v, blue), - None => format!("rgb({} {})", v, blue), + Some(red) => format!( + "rgb({}, {}, {})", + red.to_css_string(args.span())?, + v.to_css_string(args.span())?, + blue + ), + None => format!("rgb({} {})", v.to_css_string(args.span())?, blue), }; return Ok(Value::Ident(string, QuoteKind::None)); } - Some(v) => return Err(format!("$green: {} is not a number.", v).into()), - None => return Err("Missing element $green.".into()), + Some(v) => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $green.", args.span()).into()), }; let red = match channels.pop() { @@ -70,12 +95,23 @@ pub(crate) fn register(f: &mut HashMap) { } Some(v) if v.is_special_function() => { return Ok(Value::Ident( - format!("rgb({}, {}, {})", v, green, blue), + format!( + "rgb({}, {}, {})", + v.to_css_string(args.span())?, + green, + blue + ), QuoteKind::None, )); } - Some(v) => return Err(format!("$red: {} is not a number.", v).into()), - None => return Err("Missing element $red.".into()), + Some(v) => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $red.", args.span()).into()), }; let color = Color::from_rgba(red, green, blue, Number::one()); @@ -87,19 +123,34 @@ pub(crate) fn register(f: &mut HashMap) { v if v.is_special_function() => { let alpha = arg!(args, scope, super_selector, 1, "alpha"); return Ok(Value::Ident( - format!("rgb({}, {})", v, alpha), + format!( + "rgb({}, {})", + v.to_css_string(args.span())?, + alpha.to_css_string(args.span())? + ), QuoteKind::None, )); } - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let alpha = match arg!(args, scope, super_selector, 1, "alpha") { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { return Ok(Value::Ident( @@ -108,12 +159,18 @@ pub(crate) fn register(f: &mut HashMap) { color.red(), color.green(), color.blue(), - v + v.to_css_string(args.span())? ), QuoteKind::None, )); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.with_alpha(alpha))) } else { @@ -123,24 +180,41 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$red: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$red: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { let green = arg!(args, scope, super_selector, 1, "green"); let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!("rgb({}, {}, {}", v, green, blue); + let mut string = format!( + "rgb({}, {}, {}", + v.to_css_string(args.span())?, + green.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$red: {} is not a number.", v).into()), + v => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let green = match arg!(args, scope, super_selector, 1, "green") { Value::Dimension(n, Unit::None) => n, @@ -148,23 +222,40 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$green: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$green: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!("rgb({}, {}, {}", red, v, blue); + let mut string = format!( + "rgb({}, {}, {}", + red, + v.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$green: {} is not a number.", v).into()), + v => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let blue = match arg!(args, scope, super_selector, 2, "blue") { Value::Dimension(n, Unit::None) => n, @@ -172,22 +263,35 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$blue: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$blue: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { - let mut string = format!("rgb({}, {}, {}", red, green, v); + let mut string = + format!("rgb({}, {}, {}", red, green, v.to_css_string(args.span())?); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$blue: {} is not a number.", v).into()), + v => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let alpha = match arg!( args, @@ -199,15 +303,32 @@ pub(crate) fn register(f: &mut HashMap) { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { - let string = format!("rgb({}, {}, {}, {})", red, green, blue, v); + let string = format!( + "rgb({}, {}, {}, {})", + red, + green, + blue, + v.to_css_string(args.span())? + ); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) } @@ -217,21 +338,24 @@ pub(crate) fn register(f: &mut HashMap) { "rgba".to_owned(), Builtin::new(|mut args, scope, super_selector| { if args.is_empty() { - return Err("Missing argument $channels.".into()); + return Err(("Missing argument $channels.", args.span()).into()); } if args.len() == 1 { let mut channels = match arg!(args, scope, super_selector, 0, "channels") { Value::List(v, ..) => v, - _ => return Err("Missing argument $channels.".into()), + _ => return Err(("Missing argument $channels.", args.span()).into()), }; if channels.len() > 3 { - return Err(format!( - "Only 3 elements allowed, but {} were passed.", - channels.len() + return Err(( + format!( + "Only 3 elements allowed, but {} were passed.", + channels.len() + ), + args.span(), ) - .into()); + .into()); } let blue = match channels.pop() { @@ -243,12 +367,23 @@ pub(crate) fn register(f: &mut HashMap) { let green = channels.pop().unwrap(); let red = channels.pop().unwrap(); return Ok(Value::Ident( - format!("rgba({}, {}, {})", red, green, v), + format!( + "rgba({}, {}, {})", + red.to_css_string(args.span())?, + green.to_css_string(args.span())?, + v.to_css_string(args.span())? + ), QuoteKind::None, )); } - Some(v) => return Err(format!("$blue: {} is not a number.", v).into()), - None => return Err("Missing element $blue.".into()), + Some(v) => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $blue.", args.span()).into()), }; let green = match channels.pop() { @@ -258,13 +393,24 @@ pub(crate) fn register(f: &mut HashMap) { } Some(v) if v.is_special_function() => { let string = match channels.pop() { - Some(red) => format!("rgba({}, {}, {})", red, v, blue), - None => format!("rgba({} {})", v, blue), + Some(red) => format!( + "rgba({}, {}, {})", + red.to_css_string(args.span())?, + v.to_css_string(args.span())?, + blue + ), + None => format!("rgba({} {})", v.to_css_string(args.span())?, blue), }; return Ok(Value::Ident(string, QuoteKind::None)); } - Some(v) => return Err(format!("$green: {} is not a number.", v).into()), - None => return Err("Missing element $green.".into()), + Some(v) => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $green.", args.span()).into()), }; let red = match channels.pop() { @@ -274,12 +420,23 @@ pub(crate) fn register(f: &mut HashMap) { } Some(v) if v.is_special_function() => { return Ok(Value::Ident( - format!("rgba({}, {}, {})", v, green, blue), + format!( + "rgba({}, {}, {})", + v.to_css_string(args.span())?, + green, + blue + ), QuoteKind::None, )); } - Some(v) => return Err(format!("$red: {} is not a number.", v).into()), - None => return Err("Missing element $red.".into()), + Some(v) => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } + None => return Err(("Missing element $red.", args.span()).into()), }; let color = Color::from_rgba(red, green, blue, Number::one()); @@ -291,19 +448,34 @@ pub(crate) fn register(f: &mut HashMap) { v if v.is_special_function() => { let alpha = arg!(args, scope, super_selector, 1, "alpha"); return Ok(Value::Ident( - format!("rgba({}, {})", v, alpha), + format!( + "rgba({}, {})", + v.to_css_string(args.span())?, + alpha.to_css_string(args.span())? + ), QuoteKind::None, )); } - v => return Err(format!("$color: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let alpha = match arg!(args, scope, super_selector, 1, "alpha") { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { return Ok(Value::Ident( @@ -312,12 +484,18 @@ pub(crate) fn register(f: &mut HashMap) { color.red(), color.green(), color.blue(), - v + v.to_css_string(args.span())? ), QuoteKind::None, )); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color.with_alpha(alpha))) } else { @@ -327,24 +505,41 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$red: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$red: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { let green = arg!(args, scope, super_selector, 1, "green"); let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!("rgba({}, {}, {}", v, green, blue); + let mut string = format!( + "rgba({}, {}, {}", + v.to_css_string(args.span())?, + green.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$red: {} is not a number.", v).into()), + v => { + return Err(( + format!("$red: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let green = match arg!(args, scope, super_selector, 1, "green") { Value::Dimension(n, Unit::None) => n, @@ -352,23 +547,40 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$green: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$green: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { let blue = arg!(args, scope, super_selector, 2, "blue"); - let mut string = format!("rgba({}, {}, {}", red, v, blue); + let mut string = format!( + "rgba({}, {}, {}", + red, + v.to_css_string(args.span())?, + blue.to_css_string(args.span())? + ); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$green: {} is not a number.", v).into()), + v => { + return Err(( + format!("$green: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let blue = match arg!(args, scope, super_selector, 2, "blue") { Value::Dimension(n, Unit::None) => n, @@ -376,22 +588,35 @@ pub(crate) fn register(f: &mut HashMap) { (n / Number::from(100)) * Number::from(255) } v @ Value::Dimension(..) => { - return Err( - format!("$blue: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$blue: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { - let mut string = format!("rgba({}, {}, {}", red, green, v); + let mut string = + format!("rgba({}, {}, {}", red, green, v.to_css_string(args.span())?); if !args.is_empty() { string.push_str(", "); string.push_str( - &arg!(args, scope, super_selector, 3, "alpha").to_string(), + &arg!(args, scope, super_selector, 3, "alpha") + .to_css_string(args.span())?, ); } string.push(')'); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$blue: {} is not a number.", v).into()), + v => { + return Err(( + format!("$blue: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let alpha = match arg!( args, @@ -403,15 +628,32 @@ pub(crate) fn register(f: &mut HashMap) { Value::Dimension(n, Unit::None) => n, Value::Dimension(n, Unit::Percent) => n / Number::from(100), v @ Value::Dimension(..) => { - return Err( - format!("$alpha: Expected {} to have no units or \"%\".", v).into() + return Err(( + format!( + "$alpha: Expected {} to have no units or \"%\".", + v.to_css_string(args.span())? + ), + args.span(), ) + .into()) } v if v.is_special_function() => { - let string = format!("rgba({}, {}, {}, {})", red, green, blue, v); + let string = format!( + "rgba({}, {}, {}, {})", + red, + green, + blue, + v.to_css_string(args.span())? + ); return Ok(Value::Ident(string, QuoteKind::None)); } - v => return Err(format!("$alpha: {} is not a number.", v).into()), + v => { + return Err(( + format!("$alpha: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::Color(Color::from_rgba(red, green, blue, alpha))) } @@ -423,7 +665,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.red(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -433,7 +679,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.green(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -443,7 +693,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "color") { Value::Color(c) => Ok(Value::Dimension(c.blue(), Unit::None)), - v => Err(format!("$color: {} is not a color.", v).into()), + v => Err(( + format!("$color: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -453,12 +707,24 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 3); let color1 = match arg!(args, scope, super_selector, 0, "color1") { Value::Color(c) => c, - v => return Err(format!("$color1: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color1: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let color2 = match arg!(args, scope, super_selector, 1, "color2") { Value::Color(c) => c, - v => return Err(format!("$color2: {} is not a color.", v).into()), + v => { + return Err(( + format!("$color2: {} is not a color.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let weight = match arg!( @@ -468,8 +734,17 @@ pub(crate) fn register(f: &mut HashMap) { 2, "weight" = Value::Dimension(Number::from(50), Unit::None) ) { - Value::Dimension(n, u) => bound!("weight", n, u, 0, 100) / Number::from(100), - v => return Err(format!("$weight: {} is not a number.", v).into()), + Value::Dimension(n, u) => bound!(args, "weight", n, u, 0, 100) / Number::from(100), + v => { + return Err(( + format!( + "$weight: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Color(color1.mix(&color2, weight))) }), diff --git a/src/builtin/list.rs b/src/builtin/list.rs index 5d38d3f..3bd5e1f 100644 --- a/src/builtin/list.rs +++ b/src/builtin/list.rs @@ -31,24 +31,33 @@ pub(crate) fn register(f: &mut HashMap) { }; let n = match arg!(args, scope, super_selector, 1, "n") { Value::Dimension(num, _) => num, - v => return Err(format!("$n: {} is not a number.", v).into()), + v => { + return Err(( + format!("$n: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; if n.is_zero() { - return Err("$n: List index may not be 0.".into()); + return Err(("$n: List index may not be 0.", args.span()).into()); } if n.abs() > Number::from(list.len()) { - return Err(format!( - "$n: Invalid index {} for a list with {} elements.", - n, - list.len() + return Err(( + format!( + "$n: Invalid index {} for a list with {} elements.", + n, + list.len() + ), + args.span(), ) - .into()); + .into()); } if n.is_decimal() { - return Err(format!("$n: {} is not an int.", n).into()); + return Err((format!("$n: {} is not an int.", n), args.span()).into()); } if n.is_positive() { @@ -83,23 +92,31 @@ pub(crate) fn register(f: &mut HashMap) { }; let n = match arg!(args, scope, super_selector, 1, "n") { Value::Dimension(num, _) => num, - v => return Err(format!("$n: {} is not a number.", v).into()), + v => { + return Err(( + format!("$n: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; if n.is_zero() { - return Err("$n: List index may not be 0.".into()); + return Err(("$n: List index may not be 0.", args.span()).into()); } let len = list.len(); if n.abs() > Number::from(len) { - return Err( - format!("$n: Invalid index {} for a list with {} elements.", n, len).into(), - ); + return Err(( + format!("$n: Invalid index {} for a list with {} elements.", n, len), + args.span(), + ) + .into()); } if n.is_decimal() { - return Err(format!("$n: {} is not an int.", n).into()); + return Err((format!("$n: {} is not an int.", n), args.span()).into()); } let val = arg!(args, scope, super_selector, 2, "value"); @@ -134,10 +151,23 @@ pub(crate) fn register(f: &mut HashMap) { "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, _ => { - return Err("$separator: Must be \"space\", \"comma\", or \"auto\".".into()) + return Err(( + "$separator: Must be \"space\", \"comma\", or \"auto\".", + args.span(), + ) + .into()) } }, - v => return Err(format!("$separator: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$separator: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; list.push(val); @@ -177,10 +207,23 @@ pub(crate) fn register(f: &mut HashMap) { "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, _ => { - return Err("$separator: Must be \"space\", \"comma\", or \"auto\".".into()) + return Err(( + "$separator: Must be \"space\", \"comma\", or \"auto\".", + args.span(), + ) + .into()) } }, - v => return Err(format!("$separator: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$separator: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let brackets = match arg!( @@ -195,7 +238,7 @@ pub(crate) fn register(f: &mut HashMap) { _ => Brackets::Bracketed, }, v => { - if v.is_true()? { + if v.is_true(args.span())? { Brackets::Bracketed } else { Brackets::None @@ -240,7 +283,7 @@ pub(crate) fn register(f: &mut HashMap) { // Potential input to fuzz: index(1px 1in 1cm, 96px + 1rem) let index = match list .into_iter() - .position(|v| v.equals(value.clone()).unwrap()) + .position(|v| v.equals(value.clone(), args.span()).unwrap()) { Some(v) => Number::from(v + 1), None => return Ok(Value::Null), @@ -254,7 +297,7 @@ pub(crate) fn register(f: &mut HashMap) { let lists = args .get_variadic(scope, super_selector)? .into_iter() - .map(|x| match x { + .map(|x| match x.node { Value::List(v, ..) => v, Value::Map(m) => m.entries(), v => vec![v], diff --git a/src/builtin/macros.rs b/src/builtin/macros.rs index f87d178..5c16d33 100644 --- a/src/builtin/macros.rs +++ b/src/builtin/macros.rs @@ -1,18 +1,20 @@ macro_rules! arg { ($args:ident, $scope:ident, $super_selector:ident, $idx:literal, $name:literal) => { match $args.get_positional($idx, $scope, $super_selector) { - Some(v) => v?.eval()?, + Some(v) => v?.node.eval($args.span())?.node, None => match $args.get_named($name.to_owned(), $scope, $super_selector) { - Some(v) => v?.eval()?, - None => return Err(concat!("Missing argument $", $name, ".").into()), + Some(v) => v?.node.eval($args.span())?.node, + None => { + return Err((concat!("Missing argument $", $name, "."), $args.span()).into()) + } }, }; }; ($args:ident, $scope:ident, $super_selector:ident, $idx:literal, $name:literal=$default:expr) => { match $args.get_positional($idx, $scope, $super_selector) { - Some(v) => v?.eval()?, + Some(v) => v?.node.eval($args.span())?.node, None => match $args.get_named($name.to_owned(), $scope, $super_selector) { - Some(v) => v?.eval()?, + Some(v) => v?.node.eval($args.span())?.node, None => $default, }, }; @@ -22,13 +24,13 @@ macro_rules! arg { macro_rules! named_arg { ($args:ident, $scope:ident, $super_selector:ident, $name:literal) => { match $args.get_named($name.to_owned(), $scope, $super_selector) { - Some(v) => v?.eval()?, - None => return Err(concat!("Missing argument $", $name, ".").into()), + Some(v) => v?.node.eval($args.span())?.node, + None => return Err((concat!("Missing argument $", $name, "."), $args.span()).into()), }; }; ($args:ident, $scope:ident, $super_selector:ident, $name:literal=$default:expr) => { match $args.get_named($name.to_owned(), $scope, $super_selector) { - Some(v) => v?.eval()?, + Some(v) => v?.node.eval($args.span())?.node, None => $default, }; }; @@ -48,19 +50,22 @@ macro_rules! max_args { } else { err.push_str("were passed.") } - return Err(err.into()); + return Err((err, $args.span()).into()); } }; } macro_rules! bound { - ($name:literal, $arg:ident, $unit:ident, $low:literal, $high:literal) => { + ($args:ident, $name:literal, $arg:ident, $unit:ident, $low:literal, $high:literal) => { if $arg > Number::from($high) || $arg < Number::from($low) { - return Err(format!( - "${}: Expected {}{} to be within {}{} and {}{}.", - $name, $arg, $unit, $low, $unit, $high, $unit, + return Err(( + format!( + "${}: Expected {}{} to be within {}{} and {}{}.", + $name, $arg, $unit, $low, $unit, $high, $unit, + ), + $args.span(), ) - .into()); + .into()); } else { $arg } @@ -68,24 +73,30 @@ macro_rules! bound { // HACK: we accept `$low` as an ident here in order to work around // a bug in the nightly compiler. // https://github.com/rust-lang/rust/issues/70050 - ($name:literal, $arg:ident, $unit:ident, $low:ident, $high:literal) => { + ($args:ident, $name:literal, $arg:ident, $unit:ident, $low:ident, $high:literal) => { if $arg > Number::from($high) || $arg < Number::from($low) { - return Err(format!( - "${}: Expected {}{} to be within {}{} and {}{}.", - $name, $arg, $unit, $low, $unit, $high, $unit, + return Err(( + format!( + "${}: Expected {}{} to be within {}{} and {}{}.", + $name, $arg, $unit, $low, $unit, $high, $unit, + ), + $args.span(), ) - .into()); + .into()); } else { $arg } }; - ($name:literal, $arg:ident, $unit:path, $low:literal, $high:literal) => { + ($args:ident, $name:literal, $arg:ident, $unit:path, $low:literal, $high:literal) => { if $arg > Number::from($high) || $arg < Number::from($low) { - return Err(format!( - "${}: Expected {}{} to be within {}{} and {}{}.", - $name, $arg, $unit, $low, $unit, $high, $unit, + return Err(( + format!( + "${}: Expected {}{} to be within {}{} and {}{}.", + $name, $arg, $unit, $low, $unit, $high, $unit, + ), + $args.span(), ) - .into()); + .into()); } else { $arg } @@ -93,13 +104,16 @@ macro_rules! bound { // HACK: we accept `$low` as an ident here in order to work around // a bug in the nightly compiler. // https://github.com/rust-lang/rust/issues/70050 - ($name:literal, $arg:ident, $unit:path, $low:ident, $high:literal) => { + ($args:ident, $name:literal, $arg:ident, $unit:path, $low:ident, $high:literal) => { if $arg > Number::from($high) || $arg < Number::from($low) { - return Err(format!( - "${}: Expected {}{} to be within {}{} and {}{}.", - $name, $arg, $unit, $low, $unit, $high, $unit, + return Err(( + format!( + "${}: Expected {}{} to be within {}{} and {}{}.", + $name, $arg, $unit, $low, $unit, $high, $unit, + ), + $args.span(), ) - .into()); + .into()); } else { $arg } diff --git a/src/builtin/map.rs b/src/builtin/map.rs index 35070ae..9052397 100644 --- a/src/builtin/map.rs +++ b/src/builtin/map.rs @@ -13,9 +13,15 @@ pub(crate) fn register(f: &mut HashMap) { let map = match arg!(args, scope, super_selector, 0, "map") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; - Ok(map.get(&key)?.unwrap_or(Value::Null)) + Ok(map.get(&key, args.span())?.unwrap_or(Value::Null)) }), ); f.insert( @@ -26,9 +32,15 @@ pub(crate) fn register(f: &mut HashMap) { let map = match arg!(args, scope, super_selector, 0, "map") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; - Ok(Value::bool(map.get(&key)?.is_some())) + Ok(Value::bool(map.get(&key, args.span())?.is_some())) }), ); f.insert( @@ -38,7 +50,13 @@ pub(crate) fn register(f: &mut HashMap) { let map = match arg!(args, scope, super_selector, 0, "map") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::List( map.keys(), @@ -54,7 +72,13 @@ pub(crate) fn register(f: &mut HashMap) { let map = match arg!(args, scope, super_selector, 0, "map") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; Ok(Value::List( map.values(), @@ -70,12 +94,24 @@ pub(crate) fn register(f: &mut HashMap) { let mut map1 = match arg!(args, scope, super_selector, 0, "map1") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map1: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map1: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let map2 = match arg!(args, scope, super_selector, 1, "map2") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map2: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map2: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; map1.merge(map2); Ok(Value::Map(map1)) @@ -87,7 +123,13 @@ pub(crate) fn register(f: &mut HashMap) { let mut map = match arg!(args, scope, super_selector, 0, "map") { Value::Map(m) => m, Value::List(v, ..) if v.is_empty() => SassMap::new(), - v => return Err(format!("$map: {} is not a map.", v).into()), + v => { + return Err(( + format!("$map: {} is not a map.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; let keys = args.get_variadic(scope, super_selector)?; for key in keys { diff --git a/src/builtin/math.rs b/src/builtin/math.rs index 657946b..52ee1f2 100644 --- a/src/builtin/math.rs +++ b/src/builtin/math.rs @@ -17,9 +17,25 @@ pub(crate) fn register(f: &mut HashMap) { let num = match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(n, Unit::None) => n * Number::from(100), v @ Value::Dimension(..) => { - return Err(format!("$number: Expected {} to have no units.", v).into()) + return Err(( + format!( + "$number: Expected {} to have no units.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v => { + return Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) } - v => return Err(format!("$number: {} is not a number.", v).into()), }; Ok(Value::Dimension(num, Unit::Percent)) }), @@ -30,7 +46,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(n, u) => Ok(Value::Dimension(n.round(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -40,7 +63,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(n, u) => Ok(Value::Dimension(n.ceil(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -50,7 +80,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(n, u) => Ok(Value::Dimension(n.floor(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -60,7 +97,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(n, u) => Ok(Value::Dimension(n.abs(), u)), - v => Err(format!("$number: {} is not a number.", v).into()), + v => Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -70,11 +114,29 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let unit1 = match arg!(args, scope, super_selector, 0, "number1") { Value::Dimension(_, u) => u, - v => return Err(format!("$number1: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$number1: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let unit2 = match arg!(args, scope, super_selector, 1, "number2") { Value::Dimension(_, u) => u, - v => return Err(format!("$number2: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$number2: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::bool(unit1.comparable(&unit2))) @@ -95,7 +157,13 @@ pub(crate) fn register(f: &mut HashMap) { Unit::None, )); } - v => return Err(format!("$limit: {} is not a number.", v).into()), + v => { + return Err(( + format!("$limit: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; if limit.is_one() { @@ -103,19 +171,25 @@ pub(crate) fn register(f: &mut HashMap) { } if limit.is_decimal() { - return Err(format!("$limit: {} is not an int.", limit).into()); + return Err((format!("$limit: {} is not an int.", limit), args.span()).into()); } if limit.is_zero() || limit.is_negative() { - return Err(format!("$limit: Must be greater than 0, was {}.", limit).into()); + return Err(( + format!("$limit: Must be greater than 0, was {}.", limit), + args.span(), + ) + .into()); } let limit = match limit.to_integer().to_u32() { Some(n) => n, None => { - return Err( - format!("max must be in range 0 < max ≤ 2^32, was {}", limit).into(), + return Err(( + format!("max must be in range 0 < max ≤ 2^32, was {}", limit), + args.span(), ) + .into()) } }; diff --git a/src/builtin/meta.rs b/src/builtin/meta.rs index cc7557f..f97d008 100644 --- a/src/builtin/meta.rs +++ b/src/builtin/meta.rs @@ -1,5 +1,7 @@ use std::collections::HashMap; +use codemap::Spanned; + use super::{Builtin, GLOBAL_FUNCTIONS}; use crate::common::QuoteKind; use crate::scope::global_var_exists; @@ -11,7 +13,7 @@ pub(crate) fn register(f: &mut HashMap) { "if".to_owned(), Builtin::new(|mut args, scope, super_selector| { max_args!(args, 3); - if arg!(args, scope, super_selector, 0, "condition").is_true()? { + if arg!(args, scope, super_selector, 0, "condition").is_true(args.span())? { Ok(arg!(args, scope, super_selector, 1, "if-true")) } else { Ok(arg!(args, scope, super_selector, 2, "if-false")) @@ -41,7 +43,14 @@ pub(crate) fn register(f: &mut HashMap) { "custom-property" => Ok(Value::False), _ => Ok(Value::False), }, - v => Err(format!("$feature: {} is not a string.", v).into()), + v => Err(( + format!( + "$feature: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -51,7 +60,16 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); let unit = match arg!(args, scope, super_selector, 0, "number") { Value::Dimension(_, u) => u.to_string(), - v => return Err(format!("$number: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$number: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(Value::Ident(unit, QuoteKind::Double)) }), @@ -61,7 +79,10 @@ pub(crate) fn register(f: &mut HashMap) { Builtin::new(|mut args, scope, super_selector| { max_args!(args, 1); let value = arg!(args, scope, super_selector, 0, "value"); - Ok(Value::Ident(value.kind()?.to_owned(), QuoteKind::None)) + Ok(Value::Ident( + value.kind(args.span())?.to_owned(), + QuoteKind::None, + )) }), ); f.insert( @@ -80,7 +101,7 @@ pub(crate) fn register(f: &mut HashMap) { Builtin::new(|mut args, scope, super_selector| { max_args!(args, 1); Ok(Value::Ident( - arg!(args, scope, super_selector, 0, "value").inspect(), + arg!(args, scope, super_selector, 0, "value").inspect(args.span())?, QuoteKind::None, )) }), @@ -91,7 +112,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "name") { Value::Ident(s, _) => Ok(Value::bool(scope.var_exists(&s))), - v => Err(format!("$name: {} is not a string.", v).into()), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -101,7 +126,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "name") { Value::Ident(s, _) => Ok(Value::bool(global_var_exists(&s))), - v => Err(format!("$name: {} is not a string.", v).into()), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -111,7 +140,11 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); match arg!(args, scope, super_selector, 0, "name") { Value::Ident(s, _) => Ok(Value::bool(scope.mixin_exists(&s))), - v => Err(format!("$name: {} is not a string.", v).into()), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -123,7 +156,11 @@ pub(crate) fn register(f: &mut HashMap) { Value::Ident(s, _) => Ok(Value::bool( scope.fn_exists(&s) || GLOBAL_FUNCTIONS.contains_key(&s), )), - v => Err(format!("$name: {} is not a string.", v).into()), + v => Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()), } }), ); @@ -133,24 +170,49 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 3); let name = match arg!(args, scope, super_selector, 0, "name") { Value::Ident(s, _) => s, - v => return Err(format!("$name: {} is not a string.", v).into()), + v => { + return Err(( + format!("$name: {} is not a string.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) + } }; - let css = arg!(args, scope, super_selector, 1, "css" = Value::False).is_true()?; + let css = + arg!(args, scope, super_selector, 1, "css" = Value::False).is_true(args.span())?; let module = match arg!(args, scope, super_selector, 2, "module" = Value::Null) { Value::Ident(s, ..) => Some(s), Value::Null => None, - v => return Err(format!("$module: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$module: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; if module.is_some() && css { - return Err("$css and $module may not both be passed at once.".into()); + return Err(( + "$css and $module may not both be passed at once.", + args.span(), + ) + .into()); } - let func = match scope.get_fn(&name) { + let func = match scope.get_fn(Spanned { + node: name.clone(), + span: args.span(), + }) { Ok(f) => SassFunction::UserDefined(Box::new(f), name), Err(..) => match GLOBAL_FUNCTIONS.get(&name) { Some(f) => SassFunction::Builtin(f.clone(), name), - None => return Err(format!("Function not found: {}", name).into()), + None => { + return Err((format!("Function not found: {}", name), args.span()).into()) + } }, }; @@ -162,7 +224,16 @@ pub(crate) fn register(f: &mut HashMap) { Builtin::new(|mut args, scope, super_selector| { let func = match arg!(args, scope, super_selector, 0, "function") { Value::Function(f) => f, - v => return Err(format!("$function: {} is not a function reference.", v).into()), + v => { + return Err(( + format!( + "$function: {} is not a function reference.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; func.call(args.decrement(), scope, super_selector) }), diff --git a/src/builtin/string.rs b/src/builtin/string.rs index 5089cdf..2d42b29 100644 --- a/src/builtin/string.rs +++ b/src/builtin/string.rs @@ -18,7 +18,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "string") { Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_uppercase(), q)), - v => Err(format!("$string: {} is not a string.", v).into()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -28,7 +35,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "string") { Value::Ident(i, q) => Ok(Value::Ident(i.to_ascii_lowercase(), q)), - v => Err(format!("$string: {} is not a string.", v).into()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -41,7 +55,14 @@ pub(crate) fn register(f: &mut HashMap) { Number::from(i.chars().count()), Unit::None, )), - v => Err(format!("$string: {} is not a string.", v).into()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -51,7 +72,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "string") { Value::Ident(i, _) => Ok(Value::Ident(i, QuoteKind::Double)), - v => Err(format!("$string: {} is not a string.", v).into()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -61,7 +89,14 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 1); match arg!(args, scope, super_selector, 0, "string") { i @ Value::Ident(..) => Ok(i.unquote()), - v => Err(format!("$string: {} is not a string.", v).into()), + v => Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()), } }), ); @@ -71,12 +106,21 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 3); let (string, quotes) = match arg!(args, scope, super_selector, 0, "string") { Value::Ident(s, q) => (s, q), - v => return Err(format!("$string: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let str_len = string.chars().count(); let start = match arg!(args, scope, super_selector, 1, "start-at") { Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err(format!("{} is not an int.", n).into()) + return Err((format!("{} is not an int.", n), args.span()).into()) } Value::Dimension(n, Unit::None) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) @@ -87,13 +131,29 @@ pub(crate) fn register(f: &mut HashMap) { .to_usize() .unwrap(), v @ Value::Dimension(..) => { - return Err(format!("$start: Expected {} to have no units.", v).into()) + return Err(( + format!( + "$start: Expected {} to have no units.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v => { + return Err(( + format!( + "$start-at: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) } - v => return Err(format!("$start-at: {} is not a number.", v).into()), }; let mut end = match arg!(args, scope, super_selector, 2, "end-at" = Value::Null) { Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err(format!("{} is not an int.", n).into()) + return Err((format!("{} is not an int.", n), args.span()).into()) } Value::Dimension(n, Unit::None) if n.is_positive() => { n.to_integer().to_usize().unwrap_or(str_len + 1) @@ -104,10 +164,26 @@ pub(crate) fn register(f: &mut HashMap) { .to_usize() .unwrap_or(str_len + 1), v @ Value::Dimension(..) => { - return Err(format!("$end: Expected {} to have no units.", v).into()) + return Err(( + format!( + "$end: Expected {} to have no units.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) } Value::Null => str_len, - v => return Err(format!("$end-at: {} is not a number.", v).into()), + v => { + return Err(( + format!( + "$end-at: {} is not a number.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; if end > str_len { @@ -134,12 +210,30 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 2); let s1 = match arg!(args, scope, super_selector, 0, "string") { Value::Ident(i, _) => i, - v => return Err(format!("$string: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let substr = match arg!(args, scope, super_selector, 1, "substring") { Value::Ident(i, _) => i, - v => return Err(format!("$substring: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$substring: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; Ok(match s1.find(&substr) { @@ -154,23 +248,54 @@ pub(crate) fn register(f: &mut HashMap) { max_args!(args, 3); let (s1, quotes) = match arg!(args, scope, super_selector, 0, "string") { Value::Ident(i, q) => (i, q.normalize()), - v => return Err(format!("$string: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$string: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let substr = match arg!(args, scope, super_selector, 1, "insert") { Value::Ident(i, _) => i, - v => return Err(format!("$insert: {} is not a string.", v).into()), + v => { + return Err(( + format!( + "$insert: {} is not a string.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } }; let index = match arg!(args, scope, super_selector, 2, "index") { Value::Dimension(n, Unit::None) if n.is_decimal() => { - return Err(format!("$index: {} is not an int.", n).into()) + return Err((format!("$index: {} is not an int.", n), args.span()).into()) } Value::Dimension(n, Unit::None) => n, v @ Value::Dimension(..) => { - return Err(format!("$index: Expected {} to have no units.", v).into()) + return Err(( + format!( + "$index: Expected {} to have no units.", + v.to_css_string(args.span())? + ), + args.span(), + ) + .into()) + } + v => { + return Err(( + format!("$index: {} is not a number.", v.to_css_string(args.span())?), + args.span(), + ) + .into()) } - v => return Err(format!("$index: {} is not a number.", v).into()), }; if s1.is_empty() { diff --git a/src/common.rs b/src/common.rs index a8de3ef..bd3f970 100644 --- a/src/common.rs +++ b/src/common.rs @@ -61,45 +61,6 @@ impl Op { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub struct Pos { - line: u32, - column: u32, -} - -impl Pos { - pub const fn new() -> Self { - Pos { line: 1, column: 1 } - } - - pub const fn line(self) -> u32 { - self.line - } - - pub const fn column(self) -> u32 { - self.column - } - - pub fn newline(&mut self) { - self.line += 1; - self.column = 0; - } - - pub fn next_char(&mut self) { - self.column += 1; - } - - pub fn chars(&mut self, num: u32) { - self.column += num; - } -} - -impl Display for Pos { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "line:{} col:{}", self.line, self.column) - } -} - #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum QuoteKind { Single, diff --git a/src/error.rs b/src/error.rs index ac7794e..165d7fa 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,36 +3,81 @@ use std::fmt::{self, Display}; use std::io; use std::string::FromUtf8Error; -use crate::common::Pos; +use codemap::{Span, SpanLoc}; pub type SassResult = Result; -#[derive(Debug, Clone, Eq, PartialEq)] +#[derive(Debug)] pub struct SassError { - message: String, - pos: Pos, + kind: SassErrorKind, } impl SassError { - pub fn new>(message: S, pos: Pos) -> Self { + pub(crate) fn raw(self) -> (String, Span) { + match self.kind { + SassErrorKind::Raw(string, span) => (string, span), + _ => todo!(), + } + } + + pub(crate) fn from_loc(message: String, loc: SpanLoc) -> Self { SassError { - message: message.into(), - pos, + kind: SassErrorKind::ParseError { message, loc }, } } } +#[derive(Debug)] +enum SassErrorKind { + /// A raw error with no additional metadata + /// It contains only a `String` message and + /// a span + Raw(String, Span), + ParseError { + message: String, + loc: SpanLoc, + }, + IoError(io::Error), + FmtError(fmt::Error), + FromUtf8Error(String), +} + impl Display for SassError { + // TODO: trim whitespace + // TODO: color errors + // TODO: integrate with codemap-diagnostics fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Error: {}", self.message) + let (message, loc) = match &self.kind { + SassErrorKind::ParseError { message, loc } => (message, loc), + _ => todo!(), + }; + let line = loc.begin.line + 1; + let col = loc.begin.column + 1; + writeln!(f, "Error: {}", message)?; + let padding = vec![' '; format!("{}", line).len() + 1] + .iter() + .collect::(); + writeln!(f, "{}|", padding)?; + writeln!(f, "{} | {}", line, loc.file.source_line(loc.begin.line))?; + writeln!( + f, + "{}| {}{}", + padding, + vec![' '; loc.begin.column].iter().collect::(), + vec!['^'; loc.end.column - loc.begin.column] + .iter() + .collect::() + )?; + writeln!(f, "{}|", padding)?; + writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?; + Ok(()) } } impl From for SassError { fn from(error: io::Error) -> Self { SassError { - pos: Pos::new(), - message: format!("{}", error), + kind: SassErrorKind::IoError(error), } } } @@ -40,8 +85,7 @@ impl From for SassError { impl From for SassError { fn from(error: std::fmt::Error) -> Self { SassError { - pos: Pos::new(), - message: format!("{}", error), + kind: SassErrorKind::FmtError(error), } } } @@ -49,35 +93,28 @@ impl From for SassError { impl From for SassError { fn from(error: FromUtf8Error) -> Self { SassError { - pos: Pos::new(), - message: format!("Invalid UTF-8 character \"\\x{:X?}\"", error.as_bytes()[0]), + kind: SassErrorKind::FromUtf8Error(format!( + "Invalid UTF-8 character \"\\x{:X?}\"", + error.as_bytes()[0] + )), } } } -impl From for String { +impl From<(&str, Span)> for SassError { #[inline] - fn from(error: SassError) -> String { - error.message - } -} - -impl From<&str> for SassError { - #[inline] - fn from(error: &str) -> SassError { + fn from(error: (&str, Span)) -> SassError { SassError { - pos: Pos::new(), - message: error.to_string(), + kind: SassErrorKind::Raw(error.0.to_owned(), error.1), } } } -impl From for SassError { +impl From<(String, Span)> for SassError { #[inline] - fn from(error: String) -> SassError { + fn from(error: (String, Span)) -> SassError { SassError { - pos: Pos::new(), - message: error, + kind: SassErrorKind::Raw(error.0, error.1), } } } diff --git a/src/format.rs b/src/format.rs index 79956cc..2165f13 100644 --- a/src/format.rs +++ b/src/format.rs @@ -32,7 +32,7 @@ impl PrettyPrinter { self.scope -= 1; } Stmt::Style(s) => { - writeln!(self.buf, "{}{}", padding, s)?; + writeln!(self.buf, "{}{}", padding, s.to_string()?)?; } Stmt::AtRule(r) => match r { AtRule::Unknown(..) => todo!("Display @rules properly"), @@ -63,7 +63,7 @@ mod test_scss { fn $func() { assert_eq!( String::from($input), - StyleSheet::new($input) + StyleSheet::new($input.to_string()) .expect(concat!("failed to parse on ", $input)) .to_string() ); @@ -74,7 +74,7 @@ mod test_scss { fn $func() { assert_eq!( String::from($output), - StyleSheet::new($input) + StyleSheet::new($input.to_string()) .expect(concat!("failed to parse on ", $input)) .to_string() ); diff --git a/src/imports.rs b/src/imports.rs index f625617..fc55f73 100644 --- a/src/imports.rs +++ b/src/imports.rs @@ -1,12 +1,14 @@ use std::ffi::OsStr; use std::path::Path; +use codemap::Spanned; + use crate::error::SassResult; use crate::scope::Scope; use crate::{Stmt, StyleSheet}; -pub(crate) fn import>(path: P) -> SassResult<(Vec, Scope)> { - let mut rules: Vec = Vec::new(); +pub(crate) fn import>(path: P) -> SassResult<(Vec>, Scope)> { + let mut rules = Vec::new(); let mut scope = Scope::new(); let path_buf = path.as_ref().to_path_buf(); let name = path_buf.file_name().expect("todo! path ended in `..`"); diff --git a/src/lexer.rs b/src/lexer.rs index 3ec5def..bfd3a3c 100644 --- a/src/lexer.rs +++ b/src/lexer.rs @@ -1,7 +1,9 @@ use std::iter::Peekable; use std::str::Chars; +use std::sync::Arc; + +use codemap::File; -use crate::common::Pos; use crate::Token; pub const FORM_FEED: char = '\x0C'; @@ -9,41 +11,42 @@ pub const FORM_FEED: char = '\x0C'; #[derive(Debug, Clone)] pub(crate) struct Lexer<'a> { buf: Peekable>, - pos: Pos, + pos: usize, + file: &'a Arc, } impl<'a> Iterator for Lexer<'a> { type Item = Token; fn next(&mut self) -> Option { let kind = match self.buf.next()? { - '\n' | FORM_FEED => { - self.pos.newline(); - '\n' - } + '\n' | FORM_FEED => '\n', '\r' => { if self.buf.peek() == Some(&'\n') { + self.pos += 1; self.buf.next(); '\n' } else { '\n' } } - '\0' => return None, c => c, }; - self.pos.next_char(); - Some(Token { - kind, - pos: self.pos, - }) + let len = kind.len_utf8(); + let pos = self + .file + .span + .subspan(self.pos as u64, (self.pos + len) as u64); + self.pos += len; + Some(Token { kind, pos }) } } impl<'a> Lexer<'a> { - pub fn new(buf: &'a str) -> Lexer<'a> { + pub fn new(file: &'a Arc) -> Lexer<'a> { Lexer { - buf: buf.chars().peekable(), - pos: Pos::new(), + buf: file.source().clone().chars().peekable(), + pos: 0, + file, } } } diff --git a/src/lib.rs b/src/lib.rs index cee1028..ef9bf36 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,8 +84,9 @@ use std::io::Write; use std::iter::{Iterator, Peekable}; use std::path::Path; +use codemap::{CodeMap, Span, Spanned}; + use crate::atrule::{eat_include, AtRule, AtRuleKind, Function, Mixin}; -use crate::common::Pos; pub use crate::error::{SassError, SassResult}; use crate::format::PrettyPrinter; use crate::imports::import; @@ -121,7 +122,7 @@ mod value; /// Represents a parsed SASS stylesheet with nesting #[derive(Debug, Clone)] -pub struct StyleSheet(Vec); +pub struct StyleSheet(Vec>); #[derive(Clone, Debug)] pub(crate) enum Stmt { @@ -135,6 +136,12 @@ pub(crate) enum Stmt { AtRule(AtRule), } +impl Stmt { + fn span(self, span: Span) -> Spanned { + Spanned { node: self, span } + } +} + /// Represents a single rule set. Rule sets can contain other rule sets /// /// ```scss @@ -148,7 +155,7 @@ pub(crate) enum Stmt { #[derive(Clone, Debug)] pub(crate) struct RuleSet { selector: Selector, - rules: Vec, + rules: Vec>, // potential optimization: we don't *need* to own the selector super_selector: Selector, } @@ -174,19 +181,13 @@ enum Expr { /// A full selector `a > h1` Selector(Selector), /// A variable declaration `$var: 1px` - VariableDecl(String, Box), + VariableDecl(String, Box>), /// A mixin declaration `@mixin foo {}` MixinDecl(String, Box), FunctionDecl(String, Box), - /// An include statement `@include foo;` - Include(Vec), /// A multiline comment: `/* foobar */` MultilineComment(String), - Debug(Pos, String), - Warn(Pos, String), AtRule(AtRule), - // /// Function call: `calc(10vw - 1px)` - // FuncCall(String, Vec), } /// Print the internal representation of a parsed stylesheet @@ -203,51 +204,70 @@ impl Display for StyleSheet { } } +fn raw_to_parse_error(map: &CodeMap, err: SassError) -> SassError { + let (message, span) = err.raw(); + SassError::from_loc(message, map.look_up_span(span)) +} + impl StyleSheet { #[inline] - pub fn new(input: &str) -> SassResult { + pub fn new(input: String) -> SassResult { + let mut map = CodeMap::new(); + let file = map.add_file("stdin".into(), input); Ok(StyleSheet( - StyleSheetParser { - global_scope: Scope::new(), - lexer: Lexer::new(input).peekable(), - rules: Vec::new(), - scope: 0, - file: String::from("stdin"), + match (StyleSheetParser { + lexer: Lexer::new(&file).peekable(), + nesting: 0, + map: &map, + } + .parse_toplevel()) + { + Ok(v) => v, + Err(e) => return Err(raw_to_parse_error(&map, e)), } - .parse_toplevel()? .0, )) } #[inline] - pub fn from_path + Into>(p: P) -> SassResult { + pub fn from_path + Into + Clone>(p: P) -> SassResult { + let mut map = CodeMap::new(); + let file = map.add_file(p.clone().into(), String::from_utf8(fs::read(p.as_ref())?)?); Ok(StyleSheet( - StyleSheetParser { - global_scope: Scope::new(), - lexer: Lexer::new(&String::from_utf8(fs::read(p.as_ref())?)?).peekable(), - rules: Vec::new(), - scope: 0, - file: p.into(), + match (StyleSheetParser { + lexer: Lexer::new(&file).peekable(), + nesting: 0, + map: &map, + } + .parse_toplevel()) + { + Ok(v) => v, + Err(e) => return Err(raw_to_parse_error(&map, e)), } - .parse_toplevel()? .0, )) } - pub(crate) fn export_from_path + Into>( + pub(crate) fn export_from_path + Into + Clone>( p: P, - ) -> SassResult<(Vec, Scope)> { - Ok(StyleSheetParser { - global_scope: Scope::new(), - lexer: Lexer::new(&String::from_utf8(fs::read(p.as_ref())?)?).peekable(), - rules: Vec::new(), - scope: 0, - file: p.into(), - } - .parse_toplevel()?) + ) -> SassResult<(Vec>, Scope)> { + let mut map = CodeMap::new(); + let file = map.add_file(p.clone().into(), String::from_utf8(fs::read(p.as_ref())?)?); + Ok( + match (StyleSheetParser { + lexer: Lexer::new(&file).peekable(), + nesting: 0, + map: &map, + } + .parse_toplevel()) + { + Ok(v) => v, + Err(e) => return Err(raw_to_parse_error(&map, e)), + }, + ) } - pub(crate) fn from_stmts(s: Vec) -> StyleSheet { + pub(crate) fn from_stmts(s: Vec>) -> StyleSheet { StyleSheet(s) } @@ -268,18 +288,15 @@ impl StyleSheet { } } -#[derive(Debug, Clone)] struct StyleSheetParser<'a> { - global_scope: Scope, lexer: Peekable>, - rules: Vec, - scope: u32, - file: String, + nesting: u32, + map: &'a CodeMap, } impl<'a> StyleSheetParser<'a> { - fn parse_toplevel(mut self) -> SassResult<(Vec, Scope)> { - let mut rules: Vec = Vec::new(); + fn parse_toplevel(mut self) -> SassResult<(Vec>, Scope)> { + let mut rules: Vec> = Vec::new(); while let Some(Token { kind, .. }) = self.lexer.peek() { match kind { 'a'..='z' | 'A'..='Z' | '_' | '-' | '0'..='9' @@ -293,20 +310,18 @@ impl<'a> StyleSheetParser<'a> { self.lexer.next(); let name = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?; devour_whitespace(&mut self.lexer); - if self + let Token { kind, pos } = self .lexer .next() - .unwrap() - .kind - != ':' - { - return Err("expected \":\".".into()); + .unwrap(); + if kind != ':' { + return Err(("expected \":\".", pos).into()); } let VariableDecl { val, default, .. } = eat_variable_value(&mut self.lexer, &Scope::new(), &Selector::new())?; GLOBAL_SCOPE.with(|s| { - if !default || s.borrow().get_var(&name).is_err() { - match s.borrow_mut().insert_var(&name, val) { + if !default || s.borrow().get_var(name.clone()).is_err() { + match s.borrow_mut().insert_var(&name.node, val) { Ok(..) => Ok(()), Err(e) => Err(e), } @@ -319,7 +334,8 @@ impl<'a> StyleSheetParser<'a> { self.lexer.next(); if '*' == self.lexer.peek().unwrap().kind { self.lexer.next(); - rules.push(Stmt::MultilineComment(eat_comment(&mut self.lexer, &Scope::new(), &Selector::new())?)); + let comment = eat_comment(&mut self.lexer, &Scope::new(), &Selector::new())?; + rules.push(Spanned { node: Stmt::MultilineComment(comment.node), span: comment.span }); } else if '/' == self.lexer.peek().unwrap().kind { read_until_newline(&mut self.lexer); devour_whitespace(&mut self.lexer); @@ -329,9 +345,9 @@ impl<'a> StyleSheetParser<'a> { } '@' => { self.lexer.next(); - let at_rule_kind = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?; + let Spanned { node: at_rule_kind, span } = eat_ident(&mut self.lexer, &Scope::new(), &Selector::new())?; if at_rule_kind.is_empty() { - return Err("Expected identifier.".into()); + return Err(("Expected identifier.", span).into()); } match AtRuleKind::from(at_rule_kind.as_str()) { AtRuleKind::Include => rules.extend(eat_include( @@ -349,7 +365,7 @@ impl<'a> StyleSheetParser<'a> { .kind { q @ '"' | q @ '\'' => { - file_name.push_str(&parse_quoted_string(&mut self.lexer, &Scope::new(), q, &Selector::new())?.unquote().to_string()); + file_name.push_str(&parse_quoted_string(&mut self.lexer, &Scope::new(), q, &Selector::new())?.node.unquote().to_css_string(span)?); } _ => todo!("expected ' or \" after @import"), } @@ -364,37 +380,39 @@ impl<'a> StyleSheetParser<'a> { }); } v => { - match AtRule::from_tokens(&v, Pos::new(), &mut self.lexer, &mut Scope::new(), &Selector::new())? { - AtRule::Mixin(name, mixin) => { - insert_global_mixin(&name, *mixin); - } - AtRule::Function(name, func) => { - insert_global_fn(&name, *func); - } - AtRule::Charset => continue, - AtRule::Warn(pos, message) => self.warn(pos, &message), - AtRule::Debug(pos, message) => self.debug(pos, &message), - AtRule::Return(_) => { - return Err("This at-rule is not allowed here.".into()) - } - AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => rules.extend(s), - AtRule::Content => return Err("@content is only allowed within mixin declarations.".into()), - AtRule::If(i) => { - rules.extend(i.eval(&mut Scope::new(), &Selector::new())?); - } - AtRule::AtRoot(root_rules) => rules.extend(root_rules), - u @ AtRule::Unknown(..) => rules.push(Stmt::AtRule(u)), + let pos = self.lexer.next().unwrap().pos(); + let rule = AtRule::from_tokens(&v, pos, &mut self.lexer, &mut Scope::new(), &Selector::new())?; + match rule.node { + AtRule::Mixin(name, mixin) => { + insert_global_mixin(&name, *mixin); } + AtRule::Function(name, func) => { + insert_global_fn(&name, *func); + } + AtRule::Charset => continue, + AtRule::Warn(message) => self.warn(rule.span, &message), + AtRule::Debug(message) => self.debug(rule.span, &message), + AtRule::Return(_) => { + return Err(("This at-rule is not allowed here.", rule.span).into()) + } + AtRule::Include(s) | AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => rules.extend(s), + AtRule::Content => return Err(("@content is only allowed within mixin declarations.", rule.span).into()), + AtRule::If(i) => { + rules.extend(i.eval(&mut Scope::new(), &Selector::new())?); + } + AtRule::AtRoot(root_rules) => rules.extend(root_rules), + u @ AtRule::Unknown(..) => rules.push(Spanned { node: Stmt::AtRule(u), span: rule.span }), } + } } }, '&' => { return Err( - "Base-level rules cannot contain the parent-selector-referencing character '&'.".into(), + ("Base-level rules cannot contain the parent-selector-referencing character '&'.", self.lexer.next().unwrap().pos()).into(), ) } c if c.is_control() => { - return Err("expected selector.".into()); + return Err(("expected selector.", self.lexer.next().unwrap().pos()).into()); } _ => match dbg!(self.lexer.next()) { Some(..) => todo!("unexpected toplevel token"), @@ -405,22 +423,48 @@ impl<'a> StyleSheetParser<'a> { Ok((rules, GLOBAL_SCOPE.with(|s| s.borrow().clone()))) } - fn eat_rules(&mut self, super_selector: &Selector, scope: &mut Scope) -> SassResult> { + fn eat_rules( + &mut self, + super_selector: &Selector, + scope: &mut Scope, + ) -> SassResult>> { let mut stmts = Vec::new(); while let Some(expr) = eat_expr(&mut self.lexer, scope, super_selector)? { - match expr { - Expr::Style(s) => stmts.push(Stmt::Style(s)), + let span = expr.span; + match expr.node { + Expr::Style(s) => stmts.push(Spanned { + node: Stmt::Style(s), + span, + }), Expr::AtRule(a) => match a { - AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => stmts.extend(s), + AtRule::Include(s) | AtRule::While(s) | AtRule::Each(s) | AtRule::For(s) => { + stmts.extend(s) + } AtRule::If(i) => stmts.extend(i.eval(scope, super_selector)?), AtRule::Content => { - return Err("@content is only allowed within mixin declarations.".into()) + return Err(( + "@content is only allowed within mixin declarations.", + expr.span, + ) + .into()) + } + AtRule::Return(..) => { + return Err(("This at-rule is not allowed here.", expr.span).into()) } - AtRule::Return(..) => return Err("This at-rule is not allowed here.".into()), AtRule::AtRoot(root_stmts) => stmts.extend(root_stmts), - r => stmts.push(Stmt::AtRule(r)), + AtRule::Debug(ref message) => self.debug(expr.span, message), + AtRule::Warn(ref message) => self.warn(expr.span, message), + r => stmts.push(Spanned { + node: Stmt::AtRule(r), + span, + }), }, - Expr::Styles(s) => stmts.extend(s.into_iter().map(Box::new).map(Stmt::Style)), + Expr::Styles(s) => stmts.extend( + s.into_iter() + .map(Box::new) + .map(Stmt::Style) + .map(|style| Spanned { node: style, span }), + ), Expr::MixinDecl(name, mixin) => { scope.insert_mixin(&name, *mixin); } @@ -428,30 +472,33 @@ impl<'a> StyleSheetParser<'a> { scope.insert_fn(&name, *func); } Expr::Selector(s) => { - self.scope += 1; + self.nesting += 1; let rules = self.eat_rules(&super_selector.zip(&s), scope)?; - stmts.push(Stmt::RuleSet(RuleSet { - super_selector: super_selector.clone(), - selector: s, - rules, - })); - self.scope -= 1; - if self.scope == 0 { + stmts.push(Spanned { + node: Stmt::RuleSet(RuleSet { + super_selector: super_selector.clone(), + selector: s, + rules, + }), + span, + }); + self.nesting -= 1; + if self.nesting == 0 { return Ok(stmts); } } Expr::VariableDecl(name, val) => { - if self.scope == 0 { + if self.nesting == 0 { scope.insert_var(&name, *val.clone())?; insert_global_var(&name, *val)?; } else { scope.insert_var(&name, *val)?; } } - Expr::Include(rules) => stmts.extend(rules), - Expr::Debug(pos, ref message) => self.debug(pos, message), - Expr::Warn(pos, ref message) => self.warn(pos, message), - Expr::MultilineComment(s) => stmts.push(Stmt::MultilineComment(s)), + Expr::MultilineComment(s) => stmts.push(Spanned { + node: Stmt::MultilineComment(s), + span, + }), } } Ok(stmts) @@ -462,9 +509,15 @@ pub(crate) fn eat_expr>( toks: &mut Peekable, scope: &mut Scope, super_selector: &Selector, -) -> SassResult> { +) -> SassResult>> { let mut values = Vec::with_capacity(5); + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + return Ok(None); + }; while let Some(tok) = toks.peek() { + span = span.merge(tok.pos()); match tok.kind { ':' => { let tok = toks.next(); @@ -475,7 +528,10 @@ pub(crate) fn eat_expr>( super_selector, String::new(), )?; - return Ok(Some(Style::from_tokens(toks, scope, super_selector, prop)?)); + return Ok(Some(Spanned { + node: Style::from_tokens(toks, scope, super_selector, prop)?, + span, + })); } else { values.push(tok.unwrap()); } @@ -489,14 +545,20 @@ pub(crate) fn eat_expr>( devour_whitespace(&mut v); if v.peek().is_none() { devour_whitespace(toks); - return Ok(Some(Expr::Style(Box::new(Style { - property: String::new(), - value: Value::Null, - })))); + return Ok(Some(Spanned { + node: Expr::Style(Box::new(Style { + property: String::new(), + value: Value::Null.span(span), + })), + span, + })); } let property = Style::parse_property(&mut v, scope, super_selector, String::new())?; let value = Style::parse_value(&mut v, scope, super_selector)?; - return Ok(Some(Expr::Style(Box::new(Style { property, value })))); + return Ok(Some(Spanned { + node: Expr::Style(Box::new(Style { property, value })), + span, + })); } '}' => { if values.is_empty() { @@ -515,17 +577,23 @@ pub(crate) fn eat_expr>( let property = Style::parse_property(&mut v, scope, super_selector, String::new())?; let value = Style::parse_value(&mut v, scope, super_selector)?; - return Ok(Some(Expr::Style(Box::new(Style { property, value })))); + return Ok(Some(Spanned { + node: Expr::Style(Box::new(Style { property, value })), + span, + })); } } '{' => { toks.next(); devour_whitespace(toks); - return Ok(Some(Expr::Selector(Selector::from_tokens( - &mut values.into_iter().peekable(), - scope, - super_selector, - )?))); + return Ok(Some(Spanned { + node: Expr::Selector(Selector::from_tokens( + &mut values.into_iter().peekable(), + scope, + super_selector, + )?), + span, + })); } '$' => { let tok = toks.next().unwrap(); @@ -545,22 +613,28 @@ pub(crate) fn eat_expr>( global, } = eat_variable_value(toks, scope, super_selector)?; if global { - insert_global_var(&name, val.clone())?; + insert_global_var(&name.node, val.clone())?; } - if !default || scope.get_var(&name).is_err() { - return Ok(Some(Expr::VariableDecl(name, Box::new(val)))); + if !default || scope.get_var(name.clone()).is_err() { + return Ok(Some(Spanned { + node: Expr::VariableDecl(name.node, Box::new(val)), + span, + })); } } else { values.push(tok); - // HACK: we add the name back in, but lose the position information - // potentially requires refactoring heuristics for - // no space between colon and style value - values.extend(name.chars().map(|c| Token::new(Pos::new(), c))); + let mut current_pos = 0; + values.extend(name.chars().map(|x| { + let len = x.len_utf8() as u64; + let tok = Token::new(span.subspan(current_pos, current_pos + len), x); + current_pos += len; + tok + })); } } '/' => { let tok = toks.next().unwrap(); - let peeked = toks.peek().ok_or("expected more input.")?; + let peeked = toks.peek().ok_or(("expected more input.", tok.pos()))?; if peeked.kind == '/' { read_until_newline(toks); devour_whitespace(toks); @@ -569,43 +643,44 @@ pub(crate) fn eat_expr>( toks.next(); let comment = eat_comment(toks, scope, super_selector)?; devour_whitespace(toks); - return Ok(Some(Expr::MultilineComment(comment))); + return Ok(Some(Spanned { + node: Expr::MultilineComment(comment.node), + span: comment.span, + })); } else { values.push(tok); } } '@' => { - let pos = toks.next().unwrap().pos(); - match AtRuleKind::from(eat_ident(toks, scope, super_selector)?.as_str()) { - AtRuleKind::Include => { - devour_whitespace(toks); - return Ok(Some(Expr::Include(eat_include( - toks, - scope, - super_selector, - )?))); - } - v => { - devour_whitespace(toks); - return match AtRule::from_tokens(&v, pos, toks, scope, super_selector)? { - AtRule::Mixin(name, mixin) => Ok(Some(Expr::MixinDecl(name, mixin))), - AtRule::Function(name, func) => { - Ok(Some(Expr::FunctionDecl(name, func))) - } - AtRule::Charset => todo!("@charset as expr"), - AtRule::Debug(a, b) => Ok(Some(Expr::Debug(a, b))), - AtRule::Warn(a, b) => Ok(Some(Expr::Warn(a, b))), - a @ AtRule::Return(_) => Ok(Some(Expr::AtRule(a))), - c @ AtRule::Content => Ok(Some(Expr::AtRule(c))), - f @ AtRule::If(..) => Ok(Some(Expr::AtRule(f))), - f @ AtRule::For(..) => Ok(Some(Expr::AtRule(f))), - f @ AtRule::While(..) => Ok(Some(Expr::AtRule(f))), - f @ AtRule::Each(..) => Ok(Some(Expr::AtRule(f))), - u @ AtRule::Unknown(..) => Ok(Some(Expr::AtRule(u))), - u @ AtRule::AtRoot(..) => Ok(Some(Expr::AtRule(u))), - }; - } - } + toks.next(); + let Spanned { node: ident, span } = eat_ident(toks, scope, super_selector)?; + devour_whitespace(toks); + let rule = AtRule::from_tokens( + &AtRuleKind::from(ident.as_str()), + span, + toks, + scope, + super_selector, + )?; + return Ok(Some(Spanned { + node: match rule.node { + AtRule::Mixin(name, mixin) => Expr::MixinDecl(name, mixin), + AtRule::Function(name, func) => Expr::FunctionDecl(name, func), + AtRule::Charset => todo!("@charset as expr"), + d @ AtRule::Debug(..) => Expr::AtRule(d), + w @ AtRule::Warn(..) => Expr::AtRule(w), + a @ AtRule::Return(_) => Expr::AtRule(a), + c @ AtRule::Content => Expr::AtRule(c), + f @ AtRule::If(..) => Expr::AtRule(f), + f @ AtRule::For(..) => Expr::AtRule(f), + f @ AtRule::While(..) => Expr::AtRule(f), + f @ AtRule::Each(..) => Expr::AtRule(f), + u @ AtRule::Unknown(..) => Expr::AtRule(u), + u @ AtRule::AtRoot(..) => Expr::AtRule(u), + u @ AtRule::Include(..) => Expr::AtRule(u), + }, + span, + })); } '#' => { values.push(toks.next().unwrap()); @@ -639,17 +714,24 @@ fn eat_interpolation>(toks: &mut Peekable) -> Vec StyleSheetParser<'a> { - fn debug(&self, pos: Pos, message: &str) { - eprintln!("{}:{} Debug: {}", self.file, pos.line(), message); + fn debug(&self, span: Span, message: &str) { + let loc = self.map.look_up_span(span); + eprintln!( + "{}:{} Debug: {}", + loc.file.name(), + loc.begin.line + 1, + message + ); } - fn warn(&self, pos: Pos, message: &str) { + fn warn(&self, span: Span, message: &str) { + let loc = self.map.look_up_span(span); eprintln!( - "Warning: {}\n\t{} {}:{} todo!(scope)", + "Warning: {}\n {} {}:{} root stylesheet", message, - self.file, - pos.line(), - pos.column() + loc.file.name(), + loc.begin.line + 1, + loc.begin.column + 1 ); } } diff --git a/src/output.rs b/src/output.rs index 76912a4..3214751 100644 --- a/src/output.rs +++ b/src/output.rs @@ -1,5 +1,4 @@ //! # Convert from SCSS AST to CSS -use std::fmt; use std::io::Write; use crate::atrule::AtRule; @@ -20,11 +19,11 @@ enum BlockEntry { MultilineComment(String), } -impl fmt::Display for BlockEntry { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { +impl BlockEntry { + pub fn to_string(&self) -> SassResult { match self { - BlockEntry::Style(s) => writeln!(f, "{}", s), - BlockEntry::MultilineComment(s) => writeln!(f, "/*{}*/", s), + BlockEntry::Style(s) => s.to_string(), + BlockEntry::MultilineComment(s) => Ok(format!("/*{}*/", s)), } } } @@ -35,7 +34,7 @@ impl Toplevel { } fn push_style(&mut self, mut s: Style) -> SassResult<()> { - s.value = s.value.eval()?; + s = s.eval()?; if s.value.is_null() { return Ok(()); } @@ -79,8 +78,8 @@ impl Css { } let mut vals = vec![Toplevel::new_rule(selector)]; for rule in rules { - match rule { - Stmt::RuleSet(_) => vals.extend(self.parse_stmt(rule)?), + match rule.node { + Stmt::RuleSet(_) => vals.extend(self.parse_stmt(rule.node)?), Stmt::Style(s) => vals .get_mut(0) .expect("expected block to exist") @@ -91,7 +90,7 @@ impl Css { .push_comment(s), Stmt::AtRule(AtRule::AtRoot(stmts)) => stmts .into_iter() - .map(|r| Ok(vals.extend(self.parse_stmt(r)?))) + .map(|r| Ok(vals.extend(self.parse_stmt(r.node)?))) .collect::>()?, Stmt::AtRule(r) => vals.push(Toplevel::AtRule(r)), }; @@ -107,7 +106,7 @@ impl Css { fn parse_stylesheet(mut self, s: StyleSheet) -> SassResult { let mut is_first = true; for stmt in s.0 { - let v = self.parse_stmt(stmt)?; + let v = self.parse_stmt(stmt.node)?; // this is how we print newlines between unrelated styles // it could probably be refactored if !v.is_empty() { @@ -145,7 +144,7 @@ impl Css { has_written = true; writeln!(buf, "{}{} {{", padding, selector)?; for style in styles { - write!(buf, "{} {}", padding, style)?; + writeln!(buf, "{} {}", padding, style.to_string()?)?; } writeln!(buf, "{}}}", padding)?; } diff --git a/src/scope.rs b/src/scope.rs index a3f2de2..4afd06f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,16 +1,18 @@ use std::cell::RefCell; use std::collections::HashMap; +use codemap::Spanned; + use crate::atrule::{Function, Mixin}; use crate::error::SassResult; use crate::value::Value; thread_local!(pub(crate) static GLOBAL_SCOPE: RefCell = RefCell::new(Scope::new())); -pub(crate) fn get_global_var(s: &str) -> SassResult { - GLOBAL_SCOPE.with(|scope| match scope.borrow().vars().get(s) { +pub(crate) fn get_global_var(s: Spanned) -> SassResult> { + GLOBAL_SCOPE.with(|scope| match scope.borrow().vars().get(&s.node) { Some(v) => Ok(v.clone()), - None => Err("Undefined variable.".into()), + None => Err(("Undefined variable.", s.span).into()), }) } @@ -18,14 +20,14 @@ pub(crate) fn global_var_exists(v: &str) -> bool { GLOBAL_SCOPE.with(|scope| scope.borrow().vars().contains_key(v)) } -pub(crate) fn insert_global_var(s: &str, v: Value) -> SassResult> { +pub(crate) fn insert_global_var(s: &str, v: Spanned) -> SassResult>> { GLOBAL_SCOPE.with(|scope| scope.borrow_mut().insert_var(s, v)) } -pub(crate) fn get_global_fn(s: &str) -> SassResult { - GLOBAL_SCOPE.with(|scope| match scope.borrow().functions().get(s) { +pub(crate) fn get_global_fn(s: Spanned) -> SassResult { + GLOBAL_SCOPE.with(|scope| match scope.borrow().functions().get(&s.node) { Some(v) => Ok(v.clone()), - None => Err("Undefined function.".into()), + None => Err(("Undefined function.", s.span).into()), }) } @@ -37,10 +39,10 @@ pub(crate) fn insert_global_fn(s: &str, v: Function) -> Option { GLOBAL_SCOPE.with(|scope| scope.borrow_mut().insert_fn(s, v)) } -pub(crate) fn get_global_mixin(s: &str) -> SassResult { - GLOBAL_SCOPE.with(|scope| match scope.borrow().mixins().get(s) { +pub(crate) fn get_global_mixin(s: Spanned) -> SassResult { + GLOBAL_SCOPE.with(|scope| match scope.borrow().mixins().get(&s.node) { Some(v) => Ok(v.clone()), - None => Err("Undefined mixin.".into()), + None => Err(("Undefined mixin.", s.span).into()), }) } @@ -54,7 +56,7 @@ pub(crate) fn insert_global_mixin(s: &str, v: Mixin) -> Option { #[derive(Debug, Clone)] pub(crate) struct Scope { - vars: HashMap, + vars: HashMap>, mixins: HashMap, functions: HashMap, } @@ -69,7 +71,7 @@ impl Scope { } } - pub const fn vars(&self) -> &HashMap { + pub const fn vars(&self) -> &HashMap> { &self.vars } @@ -81,16 +83,17 @@ impl Scope { &self.mixins } - pub fn get_var(&self, v: &str) -> SassResult { - let name = &v.replace('_', "-"); - match self.vars.get(name) { + pub fn get_var(&self, mut name: Spanned) -> SassResult> { + name.node = name.node.replace('_', "-"); + match self.vars.get(&name.node) { Some(v) => Ok(v.clone()), None => get_global_var(name), } } - pub fn insert_var(&mut self, s: &str, v: Value) -> SassResult> { - Ok(self.vars.insert(s.replace('_', "-"), v.eval()?)) + pub fn insert_var(&mut self, s: &str, v: Spanned) -> SassResult>> { + let Spanned { node, span } = v; + Ok(self.vars.insert(s.replace('_', "-"), node.eval(span)?)) } pub fn var_exists(&self, v: &str) -> bool { @@ -98,9 +101,9 @@ impl Scope { self.vars.contains_key(name) || global_var_exists(name) } - pub fn get_mixin(&self, v: &str) -> SassResult { - let name = &v.replace('_', "-"); - match self.mixins.get(name) { + pub fn get_mixin(&self, mut name: Spanned) -> SassResult { + name.node = name.node.replace('_', "-"); + match self.mixins.get(&name.node) { Some(v) => Ok(v.clone()), None => get_global_mixin(name), } @@ -115,9 +118,9 @@ impl Scope { self.mixins.contains_key(name) || global_mixin_exists(name) } - pub fn get_fn(&self, v: &str) -> SassResult { - let name = &v.replace('_', "-"); - match self.functions.get(name) { + pub fn get_fn(&self, mut name: Spanned) -> SassResult { + name.node = name.node.replace('_', "-"); + match self.functions.get(&name.node) { Some(v) => Ok(v.clone()), None => get_global_fn(name), } diff --git a/src/selector/attribute.rs b/src/selector/attribute.rs index 8e87166..e839256 100644 --- a/src/selector/attribute.rs +++ b/src/selector/attribute.rs @@ -1,6 +1,7 @@ use std::fmt::{self, Display}; use std::iter::Peekable; -use std::string::ToString; + +use codemap::Span; use super::{Selector, SelectorKind}; use crate::error::SassResult; @@ -23,25 +24,34 @@ impl Attribute { toks: &mut Peekable, scope: &Scope, super_selector: &Selector, + mut start: Span, ) -> SassResult { devour_whitespace(toks); - let attr = match toks.peek().ok_or("Expected identifier.")?.kind { - c if is_ident_char(c) => eat_ident(toks, scope, super_selector)?, + let next_tok = toks.peek().ok_or(("Expected identifier.", start))?; + let attr = match next_tok.kind { + c if is_ident_char(c) => { + let i = eat_ident(toks, scope, super_selector)?; + start = i.span; + i.node + } '#' => { - toks.next(); - if toks.next().ok_or("Expected expression.")?.kind == '{' { - parse_interpolation(toks, scope, super_selector)?.to_string() + start.merge(toks.next().unwrap().pos()); + if toks.next().ok_or(("Expected expression.", start))?.kind == '{' { + let interpolation = parse_interpolation(toks, scope, super_selector)?; + interpolation.node.to_css_string(interpolation.span)? } else { - return Err("Expected expression.".into()); + return Err(("Expected expression.", start).into()); } } - _ => return Err("Expected identifier.".into()), + _ => return Err(("Expected identifier.", start).into()), }; devour_whitespace(toks); - let kind = match toks.next().ok_or("expected \"{\".")?.kind { - c if is_ident_char(c) => return Err("Expected \"]\".".into()), + let next = toks.next().ok_or(("expected \"]\".", start))?; + + let kind = match next.kind { + c if is_ident_char(c) => return Err(("Expected \"]\".", next.pos()).into()), ']' => { return Ok(SelectorKind::Attribute(Attribute { kind: AttributeKind::Any, @@ -56,29 +66,36 @@ impl Attribute { '^' => AttributeKind::Prefix, '$' => AttributeKind::Suffix, '*' => AttributeKind::Contains, - _ => return Err("expected \"]\".".into()), + _ => return Err(("expected \"]\".", next.pos()).into()), }; if kind != AttributeKind::Equals { - match toks.next().ok_or("expected \"=\".")?.kind { + let next = toks.next().ok_or(("expected \"=\".", next.pos()))?; + match next.kind { '=' => {} - _ => return Err("expected \"=\".".into()), + _ => return Err(("expected \"=\".", next.pos()).into()), } } devour_whitespace(toks); - let value = match toks.next().ok_or("Expected identifier.")?.kind { + let next = toks.next().ok_or(("Expected identifier.", next.pos()))?; + + let value = match next.kind { v @ 'a'..='z' | v @ 'A'..='Z' | v @ '-' | v @ '_' => { - format!("{}{}", v, eat_ident(toks, scope, super_selector)?) + format!("{}{}", v, eat_ident(toks, scope, super_selector)?.node) } - q @ '"' | q @ '\'' => parse_quoted_string(toks, scope, q, super_selector)?.to_string(), - _ => return Err("Expected identifier.".into()), + q @ '"' | q @ '\'' => { + parse_quoted_string(toks, scope, q, super_selector)?.to_css_string(next.pos())? + } + _ => return Err(("Expected identifier.", next.pos()).into()), }; devour_whitespace(toks); - let modifier = match toks.next().ok_or("expected \"]\".")?.kind { + let next = toks.next().ok_or(("expected \"]\".", next.pos()))?; + + let modifier = match next.kind { ']' => { return Ok(SelectorKind::Attribute(Attribute { kind, @@ -88,13 +105,14 @@ impl Attribute { })) } v @ 'a'..='z' | v @ 'A'..='Z' => { - match toks.next().ok_or("expected \"]\".")?.kind { + let next = toks.next().ok_or(("expected \"]\".", next.pos()))?; + match next.kind { ']' => {} - _ => return Err("expected \"]\".".into()), + _ => return Err(("expected \"]\".", next.pos()).into()), } Some(v) } - _ => return Err("expected \"]\".".into()), + _ => return Err(("expected \"]\".", next.pos()).into()), }; Ok(SelectorKind::Attribute(Attribute { diff --git a/src/selector/mod.rs b/src/selector/mod.rs index 43a9d06..299152b 100644 --- a/src/selector/mod.rs +++ b/src/selector/mod.rs @@ -1,9 +1,7 @@ use std::fmt::{self, Display, Write}; use std::iter::Peekable; -use std::string::ToString; use crate::error::SassResult; -use crate::lexer::Lexer; use crate::scope::Scope; use crate::utils::{ devour_whitespace, eat_comment, eat_ident_no_interpolation, parse_interpolation, @@ -183,13 +181,20 @@ impl Selector { super_selector: &Selector, ) -> SassResult { let mut string = String::new(); + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + return Ok(Selector::new()); + }; while let Some(tok) = toks.next() { + span = span.merge(tok.pos()); match tok.kind { '#' => { if toks.peek().is_some() && toks.peek().unwrap().kind == '{' { toks.next(); string.push_str( - &parse_interpolation(toks, scope, super_selector)?.to_string(), + &parse_interpolation(toks, scope, super_selector)? + .to_css_string(span)?, ); } else { string.push('#'); @@ -207,7 +212,7 @@ impl Selector { } '/' => { if toks.peek().is_none() { - return Err("Expected selector.".into()); + return Err(("Expected selector.", tok.pos()).into()); } else if '*' == toks.peek().unwrap().kind { toks.next(); eat_comment(toks, &Scope::new(), &Selector::new())?; @@ -215,7 +220,7 @@ impl Selector { read_until_newline(toks); devour_whitespace(toks); } else { - return Err("Expected selector.".into()); + return Err(("Expected selector.", tok.pos()).into()); } string.push(' '); } @@ -228,7 +233,6 @@ impl Selector { continue; } string.push(c); - string.push(','); break; } @@ -238,17 +242,24 @@ impl Selector { let mut contains_super_selector = false; let mut parts = Vec::new(); - // HACK: we re-lex here to get access to generic helper functions that - // operate on `Token`s. Ideally, we would in the future not have - // to do this, or at the very least retain the span information. - let mut iter = Lexer::new(&string).peekable(); + let mut sel_toks = Vec::new(); + + let mut current_pos = 0; + sel_toks.extend(string.chars().map(|x| { + let len = x.len_utf8() as u64; + let tok = Token::new(span.subspan(current_pos, current_pos + len), x); + current_pos += len; + tok + })); + + let mut iter = sel_toks.into_iter().peekable(); while let Some(tok) = iter.peek() { inner.push(match tok.kind { _ if is_selector_name_char(tok.kind) => { - inner.push(SelectorKind::Element(eat_ident_no_interpolation( - &mut iter, - )?)); + inner.push(SelectorKind::Element( + eat_ident_no_interpolation(&mut iter)?.node, + )); continue; } '&' => { @@ -257,20 +268,24 @@ impl Selector { } '.' => { iter.next(); - inner.push(SelectorKind::Class(eat_ident_no_interpolation(&mut iter)?)); + inner.push(SelectorKind::Class( + eat_ident_no_interpolation(&mut iter)?.node, + )); continue; } '#' => { iter.next(); - inner.push(SelectorKind::Id(eat_ident_no_interpolation(&mut iter)?)); + inner.push(SelectorKind::Id( + eat_ident_no_interpolation(&mut iter)?.node, + )); continue; } '%' => { iter.next(); is_invisible = true; - inner.push(SelectorKind::Placeholder(eat_ident_no_interpolation( - &mut iter, - )?)); + inner.push(SelectorKind::Placeholder( + eat_ident_no_interpolation(&mut iter)?.node, + )); continue; } '>' => SelectorKind::ImmediateChild, @@ -298,8 +313,13 @@ impl Selector { continue; } '[' => { - iter.next(); - inner.push(Attribute::from_tokens(&mut iter, scope, super_selector)?); + let span = iter.next().unwrap().pos(); + inner.push(Attribute::from_tokens( + &mut iter, + scope, + super_selector, + span, + )?); continue; } ':' => { @@ -322,7 +342,7 @@ impl Selector { } continue; } - _ => return Err("expected selector.".into()), + _ => return Err(("expected selector.", tok.pos()).into()), }); iter.next(); } @@ -351,7 +371,7 @@ impl Selector { false }; if is_selector_name_char(toks.peek().unwrap().kind) { - let name = eat_ident_no_interpolation(toks)?; + let name = eat_ident_no_interpolation(toks)?.node; Ok( if toks.peek().is_some() && toks.peek().unwrap().kind == '(' { toks.next(); diff --git a/src/style.rs b/src/style.rs index cb14eba..21fff78 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,6 +1,7 @@ -use std::fmt::{self, Display}; use std::iter::Peekable; +use codemap::Spanned; + use crate::error::SassResult; use crate::scope::Scope; use crate::selector::Selector; @@ -15,13 +16,7 @@ use crate::{Expr, Token}; #[derive(Clone, Debug, Eq, PartialEq)] pub(crate) struct Style { pub property: String, - pub value: Value, -} - -impl Display for Style { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {};", self.property, self.value) - } + pub value: Spanned, } impl Style { @@ -34,11 +29,29 @@ impl Style { StyleParser::new(scope, super_selector).parse_property(toks, super_property) } + pub fn to_string(&self) -> SassResult { + Ok(format!( + "{}: {};", + self.property, + self.value.node.to_css_string(self.value.span)? + )) + } + + pub(crate) fn eval(self) -> SassResult { + Ok(Style { + property: self.property, + value: Spanned { + span: self.value.span, + node: self.value.node.eval(self.value.span)?.node, + }, + }) + } + pub fn parse_value>( toks: &mut Peekable, scope: &Scope, super_selector: &Selector, - ) -> SassResult { + ) -> SassResult> { StyleParser::new(scope, super_selector).parse_style_value(toks, scope) } @@ -69,7 +82,7 @@ impl<'a> StyleParser<'a> { &self, toks: &mut Peekable, scope: &Scope, - ) -> SassResult { + ) -> SassResult> { devour_whitespace(toks); Value::from_vec( read_until_semicolon_or_open_or_closing_curly_brace(toks), @@ -152,8 +165,8 @@ impl<'a> StyleParser<'a> { } } _ => { - let val = self.parse_style_value(toks, scope)?; - let t = toks.peek().ok_or("expected more input.")?; + let value = self.parse_style_value(toks, scope)?; + let t = toks.peek().ok_or(("expected more input.", value.span))?; match t.kind { '}' => {} ';' => { @@ -163,7 +176,7 @@ impl<'a> StyleParser<'a> { '{' => { let mut v = vec![Style { property: super_property.clone(), - value: val, + value, }]; match self.eat_style_group(toks, super_property, scope)? { Expr::Style(s) => v.push(*s), @@ -176,7 +189,7 @@ impl<'a> StyleParser<'a> { } return Ok(Expr::Style(Box::new(Style { property: super_property, - value: val, + value, }))); } } @@ -190,7 +203,7 @@ impl<'a> StyleParser<'a> { mut super_property: String, ) -> SassResult { devour_whitespace(toks); - let property = eat_ident(toks, self.scope, self.super_selector)?; + let property = eat_ident(toks, self.scope, self.super_selector)?.node; devour_whitespace_or_comment(toks)?; if toks.peek().is_some() && toks.peek().unwrap().kind == ':' { toks.next(); diff --git a/src/token.rs b/src/token.rs index b6cff32..bc6c8b1 100644 --- a/src/token.rs +++ b/src/token.rs @@ -1,18 +1,19 @@ -use crate::common::Pos; use crate::utils::IsWhitespace; +use codemap::Span; + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) struct Token { - pub pos: Pos, + pub pos: Span, pub kind: char, } impl Token { - pub const fn new(pos: Pos, kind: char) -> Self { + pub const fn new(pos: Span, kind: char) -> Self { Self { pos, kind } } - pub const fn pos(&self) -> Pos { + pub const fn pos(&self) -> Span { self.pos } } diff --git a/src/utils.rs b/src/utils.rs index 54f148c..d50eccc 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,5 +1,7 @@ use std::iter::{Iterator, Peekable}; +use codemap::Spanned; + use crate::common::QuoteKind; use crate::error::SassResult; use crate::selector::Selector; @@ -35,27 +37,27 @@ pub(crate) trait IsComment { } pub(crate) fn devour_whitespace_or_comment>( - s: &mut Peekable, + toks: &mut Peekable, ) -> SassResult { let mut found_whitespace = false; - while let Some(w) = s.peek() { - if w.kind == '/' { - s.next(); - match s.peek().unwrap().kind { + while let Some(tok) = toks.peek() { + if tok.kind == '/' { + let pos = toks.next().unwrap().pos(); + match toks.peek().unwrap().kind { '*' => { - eat_comment(s, &Scope::new(), &Selector::new())?; + eat_comment(toks, &Scope::new(), &Selector::new())?; } - '/' => read_until_newline(s), - _ => return Err("Expected expression.".into()), + '/' => read_until_newline(toks), + _ => return Err(("Expected expression.", pos).into()), }; found_whitespace = true; continue; } - if !w.is_whitespace() { + if !tok.is_whitespace() { break; } found_whitespace = true; - s.next(); + toks.next(); } Ok(found_whitespace) } @@ -64,20 +66,23 @@ pub(crate) fn parse_interpolation>( toks: &mut Peekable, scope: &Scope, super_selector: &Selector, -) -> SassResult { +) -> SassResult> { let val = Value::from_vec(read_until_closing_curly_brace(toks), scope, super_selector)?; toks.next(); - Ok(val.eval()?.unquote()) + Ok(Spanned { + node: val.node.eval(val.span)?.node.unquote(), + span: val.span, + }) } pub(crate) struct VariableDecl { - pub val: Value, + pub val: Spanned, pub default: bool, pub global: bool, } impl VariableDecl { - pub const fn new(val: Value, default: bool, global: bool) -> VariableDecl { + pub const fn new(val: Spanned, default: bool, global: bool) -> VariableDecl { VariableDecl { val, default, @@ -327,28 +332,22 @@ pub(crate) fn eat_variable_value>( match next.kind { 'i' => todo!("!important"), 'g' => { - if eat_ident(&mut raw, scope, super_selector)? - .to_ascii_lowercase() - .as_str() - == "lobal" - { + let s = eat_ident(&mut raw, scope, super_selector)?; + if s.node.to_ascii_lowercase().as_str() == "lobal" { global = true; } else { - return Err("Invalid flag name.".into()); + return Err(("Invalid flag name.", s.span).into()); } } 'd' => { - if eat_ident(&mut raw, scope, super_selector)? - .to_ascii_lowercase() - .as_str() - == "efault" - { + let s = eat_ident(&mut raw, scope, super_selector)?; + if s.to_ascii_lowercase().as_str() == "efault" { default = true; } else { - return Err("Invalid flag name.".into()); + return Err(("Invalid flag name.", s.span).into()); } } - _ => return Err("Invalid flag name.".into()), + _ => return Err(("Invalid flag name.", next.pos()).into()), } } _ => val_toks.push(tok), @@ -364,17 +363,21 @@ pub(crate) fn eat_ident>( toks: &mut Peekable, scope: &Scope, super_selector: &Selector, -) -> SassResult { +) -> SassResult> { let mut s = String::new(); + let mut span = toks.peek().unwrap().pos(); while let Some(tok) = toks.peek() { + span = span.merge(tok.pos()); match tok.kind { '#' => { - toks.next(); - if toks.peek().ok_or("Expected identifier.")?.kind == '{' { + let tok = toks.next().unwrap(); + if toks.peek().ok_or(("Expected identifier.", tok.pos()))?.kind == '{' { toks.next(); - s.push_str(&parse_interpolation(toks, scope, super_selector)?.to_string()); + let interpolation = parse_interpolation(toks, scope, super_selector)?; + span = span.merge(interpolation.span); + s.push_str(&interpolation.node.to_css_string(interpolation.span)?); } else { - return Err("Expected identifier.".into()); + return Err(("Expected identifier.", tok.pos()).into()); } } _ if tok.kind.is_ascii_alphanumeric() @@ -385,7 +388,7 @@ pub(crate) fn eat_ident>( s.push(toks.next().unwrap().kind) } '\\' => { - toks.next(); + let span_start = toks.next().unwrap().pos(); let mut n = String::new(); while let Some(c) = toks.peek() { if !c.kind.is_ascii_hexdigit() || n.len() > 6 { @@ -395,7 +398,7 @@ pub(crate) fn eat_ident>( toks.next(); } if n.is_empty() { - let c = toks.next().ok_or("expected \"{\".")?.kind; + let c = toks.next().ok_or(("expected \"{\".", span_start))?.kind; if (c == '-' && !s.is_empty()) || c.is_ascii_alphabetic() { s.push(c); } else { @@ -418,14 +421,20 @@ pub(crate) fn eat_ident>( _ => break, } } - Ok(s) + Ok(Spanned { node: s, span }) } pub(crate) fn eat_ident_no_interpolation>( toks: &mut Peekable, -) -> SassResult { +) -> SassResult> { let mut s = String::new(); + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + todo!() + }; while let Some(tok) = toks.peek() { + span = span.merge(tok.pos()); match tok.kind { '#' => { break; @@ -471,26 +480,36 @@ pub(crate) fn eat_ident_no_interpolation>( _ => break, } } - Ok(s) + Ok(Spanned { node: s, span }) } -pub(crate) fn eat_number>(toks: &mut Peekable) -> SassResult { +pub(crate) fn eat_number>( + toks: &mut Peekable, +) -> SassResult> { let mut whole = String::new(); + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + todo!() + }; while let Some(c) = toks.peek() { if !c.kind.is_numeric() { break; } let tok = toks.next().unwrap(); + span = span.merge(tok.pos()); whole.push(tok.kind); } if toks.peek().is_none() { - return Ok(whole); + return Ok(Spanned { node: whole, span }); } let mut dec = String::new(); - if toks.peek().unwrap().kind == '.' { + let next_tok = toks.peek().unwrap().clone(); + + if next_tok.kind == '.' { toks.next(); dec.push('.'); while let Some(c) = toks.peek() { @@ -498,16 +517,17 @@ pub(crate) fn eat_number>(toks: &mut Peekable) -> S break; } let tok = toks.next().unwrap(); + span = span.merge(tok.pos()); dec.push(tok.kind); } } if dec.len() == 1 { - return Err("Expected digit.".into()); + return Err(("Expected digit.", next_tok.pos()).into()); } whole.push_str(&dec); - Ok(whole) + Ok(Spanned { node: whole, span }) } /// Eat tokens until a newline @@ -529,13 +549,21 @@ pub(crate) fn read_until_newline>(toks: &mut Peekable< /// This function assumes that the starting "/*" has already been consumed /// The entirety of the comment, including the ending "*/" is consumed. /// Note that the ending "*/" is not included in the output. +/// +/// TODO: support interpolation within multiline comments pub(crate) fn eat_comment>( toks: &mut Peekable, _scope: &Scope, _super_selector: &Selector, -) -> SassResult { +) -> SassResult> { let mut comment = String::new(); + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + todo!() + }; while let Some(tok) = toks.next() { + span = span.merge(tok.pos()); if tok.kind == '*' && toks.peek().unwrap().kind == '/' { toks.next(); break; @@ -543,7 +571,10 @@ pub(crate) fn eat_comment>( comment.push(tok.kind); } devour_whitespace(toks); - Ok(comment) + Ok(Spanned { + node: comment, + span, + }) } pub(crate) fn parse_quoted_string>( @@ -551,11 +582,17 @@ pub(crate) fn parse_quoted_string>( scope: &Scope, q: char, super_selector: &Selector, -) -> SassResult { +) -> SassResult> { let mut s = String::new(); let mut is_escaped = false; let mut found_interpolation = false; + let mut span = if let Some(tok) = toks.peek() { + tok.pos() + } else { + todo!() + }; while let Some(tok) = toks.next() { + span = span.merge(tok.pos()); match tok.kind { '"' if !is_escaped && q == '"' => break, '"' if is_escaped => { @@ -579,14 +616,15 @@ pub(crate) fn parse_quoted_string>( if toks.peek().unwrap().kind == '{' { toks.next(); found_interpolation = true; - s.push_str(&parse_interpolation(toks, scope, super_selector)?.to_string()); + let interpolation = parse_interpolation(toks, scope, super_selector)?; + s.push_str(&interpolation.node.to_css_string(interpolation.span)?); continue; } else { s.push('#'); continue; } } - '\n' => return Err("Expected \".".into()), + '\n' => return Err(("Expected \".", tok.pos()).into()), v if v.is_ascii_hexdigit() && is_escaped => { let mut n = v.to_string(); while let Some(c) = toks.peek() { @@ -628,7 +666,10 @@ pub(crate) fn parse_quoted_string>( _ => unreachable!(), } }; - Ok(Value::Ident(s, quotes)) + Ok(Spanned { + node: Value::Ident(s, quotes), + span, + }) } pub(crate) fn read_until_closing_paren>( diff --git a/src/value/css_function.rs b/src/value/css_function.rs index a7c9f86..c1539be 100644 --- a/src/value/css_function.rs +++ b/src/value/css_function.rs @@ -21,8 +21,10 @@ pub(crate) fn eat_calc_args>( } '#' => { if toks.peek().is_some() && toks.peek().unwrap().kind == '{' { - toks.next(); - string.push_str(&parse_interpolation(toks, scope, super_selector)?.to_string()); + let span = toks.next().unwrap().pos(); + string.push_str( + &parse_interpolation(toks, scope, super_selector)?.to_css_string(span)?, + ); } else { string.push('#'); } @@ -60,7 +62,9 @@ pub(crate) fn eat_progid>( super_selector: &Selector, ) -> SassResult { let mut string = String::new(); + let mut span = toks.peek().unwrap().pos(); while let Some(tok) = toks.next() { + span = span.merge(tok.pos()); match tok.kind { 'a'..='z' | 'A'..='Z' | '.' => { string.push(tok.kind); @@ -69,7 +73,7 @@ pub(crate) fn eat_progid>( string.push_str(&eat_calc_args(toks, scope, super_selector)?); break; } - _ => return Err("expected \"(\".".into()), + _ => return Err(("expected \"(\".", span).into()), } } Ok(string) diff --git a/src/value/map.rs b/src/value/map.rs index 78e3308..7052014 100644 --- a/src/value/map.rs +++ b/src/value/map.rs @@ -1,6 +1,8 @@ use std::slice::Iter; use std::vec::IntoIter; +use codemap::Span; + use super::Value; use crate::common::{Brackets, ListSeparator}; use crate::error::SassResult; @@ -13,9 +15,9 @@ impl SassMap { SassMap(Vec::new()) } - pub fn get(self, key: &Value) -> SassResult> { + pub fn get(self, key: &Value, span: Span) -> SassResult> { for (k, v) in self.0 { - if k.equals(key.clone())? { + if k.equals(key.clone(), span)? { return Ok(Some(v)); } } diff --git a/src/value/mod.rs b/src/value/mod.rs index a04450b..9eeff7e 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,7 +1,8 @@ use std::cmp::Ordering; -use std::fmt::{self, Display, Write}; use std::iter::Iterator; +use codemap::{Span, Spanned}; + use crate::color::Color; use crate::common::{Brackets, ListSeparator, Op, QuoteKind}; use crate::error::SassResult; @@ -33,112 +34,11 @@ pub(crate) enum Value { Paren(Box), Ident(String, QuoteKind), Map(SassMap), - ArgList(Vec), + ArgList(Vec>), /// Returned by `get-function()` Function(SassFunction), } -impl Display for Value { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::Important => write!(f, "!important"), - Self::Dimension(num, unit) => match unit { - Unit::Mul(..) => { - eprintln!("Error: {}{} isn't a valid CSS value.", num, unit); - std::process::exit(1); - } - _ => write!(f, "{}{}", num, unit), - }, - Self::Map(map) => write!( - f, - "({})", - map.iter() - .map(|(k, v)| format!("{}: {}", k, v)) - .collect::>() - .join(", ") - ), - Self::Function(func) => write!(f, "get-function(\"{}\")", func.name()), - Self::List(vals, sep, brackets) => match brackets { - Brackets::None => write!( - f, - "{}", - vals.iter() - .filter(|x| !x.is_null()) - .map(std::string::ToString::to_string) - .collect::>() - .join(sep.as_str()), - ), - Brackets::Bracketed => write!( - f, - "[{}]", - vals.iter() - .filter(|x| !x.is_null()) - .map(std::string::ToString::to_string) - .collect::>() - .join(sep.as_str()), - ), - }, - Self::Color(c) => write!(f, "{}", c), - Self::UnaryOp(..) | Self::BinaryOp(..) => write!( - f, - "{}", - match self.clone().eval() { - Ok(v) => v, - Err(e) => { - eprintln!("{}", e); - std::process::exit(1); - } - } - ), - Self::Paren(val) => write!(f, "{}", val), - Self::Ident(val, kind) => { - if kind == &QuoteKind::None { - return write!(f, "{}", val); - } - let has_single_quotes = val.contains(|x| x == '\''); - let has_double_quotes = val.contains(|x| x == '"'); - if has_single_quotes && !has_double_quotes { - write!(f, "\"{}\"", val) - } else if !has_single_quotes && has_double_quotes { - write!(f, "'{}'", val) - } else if !has_single_quotes && !has_double_quotes { - write!(f, "\"{}\"", val) - } else { - let quote_char = match kind { - QuoteKind::Double => '"', - QuoteKind::Single => '\'', - _ => unreachable!(), - }; - f.write_char(quote_char)?; - for c in val.chars() { - match c { - '"' | '\'' if c == quote_char => { - f.write_char('\\')?; - f.write_char(quote_char)?; - } - v => f.write_char(v)?, - } - } - f.write_char(quote_char)?; - Ok(()) - } - } - Self::True => write!(f, "true"), - Self::False => write!(f, "false"), - Self::Null => write!(f, "null"), - Self::ArgList(args) => write!( - f, - "{}", - args.iter() - .filter(|x| !x.is_null()) - .map(std::string::ToString::to_string) - .collect::>() - .join(", "), - ), - } - } -} - impl Value { pub fn is_null(&self) -> bool { match self { @@ -147,12 +47,106 @@ impl Value { _ => false, } } + pub fn to_css_string(&self, span: Span) -> SassResult { + Ok(match self { + Self::Important => format!("!important"), + Self::Dimension(num, unit) => match unit { + Unit::Mul(..) => { + return Err(( + format!("Error: {}{} isn't a valid CSS value.", num, unit), + span, + ) + .into()); + } + _ => format!("{}{}", num, unit), + }, + Self::Map(map) => format!( + "({})", + map.iter() + .map(|(k, v)| Ok(format!( + "{}: {}", + k.to_css_string(span)?, + v.to_css_string(span)? + ))) + .collect::>>()? + .join(", ") + ), + Self::Function(func) => format!("get-function(\"{}\")", func.name()), + Self::List(vals, sep, brackets) => match brackets { + Brackets::None => format!( + "{}", + vals.iter() + .filter(|x| !x.is_null()) + .map(|x| x.to_css_string(span)) + .collect::>>()? + .join(sep.as_str()), + ), + Brackets::Bracketed => format!( + "[{}]", + vals.iter() + .filter(|x| !x.is_null()) + .map(|x| x.to_css_string(span)) + .collect::>>()? + .join(sep.as_str()), + ), + }, + Self::Color(c) => format!("{}", c), + Self::UnaryOp(..) | Self::BinaryOp(..) => { + format!("{}", self.clone().eval(span)?.to_css_string(span)?) + } + Self::Paren(val) => format!("{}", val.to_css_string(span)?), + Self::Ident(val, kind) => { + if kind == &QuoteKind::None { + return Ok(val.clone()); + } + let has_single_quotes = val.contains(|x| x == '\''); + let has_double_quotes = val.contains(|x| x == '"'); + if has_single_quotes && !has_double_quotes { + format!("\"{}\"", val) + } else if !has_single_quotes && has_double_quotes { + format!("'{}'", val) + } else if !has_single_quotes && !has_double_quotes { + format!("\"{}\"", val) + } else { + let quote_char = match kind { + QuoteKind::Double => '"', + QuoteKind::Single => '\'', + _ => unreachable!(), + }; + let mut buf = String::with_capacity(val.len() + 2); + buf.push(quote_char); + for c in val.chars() { + match c { + '"' | '\'' if c == quote_char => { + buf.push('\\'); + buf.push(quote_char); + } + v => buf.push(v), + } + } + buf.push(quote_char); + buf + } + } + Self::True => "true".to_string(), + Self::False => "false".to_string(), + Self::Null => "null".to_string(), + Self::ArgList(args) => format!( + "{}", + args.iter() + .filter(|x| !x.is_null()) + .map(|a| Ok(a.node.to_css_string(span)?)) + .collect::>>()? + .join(", "), + ), + }) + } - pub fn is_true(&self) -> SassResult { + pub fn is_true(&self, span: Span) -> SassResult { match self { Value::Null | Value::False => Ok(false), Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => { - self.clone().eval()?.is_true() + self.clone().eval(span)?.is_true(span) } _ => Ok(true), } @@ -165,7 +159,11 @@ impl Value { } } - pub fn kind(&self) -> SassResult<&'static str> { + pub fn span(self, span: Span) -> Spanned { + Spanned { node: self, span } + } + + pub fn kind(&self, span: Span) -> SassResult<&'static str> { match self { Self::Color(..) => Ok("color"), Self::Ident(..) | Self::Important => Ok("string"), @@ -176,7 +174,9 @@ impl Value { Self::True | Self::False => Ok("bool"), Self::Null => Ok("null"), Self::Map(..) => Ok("map"), - Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => self.clone().eval()?.kind(), + Self::BinaryOp(..) | Self::Paren(..) | Self::UnaryOp(..) => { + self.clone().eval(span)?.kind(span) + } } } @@ -195,19 +195,19 @@ impl Value { } } - pub fn inspect(&self) -> String { - match self { + pub fn inspect(&self, span: Span) -> SassResult { + Ok(match self { Value::List(v, _, brackets) if v.is_empty() => match brackets { Brackets::None => "()".to_string(), Brackets::Bracketed => "[]".to_string(), }, Value::Function(f) => format!("get-function(\"{}\")", f.name()), - v => v.to_string(), - } + v => v.to_css_string(span)?, + }) } - pub fn equals(self, other: Value) -> SassResult { - Ok(match self.eval()? { + pub fn equals(self, other: Value, span: Span) -> SassResult { + Ok(match self.eval(span)?.node { Self::Ident(s1, ..) => match other { Self::Ident(s2, ..) => s1 == s2, _ => false, @@ -227,64 +227,71 @@ impl Value { } _ => false, }, - s => s == other.eval()?, + s => s == other.eval(span)?.node, }) } - pub fn unary_op_plus(self) -> SassResult { - Ok(match self.eval()? { + pub fn unary_op_plus(self, span: Span) -> SassResult { + Ok(match self.eval(span)?.node { v @ Value::Dimension(..) => v, - v => Value::Ident(format!("+{}", v), QuoteKind::None), + v => Value::Ident(format!("+{}", v.to_css_string(span)?), QuoteKind::None), }) } - pub fn eval(self) -> SassResult { - match self { + pub fn eval(self, span: Span) -> SassResult> { + Ok(match self { Self::BinaryOp(lhs, op, rhs) => match op { - Op::Plus => *lhs + *rhs, - Op::Minus => *lhs - *rhs, - Op::Equal => Ok(Self::bool(lhs.equals(*rhs)?)), - Op::NotEqual => Ok(Self::bool(!lhs.equals(*rhs)?)), - Op::Mul => *lhs * *rhs, - Op::Div => *lhs / *rhs, - Op::Rem => *lhs % *rhs, - Op::GreaterThan => lhs.cmp(*rhs, op), - Op::GreaterThanEqual => lhs.cmp(*rhs, op), - Op::LessThan => lhs.cmp(*rhs, op), - Op::LessThanEqual => lhs.cmp(*rhs, op), + Op::Plus => lhs.add(*rhs, span)?, + Op::Minus => lhs.sub(*rhs, span)?, + Op::Equal => Self::bool(lhs.equals(*rhs, span)?), + Op::NotEqual => Self::bool(!lhs.equals(*rhs, span)?), + Op::Mul => lhs.mul(*rhs, span)?, + Op::Div => lhs.div(*rhs, span)?, + Op::Rem => lhs.rem(*rhs, span)?, + Op::GreaterThan => return lhs.cmp(*rhs, op, span), + Op::GreaterThanEqual => return lhs.cmp(*rhs, op, span), + Op::LessThan => return lhs.cmp(*rhs, op, span), + Op::LessThanEqual => return lhs.cmp(*rhs, op, span), Op::Not => unreachable!(), - Op::And => Ok(if lhs.clone().is_true()? { - rhs.eval()? - } else { - lhs.eval()? - }), - Op::Or => Ok(if lhs.is_true()? { - lhs.eval()? - } else { - rhs.eval()? - }), + Op::And => { + if lhs.clone().is_true(span)? { + rhs.eval(span)?.node + } else { + lhs.eval(span)?.node + } + } + Op::Or => { + if lhs.is_true(span)? { + lhs.eval(span)?.node + } else { + rhs.eval(span)?.node + } + } }, - Self::Paren(v) => v.eval(), + Self::Paren(v) => v.eval(span)?.node, Self::UnaryOp(op, val) => match op { - Op::Plus => val.unary_op_plus(), - Op::Minus => -*val, - Op::Not => Ok(Self::bool(!val.eval()?.is_true()?)), + Op::Plus => val.unary_op_plus(span)?, + Op::Minus => val.neg(span)?, + Op::Not => Self::bool(!val.eval(span)?.is_true(span)?), _ => unreachable!(), }, - _ => Ok(self), + _ => self, } + .span(span)) } - pub fn cmp(self, mut other: Self, op: Op) -> SassResult { + pub fn cmp(self, mut other: Self, op: Op, span: Span) -> SassResult> { if let Self::Paren(..) = other { - other = other.eval()? + other = other.eval(span)?.node } let precedence = op.precedence(); let ordering = match self { Self::Dimension(num, unit) => match &other { Self::Dimension(num2, unit2) => { if !unit.comparable(&unit2) { - return Err(format!("Incompatible units {} and {}.", unit2, unit).into()); + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); } if &unit == unit2 { num.cmp(num2) @@ -301,41 +308,69 @@ impl Value { } } Self::BinaryOp(..) => todo!(), - v => return Err(format!("Undefined operation \"{} {} {}\".", v, op, other).into()), + v => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + v.to_css_string(span)?, + op, + other.to_css_string(span)? + ), + span, + ) + .into()) + } }, Self::BinaryOp(left, op2, right) => { return if op2.precedence() >= precedence { - Self::BinaryOp(left, op2, right).eval()?.cmp(other, op) + Self::BinaryOp(left, op2, right) + .eval(span)? + .node + .cmp(other, op, span) } else { Self::BinaryOp( left, op2, - Box::new(Self::BinaryOp(right, op, Box::new(other)).eval()?), + Box::new(Self::BinaryOp(right, op, Box::new(other)).eval(span)?.node), ) - .eval() + .eval(span) } } - Self::UnaryOp(..) | Self::Paren(..) => return self.eval()?.cmp(other, op), - _ => return Err(format!("Undefined operation \"{} {} {}\".", self, op, other).into()), + Self::UnaryOp(..) | Self::Paren(..) => { + return self.eval(span)?.node.cmp(other, op, span) + } + _ => { + return Err(( + format!( + "Undefined operation \"{} {} {}\".", + self.to_css_string(span)?, + op, + other.to_css_string(span)? + ), + span, + ) + .into()) + } }; - match op { + Ok(match op { Op::GreaterThan => match ordering { - Ordering::Greater => Ok(Self::True), - Ordering::Less | Ordering::Equal => Ok(Self::False), + Ordering::Greater => Self::True, + Ordering::Less | Ordering::Equal => Self::False, }, Op::GreaterThanEqual => match ordering { - Ordering::Greater | Ordering::Equal => Ok(Self::True), - Ordering::Less => Ok(Self::False), + Ordering::Greater | Ordering::Equal => Self::True, + Ordering::Less => Self::False, }, Op::LessThan => match ordering { - Ordering::Less => Ok(Self::True), - Ordering::Greater | Ordering::Equal => Ok(Self::False), + Ordering::Less => Self::True, + Ordering::Greater | Ordering::Equal => Self::False, }, Op::LessThanEqual => match ordering { - Ordering::Less | Ordering::Equal => Ok(Self::True), - Ordering::Greater => Ok(Self::False), + Ordering::Less | Ordering::Equal => Self::True, + Ordering::Greater => Self::False, }, _ => unreachable!(), } + .span(span)) } } diff --git a/src/value/ops.rs b/src/value/ops.rs index c155957..9b7fc48 100644 --- a/src/value/ops.rs +++ b/src/value/ops.rs @@ -1,35 +1,45 @@ -use std::ops::{Add, Div, Mul, Neg, Rem, Sub}; +use codemap::Span; use crate::common::{Op, QuoteKind}; use crate::error::SassResult; use crate::unit::{Unit, UNIT_CONVERSION_TABLE}; use crate::value::Value; -impl Add for Value { - type Output = SassResult; - - fn add(self, mut other: Self) -> Self::Output { +impl Value { + pub fn add(self, mut other: Self, span: Span) -> SassResult { if let Self::Paren(..) = other { - other = other.eval()? + other = other.eval(span)?.node } let precedence = Op::Plus.precedence(); Ok(match self { Self::Function(..) | Self::ArgList(..) | Self::Map(..) => todo!(), Self::Important | Self::True | Self::False => match other { Self::Ident(s, QuoteKind::Double) | Self::Ident(s, QuoteKind::Single) => { - Value::Ident(format!("{}{}", self, s), QuoteKind::Double) + Value::Ident( + format!("{}{}", self.to_css_string(span)?, s), + QuoteKind::Double, + ) } - Self::Null => Value::Ident(self.to_string(), QuoteKind::None), - _ => Value::Ident(format!("{}{}", self, other), QuoteKind::None), + Self::Null => Value::Ident(self.to_css_string(span)?, QuoteKind::None), + _ => Value::Ident( + format!( + "{}{}", + self.to_css_string(span)?, + other.to_css_string(span)? + ), + QuoteKind::None, + ), }, Self::Null => match other { Self::Null => Self::Null, - _ => Value::Ident(format!("{}", other), QuoteKind::None), + _ => Value::Ident(format!("{}", other.to_css_string(span)?), QuoteKind::None), }, Self::Dimension(num, unit) => match other { Self::Dimension(num2, unit2) => { if !unit.comparable(&unit2) { - return Err(format!("Incompatible units {} and {}.", unit2, unit).into()); + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); } if unit == unit2 { Value::Dimension(num + num2, unit) @@ -48,61 +58,98 @@ impl Add for Value { } Self::Ident(s, q) => Value::Ident(format!("{}{}{}", num, unit, s), q.normalize()), Self::Null => Value::Ident(format!("{}{}", num, unit), QuoteKind::None), - Self::List(..) => { - Value::Ident(format!("{}{}{}", num, unit, other), QuoteKind::None) - } + Self::List(..) => Value::Ident( + format!("{}{}{}", num, unit, other.to_css_string(span)?), + QuoteKind::None, + ), _ => { - return Err( - format!("Undefined operation \"{}{} + {}\".", num, unit, other).into(), + return Err(( + format!( + "Undefined operation \"{}{} + {}\".", + num, + unit, + other.to_css_string(span)? + ), + span, ) + .into()) } }, Self::Color(c) => match other { Self::Ident(s, q) => Value::Ident(format!("{}{}", c, s), q.normalize()), Self::Null => Value::Ident(c.to_string(), QuoteKind::None), - Self::List(..) => Value::Ident(format!("{}{}", c, other), QuoteKind::None), - _ => return Err(format!("Undefined operation \"{} + {}\".", c, other).into()), + Self::List(..) => Value::Ident( + format!("{}{}", c, other.to_css_string(span)?), + QuoteKind::None, + ), + _ => { + return Err(( + format!( + "Undefined operation \"{} + {}\".", + c, + other.to_css_string(span)? + ), + span, + ) + .into()) + } }, Self::BinaryOp(left, op, right) => { if op.precedence() >= precedence { - (Self::BinaryOp(left, op, right).eval()? + other)? + Self::BinaryOp(left, op, right) + .eval(span)? + .node + .add(other, span)? } else { Self::BinaryOp( left, op, - Box::new(Self::BinaryOp(right, Op::Plus, Box::new(other)).eval()?), + Box::new( + Self::BinaryOp(right, Op::Plus, Box::new(other)) + .eval(span)? + .node, + ), ) - .eval()? + .eval(span)? + .node } } - Self::UnaryOp(..) | Self::Paren(..) => (self.eval()? + other)?, + Self::UnaryOp(..) | Self::Paren(..) => self.eval(span)?.node.add(other, span)?, Self::Ident(s1, quotes1) => match other { Self::Ident(s2, _) => Value::Ident(format!("{}{}", s1, s2), quotes1.normalize()), - Self::Important | Self::True | Self::False | Self::Dimension(..) => { - Value::Ident(format!("{}{}", s1, other), quotes1.normalize()) - } + Self::Important | Self::True | Self::False | Self::Dimension(..) => Value::Ident( + format!("{}{}", s1, other.to_css_string(span)?), + quotes1.normalize(), + ), Self::Null => Value::Ident(s1, quotes1.normalize()), Self::Color(c) => Value::Ident(format!("{}{}", s1, c), quotes1.normalize()), - Self::List(..) => Value::Ident(format!("{}{}", s1, other), quotes1), + Self::List(..) => { + Value::Ident(format!("{}{}", s1, other.to_css_string(span)?), quotes1) + } Self::UnaryOp(..) | Self::BinaryOp(..) => todo!(), - Self::Paren(..) => (Self::Ident(s1, quotes1) + other.eval()?)?, + Self::Paren(..) => Self::Ident(s1, quotes1).add(other.eval(span)?.node, span)?, Self::Function(..) | Self::ArgList(..) | Self::Map(..) => todo!(), }, Self::List(..) => match other { - Self::Ident(s, q) => Value::Ident(format!("{}{}", self, s), q.normalize()), - Self::Paren(..) => (self + other.eval()?)?, - _ => Value::Ident(format!("{}{}", self, other), QuoteKind::None), + Self::Ident(s, q) => { + Value::Ident(format!("{}{}", self.to_css_string(span)?, s), q.normalize()) + } + Self::Paren(..) => (self.add(other.eval(span)?.node, span))?, + _ => Value::Ident( + format!( + "{}{}", + self.to_css_string(span)?, + other.to_css_string(span)? + ), + QuoteKind::None, + ), }, }) } -} -impl Sub for Value { - type Output = SassResult; - - fn sub(self, mut other: Self) -> Self::Output { + pub fn sub(self, mut other: Self, span: Span) -> SassResult { if let Self::Paren(..) = other { - other = other.eval()? + other = other.eval(span)?.node } let precedence = Op::Mul.precedence(); Ok(match self { @@ -110,7 +157,9 @@ impl Sub for Value { Self::Dimension(num, unit) => match other { Self::Dimension(num2, unit2) => { if !unit.comparable(&unit2) { - return Err(format!("Incompatible units {} and {}.", unit2, unit).into()); + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); } if unit == unit2 { Value::Dimension(num - num2, unit) @@ -127,12 +176,14 @@ impl Sub for Value { ) } } - Self::List(..) => { - Value::Ident(format!("{}{}-{}", num, unit, other), QuoteKind::None) - } - Self::Ident(..) => { - Value::Ident(format!("{}{}-{}", num, unit, other), QuoteKind::None) - } + Self::List(..) => Value::Ident( + format!("{}{}-{}", num, unit, other.to_css_string(span)?), + QuoteKind::None, + ), + Self::Ident(..) => Value::Ident( + format!("{}{}-{}", num, unit, other.to_css_string(span)?), + QuoteKind::None, + ), _ => todo!(), }, Self::Color(c) => match other { @@ -142,23 +193,42 @@ impl Sub for Value { ), Self::Null => Value::Ident(format!("{}-", c), QuoteKind::None), Self::Dimension(..) | Self::Color(..) => { - return Err(format!("Undefined operation \"{} - {}\".", c, other).into()) + return Err(( + format!( + "Undefined operation \"{} - {}\".", + c, + other.to_css_string(span)? + ), + span, + ) + .into()) } - _ => Value::Ident(format!("{}-{}", c, other), QuoteKind::None), + _ => Value::Ident( + format!("{}-{}", c, other.to_css_string(span)?), + QuoteKind::None, + ), }, Self::BinaryOp(left, op, right) => { if op.precedence() >= precedence { - (Self::BinaryOp(left, op, right).eval()? - other)? + Self::BinaryOp(left, op, right) + .eval(span)? + .node + .sub(other, span)? } else { Self::BinaryOp( left, op, - Box::new(Self::BinaryOp(right, Op::Minus, Box::new(other)).eval()?), + Box::new( + Self::BinaryOp(right, Op::Minus, Box::new(other)) + .eval(span)? + .node, + ), ) - .eval()? + .eval(span)? + .node } } - Self::Paren(..) => (self.eval()? - other)?, + Self::Paren(..) => self.eval(span)?.node.sub(other, span)?, Self::Ident(s1, q1) => match other { Self::Ident(s2, q2) => Value::Ident( format!( @@ -177,7 +247,13 @@ impl Sub for Value { | Self::False | Self::Dimension(..) | Self::Color(..) => Value::Ident( - format!("{}{}{}-{}", q1.normalize(), s1, q1.normalize(), other), + format!( + "{}{}{}-{}", + q1.normalize(), + s1, + q1.normalize(), + other.to_css_string(span)? + ), QuoteKind::None, ), Self::Null => Value::Ident( @@ -185,37 +261,66 @@ impl Sub for Value { QuoteKind::None, ), Self::List(..) => Value::Ident( - format!("{}{}{}-{}", q1.normalize(), s1, q1.normalize(), other), + format!( + "{}{}{}-{}", + q1.normalize(), + s1, + q1.normalize(), + other.to_css_string(span)? + ), QuoteKind::None, ), _ => todo!(), }, Self::List(..) => match other { Self::Ident(s, q) => Value::Ident( - format!("{}-{}{}{}", self, q.normalize(), s, q.normalize()), + format!( + "{}-{}{}{}", + self.to_css_string(span)?, + q.normalize(), + s, + q.normalize() + ), + QuoteKind::None, + ), + _ => Value::Ident( + format!( + "{}-{}", + self.to_css_string(span)?, + other.to_css_string(span)? + ), QuoteKind::None, ), - Self::Paren(..) => (self - other.eval()?)?, - _ => Value::Ident(format!("{}-{}", self, other), QuoteKind::None), }, _ => match other { Self::Ident(s, q) => Value::Ident( - format!("{}-{}{}{}", self, q.normalize(), s, q.normalize()), + format!( + "{}-{}{}{}", + self.to_css_string(span)?, + q.normalize(), + s, + q.normalize() + ), + QuoteKind::None, + ), + Self::Null => { + Value::Ident(format!("{}-", self.to_css_string(span)?), QuoteKind::None) + } + _ => Value::Ident( + format!( + "{}-{}", + self.to_css_string(span)?, + other.to_css_string(span)? + ), QuoteKind::None, ), - Self::Null => Value::Ident(format!("{}-", self), QuoteKind::None), - _ => Value::Ident(format!("{}-{}", self, other), QuoteKind::None), }, }) } -} -impl Mul for Value { - type Output = SassResult; - - fn mul(self, mut other: Self) -> Self::Output { + pub fn mul(self, mut other: Self, span: Span) -> SassResult { if let Self::Paren(..) = other { - other = other.eval()? + other = other.eval(span)?.node } let precedence = Op::Mul.precedence(); Ok(match self { @@ -238,40 +343,63 @@ impl Mul for Value { } } _ => { - return Err( - format!("Undefined operation \"{}{} * {}\".", num, unit, other).into(), + return Err(( + format!( + "Undefined operation \"{}{} * {}\".", + num, + unit, + other.to_css_string(span)? + ), + span, ) + .into()) } }, Self::BinaryOp(left, op, right) => { if op.precedence() >= precedence { - (Self::BinaryOp(left, op, right).eval()? * other)? + Self::BinaryOp(left, op, right) + .eval(span)? + .node + .mul(other, span)? } else { Self::BinaryOp( left, op, - Box::new(Self::BinaryOp(right, Op::Mul, Box::new(other)).eval()?), + Box::new( + Self::BinaryOp(right, Op::Mul, Box::new(other)) + .eval(span)? + .node, + ), ) - .eval()? + .eval(span)? + .node } } - Self::UnaryOp(..) | Self::Paren(..) => (self.eval()? * other)?, - _ => return Err(format!("Undefined operation \"{} * {}\".", self, other).into()), + Self::UnaryOp(..) | Self::Paren(..) => self.eval(span)?.node.mul(other, span)?, + _ => { + return Err(( + format!( + "Undefined operation \"{} * {}\".", + self.to_css_string(span)?, + other.to_css_string(span)? + ), + span, + ) + .into()) + } }) } -} -impl Div for Value { - type Output = SassResult; - - fn div(self, other: Self) -> Self::Output { + pub fn div(self, other: Self, span: Span) -> SassResult { let precedence = Op::Div.precedence(); Ok(match self { Self::Null => todo!(), Self::Dimension(num, unit) => match other { Self::Dimension(num2, unit2) => { if !unit.comparable(&unit2) { - return Err(format!("Incompatible units {} and {}.", unit2, unit).into()); + return Err( + (format!("Incompatible units {} and {}.", unit2, unit), span).into(), + ); } if unit == unit2 { Value::Dimension(num / num2, Unit::None) @@ -293,7 +421,7 @@ impl Div for Value { QuoteKind::None, ), Self::BinaryOp(..) | Self::Paren(..) => { - (Self::Dimension(num, unit) / other.eval()?)? + Self::Dimension(num, unit).div(other.eval(span)?.node, span)? } _ => todo!(), }, @@ -304,23 +432,42 @@ impl Div for Value { ), Self::Null => Value::Ident(format!("{}/", c), QuoteKind::None), Self::Dimension(..) | Self::Color(..) => { - return Err(format!("Undefined operation \"{} / {}\".", c, other).into()) + return Err(( + format!( + "Undefined operation \"{} / {}\".", + c, + other.to_css_string(span)? + ), + span, + ) + .into()) } - _ => Value::Ident(format!("{}/{}", c, other), QuoteKind::None), + _ => Value::Ident( + format!("{}/{}", c, other.to_css_string(span)?), + QuoteKind::None, + ), }, Self::BinaryOp(left, op, right) => { if op.precedence() >= precedence { - (Self::BinaryOp(left, op, right).eval()? / other)? + Self::BinaryOp(left, op, right) + .eval(span)? + .node + .div(other, span)? } else { Self::BinaryOp( left, op, - Box::new(Self::BinaryOp(right, Op::Div, Box::new(other)).eval()?), + Box::new( + Self::BinaryOp(right, Op::Div, Box::new(other)) + .eval(span)? + .node, + ), ) - .eval()? + .eval(span)? + .node } } - Self::Paren(..) => (self.eval()? / other)?, + Self::Paren(..) => self.eval(span)?.node.div(other, span)?, Self::Ident(s1, q1) => match other { Self::Ident(s2, q2) => Value::Ident( format!( @@ -339,7 +486,13 @@ impl Div for Value { | Self::False | Self::Dimension(..) | Self::Color(..) => Value::Ident( - format!("{}{}{}/{}", q1.normalize(), s1, q1.normalize(), other), + format!( + "{}{}{}/{}", + q1.normalize(), + s1, + q1.normalize(), + other.to_css_string(span)? + ), QuoteKind::None, ), Self::Null => Value::Ident( @@ -350,25 +503,36 @@ impl Div for Value { }, _ => match other { Self::Ident(s, q) => Value::Ident( - format!("{}/{}{}{}", self, q.normalize(), s, q.normalize()), + format!( + "{}/{}{}{}", + self.to_css_string(span)?, + q.normalize(), + s, + q.normalize() + ), + QuoteKind::None, + ), + Self::Null => { + Value::Ident(format!("{}/", self.to_css_string(span)?), QuoteKind::None) + } + _ => Value::Ident( + format!( + "{}/{}", + self.to_css_string(span)?, + other.to_css_string(span)? + ), QuoteKind::None, ), - Self::Null => Value::Ident(format!("{}/", self), QuoteKind::None), - _ => Value::Ident(format!("{}/{}", self, other), QuoteKind::None), }, }) } -} -impl Rem for Value { - type Output = SassResult; - - fn rem(self, other: Self) -> Self::Output { + pub fn rem(self, other: Self, span: Span) -> SassResult { Ok(match self { Value::Dimension(n, u) => match other { Value::Dimension(n2, u2) => { if !u.comparable(&u2) { - return Err(format!("Incompatible units {} and {}.", u2, u).into()); + return Err((format!("Incompatible units {} and {}.", u2, u), span).into()); } if u == u2 { Value::Dimension(n % n2, u) @@ -381,26 +545,35 @@ impl Rem for Value { } } _ => { - return Err(format!( - "Undefined operation \"{} % {}\".", - Value::Dimension(n, u), - other + return Err(( + format!( + "Undefined operation \"{} % {}\".", + Value::Dimension(n, u).to_css_string(span)?, + other.to_css_string(span)? + ), + span, ) - .into()) + .into()) } }, - _ => return Err(format!("Undefined operation \"{} % {}\".", self, other).into()), + _ => { + return Err(( + format!( + "Undefined operation \"{} % {}\".", + self.to_css_string(span)?, + other.to_css_string(span)? + ), + span, + ) + .into()) + } }) } -} -impl Neg for Value { - type Output = SassResult; - - fn neg(self) -> Self::Output { - Ok(match self.eval()? { + pub fn neg(self, span: Span) -> SassResult { + Ok(match self.eval(span)?.node { Value::Dimension(n, u) => Value::Dimension(-n, u), - v => Value::Ident(format!("-{}", v), QuoteKind::None), + v => Value::Ident(format!("-{}", v.to_css_string(span)?), QuoteKind::None), }) } } diff --git a/src/value/parse.rs b/src/value/parse.rs index f658be8..334b56f 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -6,6 +6,8 @@ use num_bigint::BigInt; use num_rational::BigRational; use num_traits::pow; +use codemap::{Span, Spanned}; + use super::css_function::{eat_calc_args, eat_progid}; use crate::args::eat_call_args; @@ -31,11 +33,12 @@ fn parse_hex>( toks: &mut Peekable, scope: &Scope, super_selector: &Selector, -) -> SassResult { + mut span: Span, +) -> SassResult> { let mut s = String::with_capacity(8); if toks .peek() - .ok_or("Expected identifier.")? + .ok_or(("Expected identifier.", span))? .kind .is_ascii_digit() { @@ -43,93 +46,75 @@ fn parse_hex>( if !c.kind.is_ascii_hexdigit() || s.len() == 8 { break; } - s.push(toks.next().unwrap().kind); + let tok = toks.next().unwrap(); + span = span.merge(tok.pos()); + s.push(tok.kind); } } else { let i = eat_ident(toks, scope, super_selector)?; - if i.chars().all(|c| c.is_ascii_hexdigit()) { - s = i; + if i.node.chars().all(|c| c.is_ascii_hexdigit()) { + s = i.node; + span = span.merge(i.span); } else { - return Ok(Value::Ident(format!("#{}", i), QuoteKind::None)); + return Ok(Spanned { + node: Value::Ident(format!("#{}", i.node), QuoteKind::None), + span: i.span, + }); } } match s.len() { 3 => { let v = match u16::from_str_radix(&s, 16) { Ok(a) => a, - Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None)), + Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None).span(span)), }; let red = (((v & 0xf00) >> 8) * 0x11) as u8; let green = (((v & 0x0f0) >> 4) * 0x11) as u8; let blue = ((v & 0x00f) * 0x11) as u8; - Ok(Value::Color(Color::new( - red, - green, - blue, - 1, - format!("#{}", s), - ))) + Ok(Value::Color(Color::new(red, green, blue, 1, format!("#{}", s))).span(span)) } 4 => { let v = match u16::from_str_radix(&s, 16) { Ok(a) => a, - Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None)), + Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None).span(span)), }; let red = (((v & 0xf000) >> 12) * 0x11) as u8; let green = (((v & 0x0f00) >> 8) * 0x11) as u8; let blue = (((v & 0x00f0) >> 4) * 0x11) as u8; let alpha = ((v & 0x000f) * 0x11) as u8; - Ok(Value::Color(Color::new( - red, - green, - blue, - alpha, - format!("#{}", s), - ))) + Ok(Value::Color(Color::new(red, green, blue, alpha, format!("#{}", s))).span(span)) } 6 => { let v = match u32::from_str_radix(&s, 16) { Ok(a) => a, - Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None)), + Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None).span(span)), }; let red = ((v & 0x00ff_0000) >> 16) as u8; let green = ((v & 0x0000_ff00) >> 8) as u8; let blue = (v & 0x0000_00ff) as u8; - Ok(Value::Color(Color::new( - red, - green, - blue, - 1, - format!("#{}", s), - ))) + Ok(Value::Color(Color::new(red, green, blue, 1, format!("#{}", s))).span(span)) } 8 => { let v = match u32::from_str_radix(&s, 16) { Ok(a) => a, - Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None)), + Err(_) => return Ok(Value::Ident(format!("#{}", s), QuoteKind::None).span(span)), }; let red = ((v & 0xff00_0000) >> 24) as u8; let green = ((v & 0x00ff_0000) >> 16) as u8; let blue = ((v & 0x0000_ff00) >> 8) as u8; let alpha = (v & 0x0000_00ff) as u8; - Ok(Value::Color(Color::new( - red, - green, - blue, - alpha, - format!("#{}", s), - ))) + Ok(Value::Color(Color::new(red, green, blue, alpha, format!("#{}", s))).span(span)) } - _ => Err("Expected hex digit.".into()), + _ => Err(("Expected hex digit.", span).into()), } } #[derive(Clone, Debug, Eq, PartialEq)] enum IntermediateValue { - Value(Value), - Op(Op), - Bracketed(Vec), - Paren(Vec), + Value(Spanned), + Op(Spanned), + Bracketed(Spanned>), + Paren(Spanned>), Comma, Whitespace, } @@ -144,52 +129,61 @@ impl IsWhitespace for IntermediateValue { } fn parse_paren( - t: Vec, + t: Spanned>, scope: &Scope, super_selector: &Selector, - space_separated: &mut Vec, + space_separated: &mut Vec>, ) -> SassResult<()> { if t.is_empty() { - space_separated.push(Value::List( - Vec::new(), - ListSeparator::Space, - Brackets::None, - )); + space_separated + .push(Value::List(Vec::new(), ListSeparator::Space, Brackets::None).span(t.span)); return Ok(()); } - let paren_toks = &mut t.into_iter().peekable(); + let paren_toks = &mut t.node.into_iter().peekable(); let mut map = SassMap::new(); let key = Value::from_vec(read_until_char(paren_toks, ':'), scope, super_selector)?; if paren_toks.peek().is_none() { - space_separated.push(Value::Paren(Box::new(key))); + space_separated.push(Spanned { + node: Value::Paren(Box::new(key.node)), + span: key.span, + }); return Ok(()); } let val = Value::from_vec(read_until_char(paren_toks, ','), scope, super_selector)?; - map.insert(key, val); + map.insert(key.node, val.node); if paren_toks.peek().is_none() { - space_separated.push(Value::Map(map)); + space_separated.push(Spanned { + node: Value::Map(map), + span: key.span.merge(val.span), + }); return Ok(()); } + let mut span = key.span; + loop { let key = Value::from_vec(read_until_char(paren_toks, ':'), scope, super_selector)?; devour_whitespace(paren_toks); let val = Value::from_vec(read_until_char(paren_toks, ','), scope, super_selector)?; + span = span.merge(val.span); devour_whitespace(paren_toks); - if map.insert(key, val) { - return Err("Duplicate key.".into()); + if map.insert(key.node, val.node) { + return Err(("Duplicate key.", key.span).into()); } if paren_toks.peek().is_none() { break; } } - space_separated.push(Value::Map(map)); + space_separated.push(Spanned { + node: Value::Map(map), + span, + }); Ok(()) } @@ -197,58 +191,82 @@ fn eat_op>( iter: &mut Peekable, scope: &Scope, super_selector: &Selector, - op: Op, - space_separated: &mut Vec, + op: Spanned, + space_separated: &mut Vec>, ) -> SassResult<()> { - match op { + match op.node { Op::Not => { devour_whitespace(iter); - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::UnaryOp(op, Box::new(right))); + let right = single_value(iter, scope, super_selector, op.span)?; + space_separated.push(Spanned { + node: Value::UnaryOp(op.node, Box::new(right.node)), + span: right.span, + }); } Op::Plus => { if let Some(left) = space_separated.pop() { devour_whitespace(iter); - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::BinaryOp(Box::new(left), op, Box::new(right))); + let right = single_value(iter, scope, super_selector, op.span)?; + space_separated.push(Spanned { + node: Value::BinaryOp(Box::new(left.node), op.node, Box::new(right.node)), + span: left.span.merge(right.span), + }); } else { devour_whitespace(iter); - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::UnaryOp(op, Box::new(right))); + let right = single_value(iter, scope, super_selector, op.span)?; + space_separated.push(Spanned { + node: Value::UnaryOp(op.node, Box::new(right.node)), + span: right.span, + }); } } Op::Minus => { if devour_whitespace(iter) { - let right = single_value(iter, scope, super_selector)?; + let right = single_value(iter, scope, super_selector, op.span)?; if let Some(left) = space_separated.pop() { - space_separated.push(Value::BinaryOp(Box::new(left), op, Box::new(right))); + space_separated.push(Spanned { + node: Value::BinaryOp(Box::new(left.node), op.node, Box::new(right.node)), + span: left.span.merge(right.span), + }); } else { - space_separated.push(Value::UnaryOp(op, Box::new(right))); + space_separated.push(Spanned { + node: Value::UnaryOp(op.node, Box::new(right.node)), + span: right.span, + }); } } else { - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::UnaryOp(op, Box::new(right))); + let right = single_value(iter, scope, super_selector, op.span)?; + space_separated.push(Spanned { + node: Value::UnaryOp(op.node, Box::new(right.node)), + span: right.span, + }); } } Op::And | Op::Or => { devour_whitespace(iter); if iter.peek().is_none() { - space_separated.push(Value::Ident(op.to_string(), QuoteKind::None)); + space_separated.push(Value::Ident(op.to_string(), QuoteKind::None).span(op.span)); } else if let Some(left) = space_separated.pop() { devour_whitespace(iter); - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::BinaryOp(Box::new(left), op, Box::new(right))); + let right = single_value(iter, scope, super_selector, left.span)?; + space_separated.push( + Value::BinaryOp(Box::new(left.node), op.node, Box::new(right.node)) + .span(left.span.merge(right.span)), + ); } else { - return Err("Expected expression.".into()); + return Err(("Expected expression.", op.span).into()); } } _ => { if let Some(left) = space_separated.pop() { devour_whitespace(iter); - let right = single_value(iter, scope, super_selector)?; - space_separated.push(Value::BinaryOp(Box::new(left), op, Box::new(right))); + let right = single_value(iter, scope, super_selector, left.span)?; + space_separated.push( + Value::BinaryOp(Box::new(left.node), op.node, Box::new(right.node)) + .span(left.span.merge(right.span)), + ); } else { - return Err("Expected expression.".into()); + return Err(("Expected expression.", op.span).into()); } } } @@ -259,28 +277,45 @@ fn single_value>( iter: &mut Peekable, scope: &Scope, super_selector: &Selector, -) -> SassResult { - Ok(match iter.next().ok_or("Expected expression.")? { + span: Span, +) -> SassResult> { + Ok(match iter.next().ok_or(("Expected expression.", span))? { IntermediateValue::Value(v) => v, - IntermediateValue::Op(op) => match op { + IntermediateValue::Op(op) => match op.node { Op::Minus => { devour_whitespace(iter); - (-single_value(iter, scope, super_selector)?)? + let val = single_value(iter, scope, super_selector, span)?; + Spanned { + node: val.node.neg(val.span)?, + span: op.span.merge(val.span), + } } Op::Not => { devour_whitespace(iter); - Value::UnaryOp(op, Box::new(single_value(iter, scope, super_selector)?)) + let val = single_value(iter, scope, super_selector, span)?; + Spanned { + node: Value::UnaryOp(op.node, Box::new(val.node)), + span: op.span.merge(val.span), + } } _ => todo!(), }, IntermediateValue::Whitespace => unreachable!(), - IntermediateValue::Comma => return Err("Expected expression.".into()), - IntermediateValue::Bracketed(t) => match Value::from_vec(t, scope, super_selector)? { - Value::List(v, sep, Brackets::None) => Value::List(v, sep, Brackets::Bracketed), - v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed), - }, + IntermediateValue::Comma => return Err(("Expected expression.", span).into()), + IntermediateValue::Bracketed(t) => { + let v = Value::from_vec(t.node, scope, super_selector)?; + match v.node { + Value::List(v, sep, Brackets::None) => Value::List(v, sep, Brackets::Bracketed), + v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed), + } + .span(v.span) + } IntermediateValue::Paren(t) => { - Value::Paren(Box::new(Value::from_vec(t, scope, super_selector)?)) + let val = Value::from_vec(t.node, scope, super_selector)?; + Spanned { + node: Value::Paren(Box::new(val.node)), + span: val.span, + } } }) } @@ -290,8 +325,12 @@ impl Value { toks: &mut Peekable, scope: &Scope, super_selector: &Selector, - ) -> SassResult { + ) -> SassResult> { let mut intermediate_values = Vec::new(); + let span = match toks.peek() { + Some(Token { pos, .. }) => *pos, + None => todo!("Expected expression."), + }; while toks.peek().is_some() { intermediate_values.push(Self::parse_intermediate_value(toks, scope, super_selector)?); } @@ -309,20 +348,40 @@ impl Value { if space_separated.len() == 1 { comma_separated.push(space_separated.pop().unwrap()); } else { - comma_separated.push(Value::List( - mem::take(&mut space_separated), - ListSeparator::Space, - Brackets::None, - )); + let mut span = space_separated[0].span; + comma_separated.push( + Value::List( + mem::take(&mut space_separated) + .into_iter() + .map(|a| { + span = span.merge(a.span); + a.node + }) + .collect(), + ListSeparator::Space, + Brackets::None, + ) + .span(span), + ); } } IntermediateValue::Bracketed(t) => { - space_separated.push(match Value::from_vec(t, scope, super_selector)? { - Value::List(v, sep, Brackets::None) => { - Value::List(v, sep, Brackets::Bracketed) - } - v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed), - }) + if t.node.is_empty() { + space_separated.push( + Value::List(Vec::new(), ListSeparator::Space, Brackets::Bracketed) + .span(t.span), + ); + continue; + } + space_separated.push( + match Value::from_vec(t.node, scope, super_selector)?.node { + Value::List(v, sep, Brackets::None) => { + Value::List(v, sep, Brackets::Bracketed).span(t.span) + } + v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed) + .span(t.span), + }, + ) } IntermediateValue::Paren(t) => { parse_paren(t, scope, super_selector, &mut space_separated)?; @@ -334,17 +393,30 @@ impl Value { if space_separated.len() == 1 { comma_separated.push(space_separated.pop().unwrap()); } else if !space_separated.is_empty() { - comma_separated.push(Value::List( - space_separated, - ListSeparator::Space, - Brackets::None, - )); + comma_separated.push( + Value::List( + space_separated.into_iter().map(|a| a.node).collect(), + ListSeparator::Space, + Brackets::None, + ) + .span(span), + ); } - Value::List(comma_separated, ListSeparator::Comma, Brackets::None) + Value::List( + comma_separated.into_iter().map(|a| a.node).collect(), + ListSeparator::Comma, + Brackets::None, + ) + .span(span) } else if space_separated.len() == 1 { space_separated.pop().unwrap() } else { - Value::List(space_separated, ListSeparator::Space, Brackets::None) + Value::List( + space_separated.into_iter().map(|a| a.node).collect(), + ListSeparator::Space, + Brackets::None, + ) + .span(span) }) } @@ -352,7 +424,7 @@ impl Value { toks: Vec, scope: &Scope, super_selector: &Selector, - ) -> SassResult { + ) -> SassResult> { Self::from_tokens(&mut toks.into_iter().peekable(), scope, super_selector) } @@ -361,25 +433,34 @@ impl Value { scope: &Scope, super_selector: &Selector, ) -> SassResult { - let mut s = eat_ident(toks, scope, super_selector)?; + let Spanned { node: mut s, span } = eat_ident(toks, scope, super_selector)?; if s == "progid" && toks.peek().is_some() && toks.peek().unwrap().kind == ':' { toks.next(); s.push(':'); s.push_str(&eat_progid(toks, scope, super_selector)?); - return Ok(IntermediateValue::Value(Value::Ident(s, QuoteKind::None))); + return Ok(IntermediateValue::Value(Spanned { + node: Value::Ident(s, QuoteKind::None), + span, + })); } match toks.peek() { Some(Token { kind: '(', .. }) => { toks.next(); - let func = match scope.get_fn(&s) { + let func = match scope.get_fn(Spanned { + node: s.clone(), + span, + }) { Ok(f) => f, Err(_) => match GLOBAL_FUNCTIONS.get(&s) { Some(f) => { - return Ok(IntermediateValue::Value(f.0( - eat_call_args(toks, scope, super_selector)?, - scope, - super_selector, - )?)) + return Ok(IntermediateValue::Value(Spanned { + node: f.0( + eat_call_args(toks, scope, super_selector)?, + scope, + super_selector, + )?, + span, + })) } None => { match s.as_str() { @@ -394,7 +475,10 @@ impl Value { .to_css_string(scope, super_selector)?, ), } - return Ok(IntermediateValue::Value(Value::Ident(s, QuoteKind::None))); + return Ok(IntermediateValue::Value(Spanned { + node: Value::Ident(s, QuoteKind::None), + span, + })); } }, }; @@ -405,22 +489,35 @@ impl Value { scope, super_selector, )? - .call(super_selector, func.body())?, + .call(super_selector, func.body())? + .span(span), )) } _ => { if let Ok(c) = crate::color::ColorName::try_from(s.as_ref()) { - Ok(IntermediateValue::Value(Value::Color(c.into_color(s)))) + Ok(IntermediateValue::Value(Spanned { + node: Value::Color(c.into_color(s)), + span, + })) } else { - match s.to_ascii_lowercase().as_str() { - "true" => Ok(IntermediateValue::Value(Value::True)), - "false" => Ok(IntermediateValue::Value(Value::False)), - "null" => Ok(IntermediateValue::Value(Value::Null)), - "not" => Ok(IntermediateValue::Op(Op::Not)), - "and" => Ok(IntermediateValue::Op(Op::And)), - "or" => Ok(IntermediateValue::Op(Op::Or)), - _ => Ok(IntermediateValue::Value(Value::Ident(s, QuoteKind::None))), - } + Ok(match s.to_ascii_lowercase().as_str() { + "true" => IntermediateValue::Value(Value::True.span(span)), + "false" => IntermediateValue::Value(Value::False.span(span)), + "null" => IntermediateValue::Value(Value::Null.span(span)), + "not" => IntermediateValue::Op(Spanned { + node: Op::Not, + span, + }), + "and" => IntermediateValue::Op(Spanned { + node: Op::And, + span, + }), + "or" => IntermediateValue::Op(Spanned { node: Op::Or, span }), + _ => IntermediateValue::Value(Spanned { + node: Value::Ident(s, QuoteKind::None), + span, + }), + }) } } } @@ -434,8 +531,8 @@ impl Value { if devour_whitespace(toks) { return Ok(IntermediateValue::Whitespace); } - let kind = match toks.peek() { - Some(v) => v.kind, + let (kind, span) = match toks.peek() { + Some(v) => (v.kind, v.pos()), None => panic!("unexpected eof"), }; match kind { @@ -444,14 +541,19 @@ impl Value { Ok(IntermediateValue::Comma) } '0'..='9' | '.' => { - let val = eat_number(toks)?; + let Spanned { + node: val, + mut span, + } = eat_number(toks)?; let unit = if let Some(tok) = toks.peek() { match tok.kind { 'a'..='z' | 'A'..='Z' | '_' => { - Unit::from(&eat_ident(toks, scope, super_selector)?) + let u = eat_ident(toks, scope, super_selector)?; + span = span.merge(u.span); + Unit::from(&u.node) } '%' => { - toks.next(); + span = span.merge(toks.next().unwrap().pos()); Unit::Percent } _ => Unit::None, @@ -479,35 +581,42 @@ impl Value { } BigRational::new(num.parse().unwrap(), pow(BigInt::from(10), num_dec)) }; - Ok(IntermediateValue::Value(Value::Dimension( - Number::new(n), - unit, - ))) + Ok(IntermediateValue::Value( + Value::Dimension(Number::new(n), unit).span(span), + )) } '(' => { - toks.next(); + let mut span = toks.next().unwrap().pos(); let mut inner = read_until_closing_paren(toks); // todo: the above shouldn't eat the closing paren - if !inner.is_empty() && inner.pop().unwrap().kind != ')' { - return Err("expected \")\".".into()); + if !inner.is_empty() { + let last_tok = inner.pop().unwrap(); + if last_tok.kind != ')' { + return Err(("expected \")\".", span).into()); + } + span = span.merge(last_tok.pos()); } - Ok(IntermediateValue::Paren(inner)) + Ok(IntermediateValue::Paren(Spanned { node: inner, span })) } '&' => { - toks.next(); - Ok(IntermediateValue::Value(Value::Ident( - super_selector.to_string(), - QuoteKind::None, - ))) + let span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Value(Spanned { + node: Value::Ident(super_selector.to_string(), QuoteKind::None), + span, + })) } '#' => { if let Ok(s) = eat_ident(toks, scope, super_selector) { - Ok(IntermediateValue::Value(Value::Ident(s, QuoteKind::None))) + Ok(IntermediateValue::Value(Spanned { + node: Value::Ident(s.node, QuoteKind::None), + span: s.span, + })) } else { Ok(IntermediateValue::Value(parse_hex( toks, scope, super_selector, + span, )?)) } } @@ -519,86 +628,119 @@ impl Value { Self::ident(toks, scope, super_selector) } q @ '"' | q @ '\'' => { - toks.next(); - Ok(IntermediateValue::Value(parse_quoted_string( - toks, - scope, - q, - super_selector, - )?)) + let span_start = toks.next().unwrap().pos(); + let Spanned { node, span } = parse_quoted_string(toks, scope, q, super_selector)?; + Ok(IntermediateValue::Value(Spanned { + node, + span: span_start.merge(span), + })) } '[' => { - toks.next(); + let mut span = toks.next().unwrap().pos(); let mut inner = read_until_closing_square_brace(toks); - inner.pop(); - Ok(IntermediateValue::Bracketed(inner)) + if !inner.is_empty() { + let last_tok = inner.pop().unwrap(); + if last_tok.kind != ']' { + return Err(("expected \"]\".", span).into()); + } + span = span.merge(last_tok.pos()); + } + Ok(IntermediateValue::Bracketed(Spanned { node: inner, span })) } '$' => { toks.next(); - Ok(IntermediateValue::Value( - scope.get_var(&eat_ident_no_interpolation(toks)?)?, - )) + let val = eat_ident_no_interpolation(toks)?; + Ok(IntermediateValue::Value(Spanned { + node: scope.get_var(val.clone())?.node, + span: val.span, + })) } - '@' => Err("expected \";\".".into()), + '@' => Err(("expected \";\".", span).into()), '+' => { - toks.next(); - Ok(IntermediateValue::Op(Op::Plus)) + let span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Op(Spanned { + node: Op::Plus, + span, + })) } '-' => { - toks.next(); - Ok(IntermediateValue::Op(Op::Minus)) + let span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Op(Spanned { + node: Op::Minus, + span, + })) } '*' => { - toks.next(); - Ok(IntermediateValue::Op(Op::Mul)) + let span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Op(Spanned { + node: Op::Mul, + span, + })) } '%' => { - toks.next(); - Ok(IntermediateValue::Op(Op::Rem)) + let span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Op(Spanned { + node: Op::Rem, + span, + })) } q @ '>' | q @ '<' => { - toks.next(); - Ok(IntermediateValue::Op(if toks.peek().unwrap().kind == '=' { - toks.next(); - match q { - '>' => Op::GreaterThanEqual, - '<' => Op::LessThanEqual, - _ => unreachable!(), - } - } else { - match q { - '>' => Op::GreaterThan, - '<' => Op::LessThan, - _ => unreachable!(), - } + let mut span = toks.next().unwrap().pos(); + Ok(IntermediateValue::Op(Spanned { + node: if toks.peek().unwrap().kind == '=' { + span = span.merge(toks.next().unwrap().pos()); + match q { + '>' => Op::GreaterThanEqual, + '<' => Op::LessThanEqual, + _ => unreachable!(), + } + } else { + match q { + '>' => Op::GreaterThan, + '<' => Op::LessThan, + _ => unreachable!(), + } + }, + span, })) } '=' => { - toks.next(); - if toks.next().unwrap().kind == '=' { - Ok(IntermediateValue::Op(Op::Equal)) + let mut span = toks.next().unwrap().pos(); + if let Token { kind: '=', pos } = toks.next().unwrap() { + span = span.merge(pos); + Ok(IntermediateValue::Op(Spanned { + node: Op::Equal, + span, + })) } else { - Err("expected \"=\".".into()) + Err(("expected \"=\".", span).into()) } } '!' => { - toks.next(); + let mut span = toks.next().unwrap().pos(); if toks.peek().is_some() && toks.peek().unwrap().kind == '=' { - toks.next(); - return Ok(IntermediateValue::Op(Op::NotEqual)); + span = span.merge(toks.next().unwrap().pos()); + return Ok(IntermediateValue::Op(Spanned { + node: Op::NotEqual, + span, + })); } devour_whitespace(toks); let v = eat_ident(toks, scope, super_selector)?; - if v.to_ascii_lowercase().as_str() == "important" { - Ok(IntermediateValue::Value(Value::Important)) + span = span.merge(v.span); + if v.node.to_ascii_lowercase().as_str() == "important" { + Ok(IntermediateValue::Value(Spanned { + node: Value::Important, + span, + })) } else { - Err("Expected \"important\".".into()) + Err(("Expected \"important\".", span).into()) } } '/' => { - toks.next(); + let span = toks.next().unwrap().pos(); if toks.peek().is_none() { - return Err("Expected expression.".into()); + return Err(("Expected expression.", span).into()); } if '*' == toks.peek().unwrap().kind { toks.next(); @@ -609,11 +751,14 @@ impl Value { devour_whitespace(toks); Ok(IntermediateValue::Whitespace) } else { - Ok(IntermediateValue::Op(Op::Div)) + Ok(IntermediateValue::Op(Spanned { + node: Op::Div, + span, + })) } } - ':' | '?' | ')' => Err("expected \";\".".into()), - v if v.is_control() => Err("Expected expression.".into()), + ':' | '?' | ')' => Err(("expected \";\".", span).into()), + v if v.is_control() => Err(("Expected expression.", span).into()), v => { dbg!(v); panic!("Unexpected token in value parsing")