grass_compiler-0.13.4/.cargo_vcs_info.json0000644000000001550000000000100141330ustar { "git": { "sha1": "e0bb9e2eabfc3a58e42b03089cd7b22c68d09d0b" }, "path_in_vcs": "crates/compiler" }grass_compiler-0.13.4/Cargo.toml0000644000000027520000000000100121360ustar # THIS FILE IS AUTOMATICALLY GENERATED BY CARGO # # When uploading crates to the registry Cargo will automatically # "normalize" Cargo.toml files for maximal compatibility # with all versions of Cargo and also rewrite `path` dependencies # to registry (e.g., crates.io) dependencies. # # If you are reading this file be aware that the original Cargo.toml # will likely look very different (and much more reasonable). # See Cargo.toml.orig for the original contents. [package] edition = "2021" rust-version = "1.70" name = "grass_compiler" version = "0.13.4" authors = ["Connor Skees <39542938+ConnorSkees@users.noreply.github.com>"] description = "Internal implementation of the grass compiler" readme = "README.md" keywords = [ "scss", "sass", "css", "web", ] categories = ["web-programming"] license = "MIT" repository = "https://github.com/connorskees/grass" resolver = "1" [package.metadata.docs.rs] rustdoc-args = [ "--cfg", "doc_cfg", ] [lib] name = "grass_compiler" path = "src/lib.rs" bench = false [dependencies.codemap] version = "0.1.3" [dependencies.indexmap] version = "2" [dependencies.lasso] version = "0.7" [dependencies.once_cell] version = "1.15.0" [dependencies.phf] version = "0.11" features = ["macros"] [dependencies.rand] version = "0.8" optional = true [dependencies.wasm-bindgen] version = "0.2.68" optional = true [features] custom-builtin-fns = [] default = [ "random", "custom-builtin-fns", ] random = ["rand"] wasm-exports = ["wasm-bindgen"] grass_compiler-0.13.4/Cargo.toml.orig000064400000000000000000000030141046102023000156070ustar 00000000000000[package] name = "grass_compiler" version = "0.13.4" edition = "2021" description = "Internal implementation of the grass compiler" readme = "README.md" license = "MIT" categories = ["web-programming"] keywords = ["scss", "sass", "css", "web"] repository = "https://github.com/connorskees/grass" authors = ["Connor Skees <39542938+ConnorSkees@users.noreply.github.com>"] rust-version = "1.70" [lib] name = "grass_compiler" path = "src/lib.rs" # crate-type = ["cdylib", "rlib"] bench = false [package.metadata.docs.rs] # To build locally: # RUSTDOCFLAGS="--cfg doc_cfg" cargo +nightly doc --no-deps --open rustdoc-args = ["--cfg", "doc_cfg"] [dependencies] # todo: replace with std::cell::LazyCell (msrv 1.80.0) once_cell = "1.15.0" # todo: use xorshift for random numbers rand = { version = "0.8", optional = true } # todo: update to use asref # todo: update to expose more info (for eww) # todo: update to use text_size::TextRange codemap = "0.1.3" wasm-bindgen = { version = "0.2.68", optional = true } # todo: benchmark using phf for global functions phf = { version = "0.11", features = ["macros"] } indexmap = "2" # todo: do we really need interning for things? lasso = "0.7" [features] default = ["random", "custom-builtin-fns"] # Option (enabled by default): enable the builtin functions `random([$limit])` and `unique-id()` random = ["rand"] # Option: expose JavaScript-friendly WebAssembly exports wasm-exports = ["wasm-bindgen"] # Option: expose internals necessary to implement custom builtin functions custom-builtin-fns = [] grass_compiler-0.13.4/README.md000064400000000000000000000005571046102023000142100ustar 00000000000000# grass_compiler This crate exposes the internals of the main package, [`grass`](https://crates.io/crates/grass). For most users, the preferred crate should be `grass`, as it is more stable and has a simpler API. This crate will see frequent breaking changes. [Documentation](https://docs.rs/grass_compiler/) [crates.io](https://crates.io/crates/grass_compiler) grass_compiler-0.13.4/input.scss000064400000000000000000000000251046102023000147530ustar 00000000000000a { color: red; }grass_compiler-0.13.4/src/ast/args.rs000064400000000000000000000217011046102023000156030ustar 00000000000000use std::{ collections::{BTreeMap, BTreeSet}, iter::Iterator, mem, }; use codemap::{Span, Spanned}; use crate::{ common::{Identifier, ListSeparator}, error::SassResult, utils::to_sentence, value::Value, }; use super::AstExpr; #[derive(Debug, Clone)] pub struct Argument { pub name: Identifier, pub default: Option, } #[derive(Debug, Clone)] pub struct ArgumentDeclaration { pub args: Vec, pub rest: Option, } impl ArgumentDeclaration { pub fn empty() -> Self { Self { args: Vec::new(), rest: None, } } pub fn verify( &self, num_positional: usize, names: &BTreeMap, span: Span, ) -> SassResult<()> { let mut named_used = 0; for i in 0..self.args.len() { let argument = &self.args[i]; if i < num_positional { if names.contains_key(&argument.name) { // todo: _originalArgumentName return Err(( format!( "Argument ${} was passed both by position and by name.", argument.name ), span, ) .into()); } } else if names.contains_key(&argument.name) { named_used += 1; } else if argument.default.is_none() { // todo: _originalArgumentName return Err((format!("Missing argument ${}.", argument.name), span).into()); } } if self.rest.is_some() { return Ok(()); } if num_positional > self.args.len() { return Err(( format!( "Only {} {}{} allowed, but {num_positional} {} passed.", self.args.len(), if names.is_empty() { "" } else { "positional " }, if self.args.len() == 1 { "argument" } else { "arguments" }, if num_positional == 1 { "was" } else { "were" }, num_positional = num_positional, ), span, ) .into()); } if named_used < names.len() { let mut unknown_names = names.keys().copied().collect::>(); for arg in &self.args { unknown_names.remove(&arg.name); } if unknown_names.len() == 1 { return Err(( format!( "No argument named ${}.", unknown_names.iter().next().unwrap() ), span, ) .into()); } if unknown_names.len() > 1 { return Err(( format!( "No arguments named {}.", to_sentence( unknown_names .into_iter() .map(|name| format!("${name}", name = name)) .collect(), "or" ) ), span, ) .into()); } } Ok(()) } } #[derive(Debug, Clone)] pub struct ArgumentInvocation { pub(crate) positional: Vec, pub(crate) named: BTreeMap, pub(crate) rest: Option, pub(crate) keyword_rest: Option, pub(crate) span: Span, } impl ArgumentInvocation { pub fn empty(span: Span) -> Self { Self { positional: Vec::new(), named: BTreeMap::new(), rest: None, keyword_rest: None, span, } } } // todo: hack for builtin `call` #[derive(Debug, Clone)] pub(crate) enum MaybeEvaledArguments { Invocation(ArgumentInvocation), Evaled(ArgumentResult), } /// Function arguments that have been evaluated /// /// Arguments may be passed either positionally or by name. Positional arguments /// may not come after named ones. #[derive(Debug, Clone)] pub struct ArgumentResult { pub(crate) positional: Vec, pub(crate) named: BTreeMap, pub(crate) separator: ListSeparator, pub(crate) span: Span, // todo: hack pub(crate) touched: BTreeSet, } impl ArgumentResult { /// Get argument by name /// /// Removes the argument pub fn get_named>(&mut self, val: T) -> Option> { self.named.remove(&val.into()).map(|n| Spanned { node: n, span: self.span, }) } /// Get a positional argument by 0-indexed position /// /// Replaces argument with [`Value::Null`] gravestone pub fn get_positional(&mut self, idx: usize) -> Option> { let val = match self.positional.get_mut(idx) { Some(v) => Some(Spanned { node: mem::replace(v, Value::Null), span: self.span, }), None => None, }; self.touched.insert(idx); val } /// Get an argument by either name or position /// /// If the named argument does not exist, then the position is checked. Like /// [`ArgumentResult::get_named`] and [`ArgumentResult::get_positional`], this /// function removes the argument or replaces it with a gravestone pub fn get>(&mut self, position: usize, name: T) -> Option> { match self.get_named(name) { Some(v) => Some(v), None => self.get_positional(position), } } /// Like [`ArgumentResult::get`], but returns a result if the argument doesn't exist pub fn get_err(&mut self, position: usize, name: &str) -> SassResult { match self.get_named(name) { Some(v) => Ok(v.node), None => match self.get_positional(position) { Some(v) => Ok(v.node), None => Err((format!("Missing argument ${}.", name), self.span()).into()), }, } } pub const fn span(&self) -> Span { self.span } pub(crate) fn len(&self) -> usize { self.positional.len() + self.named.len() } /// Assert that this function has at least `min` number of args pub(crate) fn min_args(&self, min: usize) -> SassResult<()> { let len = self.len(); if len < min { let phrase = match min { 1 => "one argument", 2 => "two arguments", 3 => "three arguments", _ => todo!("min args greater than three"), }; return Err((format!("At least {phrase} must be passed."), self.span()).into()); } Ok(()) } /// Assert that this function has at most `max` number of args pub fn max_args(&self, max: usize) -> SassResult<()> { let len = self.len(); if len > max { let mut err = String::with_capacity(50); #[allow(unknown_lints, clippy::format_push_string)] err.push_str(&format!("Only {max} argument", max = max)); if max != 1 { err.push('s'); } err.push_str(" allowed, but "); err.push_str(&len.to_string()); err.push(' '); if len == 1 { err.push_str("was passed."); } else { err.push_str("were passed."); } return Err((err, self.span()).into()); } Ok(()) } /// Get an argument by name or position. If the argument does not exist, use /// the default value provided pub fn default_arg(&mut self, position: usize, name: &'static str, default: Value) -> Value { match self.get(position, name) { Some(val) => val.node, None => default, } } pub(crate) fn remove_positional(&mut self, position: usize) -> Option { if self.positional.len() > position { Some(self.positional.remove(position)) } else { None } } pub(crate) fn get_variadic(self) -> SassResult>> { if let Some((name, _)) = self.named.iter().next() { return Err((format!("No argument named ${}.", name), self.span).into()); } let Self { positional, span, touched, .. } = self; // todo: complete hack, we shouldn't have the `touched` set let args = positional .into_iter() .enumerate() .filter(|(idx, _)| !touched.contains(idx)) .map(|(_, node)| Spanned { node, span }) .collect(); Ok(args) } } grass_compiler-0.13.4/src/ast/css.rs000064400000000000000000000100571046102023000154410ustar 00000000000000use codemap::Span; use crate::selector::ExtendedSelector; use super::{MediaRule, Style, UnknownAtRule}; #[derive(Debug, Clone)] pub(crate) enum CssStmt { RuleSet { selector: ExtendedSelector, body: Vec, is_group_end: bool, }, Style(Style), Media(MediaRule, bool), UnknownAtRule(UnknownAtRule, bool), Supports(SupportsRule, bool), Comment(String, Span), KeyframesRuleSet(KeyframesRuleSet), /// A plain import such as `@import "foo.css";` or /// `@import url(https://fonts.google.com/foo?bar);` // todo: named fields, 0: url, 1: modifiers Import(String, Option), } impl CssStmt { pub fn is_style_rule(&self) -> bool { matches!(self, CssStmt::RuleSet { .. }) } pub fn set_group_end(&mut self) { match self { CssStmt::Media(_, is_group_end) | CssStmt::UnknownAtRule(_, is_group_end) | CssStmt::Supports(_, is_group_end) | CssStmt::RuleSet { is_group_end, .. } => *is_group_end = true, CssStmt::Style(_) | CssStmt::Comment(_, _) | CssStmt::KeyframesRuleSet(_) | CssStmt::Import(_, _) => {} } } pub fn is_group_end(&self) -> bool { match self { CssStmt::Media(_, is_group_end) | CssStmt::UnknownAtRule(_, is_group_end) | CssStmt::Supports(_, is_group_end) | CssStmt::RuleSet { is_group_end, .. } => *is_group_end, _ => false, } } pub fn is_invisible(&self) -> bool { match self { CssStmt::RuleSet { selector, body, .. } => { selector.is_invisible() || body.iter().all(CssStmt::is_invisible) } CssStmt::Style(style) => style.value.node.is_blank(), CssStmt::Media(media_rule, ..) => media_rule.body.iter().all(CssStmt::is_invisible), CssStmt::UnknownAtRule(..) | CssStmt::Import(..) | CssStmt::Comment(..) => false, CssStmt::Supports(supports_rule, ..) => { supports_rule.body.iter().all(CssStmt::is_invisible) } CssStmt::KeyframesRuleSet(kf) => kf.body.iter().all(CssStmt::is_invisible), } } pub fn copy_without_children(&self) -> Self { match self { CssStmt::RuleSet { selector, is_group_end, .. } => CssStmt::RuleSet { selector: selector.clone(), body: Vec::new(), is_group_end: *is_group_end, }, CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..) => unreachable!(), CssStmt::Media(media, is_group_end) => CssStmt::Media( MediaRule { query: media.query.clone(), body: Vec::new(), }, *is_group_end, ), CssStmt::UnknownAtRule(at_rule, is_group_end) => CssStmt::UnknownAtRule( UnknownAtRule { name: at_rule.name.clone(), params: at_rule.params.clone(), body: Vec::new(), has_body: at_rule.has_body, }, *is_group_end, ), CssStmt::Supports(supports, is_group_end) => CssStmt::Supports( SupportsRule { params: supports.params.clone(), body: Vec::new(), }, *is_group_end, ), CssStmt::KeyframesRuleSet(keyframes) => CssStmt::KeyframesRuleSet(KeyframesRuleSet { selector: keyframes.selector.clone(), body: Vec::new(), }), } } } #[derive(Debug, Clone)] pub(crate) struct KeyframesRuleSet { pub selector: Vec, pub body: Vec, } #[derive(Debug, Clone)] pub(crate) enum KeyframesSelector { To, From, Percent(Box), } #[derive(Debug, Clone)] pub(crate) struct SupportsRule { pub params: String, pub body: Vec, } grass_compiler-0.13.4/src/ast/expr.rs000064400000000000000000000115071046102023000156300ustar 00000000000000use std::{iter::Iterator, sync::Arc}; use codemap::{Span, Spanned}; use crate::{ color::Color, common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp}, unit::Unit, value::{CalculationName, Number}, }; use super::{ArgumentInvocation, AstSupportsCondition, Interpolation, InterpolationPart}; /// Represented by the `if` function #[derive(Debug, Clone)] pub struct Ternary(pub ArgumentInvocation); #[derive(Debug, Clone)] pub struct ListExpr { pub elems: Vec>, pub separator: ListSeparator, pub brackets: Brackets, } #[derive(Debug, Clone)] pub struct FunctionCallExpr { pub namespace: Option>, pub name: Identifier, pub arguments: Arc, pub span: Span, } #[derive(Debug, Clone)] pub struct InterpolatedFunction { pub name: Interpolation, pub arguments: ArgumentInvocation, pub span: Span, } #[derive(Debug, Clone, Default)] pub struct AstSassMap(pub Vec<(Spanned, AstExpr)>); #[derive(Debug, Clone)] pub struct BinaryOpExpr { pub lhs: AstExpr, pub op: BinaryOp, pub rhs: AstExpr, pub allows_slash: bool, pub span: Span, } #[derive(Debug, Clone)] pub enum AstExpr { BinaryOp(Arc), True, False, Calculation { name: CalculationName, args: Vec, }, Color(Arc), FunctionCall(FunctionCallExpr), If(Arc), InterpolatedFunction(Arc), List(ListExpr), Map(AstSassMap), Null, Number { n: Number, unit: Unit, }, Paren(Arc), ParentSelector, String(StringExpr, Span), Supports(Arc), UnaryOp(UnaryOp, Arc, Span), Variable { name: Spanned, namespace: Option>, }, } // todo: make quotes bool // todo: track span inside #[derive(Debug, Clone)] pub struct StringExpr(pub Interpolation, pub QuoteKind); impl StringExpr { fn quote_inner_text( text: &str, quote: char, buffer: &mut Interpolation, // default=false is_static: bool, ) { let mut chars = text.chars().peekable(); while let Some(char) = chars.next() { if char == '\n' || char == '\r' { buffer.add_char('\\'); buffer.add_char('a'); if let Some(next) = chars.peek() { if next.is_ascii_whitespace() || next.is_ascii_hexdigit() { buffer.add_char(' '); } } } else { if char == quote || char == '\\' || (is_static && char == '#' && chars.peek() == Some(&'{')) { buffer.add_char('\\'); } buffer.add_char(char); } } } fn best_quote<'a>(strings: impl Iterator) -> char { let mut contains_double_quote = false; for s in strings { for c in s.chars() { if c == '\'' { return '"'; } if c == '"' { contains_double_quote = true; } } } if contains_double_quote { '\'' } else { '"' } } pub fn as_interpolation(self, is_static: bool) -> Interpolation { if self.1 == QuoteKind::None { return self.0; } let quote = Self::best_quote(self.0.contents.iter().filter_map(|c| match c { InterpolationPart::Expr(..) => None, InterpolationPart::String(text) => Some(text.as_str()), })); let mut buffer = Interpolation::new(); buffer.add_char(quote); for value in self.0.contents { match value { InterpolationPart::Expr(e) => buffer.add_expr(e), InterpolationPart::String(text) => { Self::quote_inner_text(&text, quote, &mut buffer, is_static); } } } buffer.add_char(quote); buffer } } impl AstExpr { pub fn is_variable(&self) -> bool { matches!(self, Self::Variable { .. }) } pub fn is_slash_operand(&self) -> bool { match self { Self::Number { .. } | Self::Calculation { .. } => true, Self::BinaryOp(binop) => binop.allows_slash, _ => false, } } pub fn slash(left: Self, right: Self, span: Span) -> Self { Self::BinaryOp(Arc::new(BinaryOpExpr { lhs: left, op: BinaryOp::Div, rhs: right, allows_slash: true, span, })) } pub const fn span(self, span: Span) -> Spanned { Spanned { node: self, span } } } grass_compiler-0.13.4/src/ast/interpolation.rs000064400000000000000000000042351046102023000175410ustar 00000000000000use codemap::Spanned; use super::AstExpr; #[derive(Debug, Clone)] pub struct Interpolation { pub contents: Vec, } impl Interpolation { pub fn new() -> Self { Self { contents: Vec::new(), } } pub fn is_empty(&self) -> bool { self.contents.is_empty() } pub fn new_with_expr(e: Spanned) -> Self { Self { contents: vec![InterpolationPart::Expr(e)], } } pub fn new_plain(s: String) -> Self { Self { contents: vec![InterpolationPart::String(s)], } } pub fn add_expr(&mut self, expr: Spanned) { self.contents.push(InterpolationPart::Expr(expr)); } pub fn add_string(&mut self, s: String) { match self.contents.last_mut() { Some(InterpolationPart::String(existing)) => existing.push_str(&s), _ => self.contents.push(InterpolationPart::String(s)), } } pub fn add_char(&mut self, c: char) { match self.contents.last_mut() { Some(InterpolationPart::String(existing)) => existing.push(c), _ => self.contents.push(InterpolationPart::String(c.to_string())), } } pub fn add_interpolation(&mut self, mut other: Self) { self.contents.append(&mut other.contents); } pub fn initial_plain(&self) -> &str { match self.contents.first() { Some(InterpolationPart::String(s)) => s, _ => "", } } pub fn as_plain(&self) -> Option<&str> { if self.contents.is_empty() { Some("") } else if self.contents.len() > 1 { None } else { match self.contents.first()? { InterpolationPart::String(s) => Some(s), InterpolationPart::Expr(..) => None, } } } pub fn trailing_string(&self) -> &str { match self.contents.last() { Some(InterpolationPart::String(s)) => s, Some(InterpolationPart::Expr(..)) | None => "", } } } #[derive(Debug, Clone)] pub enum InterpolationPart { String(String), Expr(Spanned), } grass_compiler-0.13.4/src/ast/media.rs000064400000000000000000000176221046102023000157350ustar 00000000000000use std::fmt::{self, Write}; use codemap::Span; use crate::{ast::CssStmt, error::SassResult, lexer::Lexer, parse::MediaQueryParser}; #[derive(Debug, Clone)] pub(crate) struct MediaRule { pub query: Vec, pub body: Vec, } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub struct MediaQuery { pub modifier: Option, pub media_type: Option, pub conditions: Vec, pub conjunction: bool, } impl MediaQuery { pub fn matches_all_types(&self) -> bool { self.media_type.is_none() || self .media_type .as_ref() .map_or(false, |v| v.to_ascii_lowercase() == "all") } pub fn condition( conditions: Vec, // default=true conjunction: bool, ) -> Self { Self { modifier: None, media_type: None, conditions, conjunction, } } pub fn media_type( media_type: Option, modifier: Option, conditions: Option>, ) -> Self { Self { modifier, conjunction: true, media_type, conditions: conditions.unwrap_or_default(), } } pub fn parse_list(list: &str, span: Span) -> SassResult> { let toks = Lexer::new_from_string(list, span); MediaQueryParser::new(toks).parse() } #[allow(clippy::if_not_else)] pub(crate) fn merge(&self, other: &Self) -> MediaQueryMergeResult { if !self.conjunction || !other.conjunction { return MediaQueryMergeResult::Unrepresentable; } let this_modifier = self.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let this_type = self.media_type.as_ref().map(|m| m.to_ascii_lowercase()); let other_modifier = other.modifier.as_ref().map(|m| m.to_ascii_lowercase()); let other_type = other.media_type.as_ref().map(|m| m.to_ascii_lowercase()); if this_type.is_none() && other_type.is_none() { return MediaQueryMergeResult::Success(Self::condition( self.conditions .iter() .chain(&other.conditions) .cloned() .collect(), true, )); } let modifier; let media_type; let conditions; if (this_modifier.as_deref() == Some("not")) != (other_modifier.as_deref() == Some("not")) { if this_modifier == other_modifier { let negative_conditions = if this_modifier.as_deref() == Some("not") { &self.conditions } else { &other.conditions }; let positive_conditions = if this_modifier.as_deref() == Some("not") { &other.conditions } else { &self.conditions }; // If the negative conditions are a subset of the positive conditions, the // query is empty. For example, `not screen and (color)` has no // intersection with `screen and (color) and (grid)`. // // However, `not screen and (color)` *does* intersect with `screen and // (grid)`, because it means `not (screen and (color))` and so it allows // a screen with no color but with a grid. if negative_conditions .iter() .all(|feat| positive_conditions.contains(feat)) { return MediaQueryMergeResult::Empty; } return MediaQueryMergeResult::Unrepresentable; } else if self.matches_all_types() || other.matches_all_types() { return MediaQueryMergeResult::Unrepresentable; } if this_modifier.as_deref() == Some("not") { modifier = &other_modifier; media_type = &other_type; conditions = other.conditions.clone(); } else { modifier = &this_modifier; media_type = &this_type; conditions = self.conditions.clone(); } } else if this_modifier.as_deref() == Some("not") { debug_assert_eq!(other_modifier.as_deref(), Some("not")); // CSS has no way of representing "neither screen nor print". if this_type != other_type { return MediaQueryMergeResult::Unrepresentable; } let more_conditions = if self.conditions.len() > other.conditions.len() { &self.conditions } else { &other.conditions }; let fewer_conditions = if self.conditions.len() > other.conditions.len() { &other.conditions } else { &self.conditions }; // If one set of conditions is a superset of the other, use those conditions // because they're strictly narrower. if fewer_conditions .iter() .all(|feat| more_conditions.contains(feat)) { modifier = &this_modifier; media_type = &this_type; conditions = more_conditions.clone(); } else { // Otherwise, there's no way to represent the intersection. return MediaQueryMergeResult::Unrepresentable; } } else if self.matches_all_types() { modifier = &other_modifier; // Omit the type if either input query did, since that indicates that they // aren't targeting a browser that requires "all and". media_type = if other.matches_all_types() && this_type.is_none() { &None } else { &other_type }; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } else if other.matches_all_types() { modifier = &this_modifier; media_type = &this_type; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } else if this_type != other_type { return MediaQueryMergeResult::Empty; } else { if this_modifier.is_some() { modifier = &this_modifier; } else { modifier = &other_modifier; } media_type = &this_type; conditions = self .conditions .iter() .chain(&other.conditions) .cloned() .collect(); } MediaQueryMergeResult::Success(MediaQuery { media_type: if media_type == &this_type { self.media_type.clone() } else { other.media_type.clone() }, modifier: if modifier == &this_modifier { self.modifier.clone() } else { other.modifier.clone() }, conditions, conjunction: true, }) } } impl fmt::Display for MediaQuery { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(modifier) = &self.modifier { f.write_str(modifier)?; f.write_char(' ')?; } if let Some(media_type) = &self.media_type { f.write_str(media_type)?; if !&self.conditions.is_empty() { f.write_str(" and ")?; } } f.write_str(&self.conditions.join(" and ")) } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum MediaQueryMergeResult { Empty, Unrepresentable, Success(MediaQuery), } grass_compiler-0.13.4/src/ast/mixin.rs000064400000000000000000000015441046102023000157760ustar 00000000000000use std::fmt; use crate::{ ast::ArgumentResult, error::SassResult, evaluate::{Environment, Visitor}, }; pub(crate) type BuiltinMixin = fn(ArgumentResult, &mut Visitor) -> SassResult<()>; pub(crate) use crate::ast::AstMixin as UserDefinedMixin; #[derive(Clone)] pub(crate) enum Mixin { UserDefined(UserDefinedMixin, Environment), Builtin(BuiltinMixin), } impl fmt::Debug for Mixin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::UserDefined(u, ..) => f .debug_struct("AstMixin") .field("name", &u.name) .field("args", &u.args) .field("body", &u.body) .field("has_content", &u.has_content) .finish(), Self::Builtin(..) => f.debug_struct("BuiltinMixin").finish(), } } } grass_compiler-0.13.4/src/ast/mod.rs000064400000000000000000000005221046102023000154240ustar 00000000000000pub use args::*; pub(crate) use css::*; pub use expr::*; pub use interpolation::*; pub(crate) use media::*; pub(crate) use mixin::*; pub use stmt::*; pub(crate) use style::*; pub(crate) use unknown::*; pub use args::ArgumentResult; mod args; mod css; mod expr; mod interpolation; mod media; mod mixin; mod stmt; mod style; mod unknown; grass_compiler-0.13.4/src/ast/stmt.rs000064400000000000000000000330161046102023000156400ustar 00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, path::PathBuf, rc::Rc, sync::Arc, }; use codemap::{Span, Spanned}; use crate::{ ast::{ArgumentDeclaration, ArgumentInvocation, AstExpr, CssStmt}, ast::{Interpolation, MediaQuery}, common::Identifier, utils::{BaseMapView, LimitedMapView, MapView, UnprefixedMapView}, value::Value, }; #[derive(Debug, Clone)] #[allow(unused)] pub struct AstSilentComment { pub text: String, pub span: Span, } #[derive(Debug, Clone)] pub struct AstPlainCssImport { pub url: Interpolation, pub modifiers: Option, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AstSassImport { pub url: String, pub span: Span, } #[derive(Debug, Clone)] pub struct AstIf { pub if_clauses: Vec, pub else_clause: Option>, } #[derive(Debug, Clone)] pub struct AstIfClause { pub condition: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstFor { pub variable: Spanned, pub from: Spanned, pub to: Spanned, pub is_exclusive: bool, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstReturn { pub val: AstExpr, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AstRuleSet { pub selector: Interpolation, pub body: Vec, pub selector_span: Span, pub span: Span, } #[derive(Debug, Clone)] pub struct AstStyle { pub name: Interpolation, pub value: Option>, pub body: Vec, pub span: Span, } impl AstStyle { pub fn is_custom_property(&self) -> bool { self.name.initial_plain().starts_with("--") } } #[derive(Debug, Clone)] pub struct AstEach { pub variables: Vec, pub list: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstMedia { pub query: Interpolation, pub query_span: Span, pub body: Vec, pub span: Span, } pub type CssMediaQuery = MediaQuery; #[derive(Debug, Clone)] pub struct AstWhile { pub condition: AstExpr, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstVariableDecl { pub namespace: Option>, pub name: Identifier, pub value: AstExpr, pub is_guarded: bool, pub is_global: bool, pub span: Span, } #[derive(Debug, Clone)] pub struct AstFunctionDecl { pub name: Spanned, pub arguments: ArgumentDeclaration, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstDebugRule { pub value: AstExpr, pub span: Span, } #[derive(Debug, Clone)] pub struct AstWarn { pub value: AstExpr, pub span: Span, } #[derive(Debug, Clone)] pub struct AstErrorRule { pub value: AstExpr, pub span: Span, } impl PartialEq for AstFunctionDecl { fn eq(&self, other: &Self) -> bool { self.name == other.name } } impl Eq for AstFunctionDecl {} #[derive(Debug, Clone)] pub struct AstLoudComment { pub text: Interpolation, pub span: Span, } #[derive(Debug, Clone)] pub struct AstMixin { pub name: Identifier, pub args: ArgumentDeclaration, pub body: Vec, /// Whether the mixin contains a `@content` rule. pub has_content: bool, } #[derive(Debug, Clone)] pub struct AstContentRule { pub args: ArgumentInvocation, } #[derive(Debug, Clone)] pub struct AstContentBlock { pub args: ArgumentDeclaration, pub body: Vec, } #[derive(Debug, Clone)] pub struct AstInclude { pub namespace: Option>, pub name: Spanned, pub args: ArgumentInvocation, pub content: Option, pub span: Span, } #[derive(Debug, Clone)] pub struct AstUnknownAtRule { pub name: Interpolation, pub value: Option, pub body: Option>, pub span: Span, } #[derive(Debug, Clone)] pub struct AstExtendRule { pub value: Interpolation, pub is_optional: bool, pub span: Span, } #[derive(Debug, Clone)] pub struct AstAtRootRule { pub body: Vec, pub query: Option>, #[allow(unused)] pub span: Span, } #[derive(Debug, Clone)] pub struct AtRootQuery { pub include: bool, pub names: HashSet, pub all: bool, pub rule: bool, } impl AtRootQuery { pub fn new(include: bool, names: HashSet) -> Self { let all = names.contains("all"); let rule = names.contains("rule"); Self { include, names, all, rule, } } pub fn excludes_name(&self, name: &str) -> bool { (self.all || self.names.contains(name)) != self.include } pub fn excludes_style_rules(&self) -> bool { (self.all || self.rule) != self.include } pub(crate) fn excludes(&self, stmt: &CssStmt) -> bool { if self.all { return !self.include; } match stmt { CssStmt::RuleSet { .. } => self.excludes_style_rules(), CssStmt::Media(..) => self.excludes_name("media"), CssStmt::Supports(..) => self.excludes_name("supports"), CssStmt::UnknownAtRule(rule, ..) => self.excludes_name(&rule.name.to_ascii_lowercase()), _ => false, } } } impl Default for AtRootQuery { fn default() -> Self { Self { include: false, names: HashSet::new(), all: false, rule: true, } } } #[derive(Debug, Clone)] pub struct AstImportRule { pub imports: Vec, } #[derive(Debug, Clone)] pub enum AstImport { Plain(AstPlainCssImport), Sass(AstSassImport), } impl AstImport { pub fn is_dynamic(&self) -> bool { matches!(self, AstImport::Sass(..)) } } #[derive(Debug, Clone)] pub struct AstUseRule { pub url: PathBuf, pub namespace: Option, pub configuration: Vec, pub span: Span, } #[derive(Debug, Clone)] pub struct ConfiguredVariable { pub name: Spanned, pub expr: Spanned, pub is_guarded: bool, } #[derive(Debug, Clone)] pub struct Configuration { pub(crate) values: Arc>, #[allow(unused)] pub(crate) original_config: Option>>, pub(crate) span: Option, } impl Configuration { pub fn through_forward( config: Rc>, forward: &AstForwardRule, ) -> Rc> { if (*config).borrow().is_empty() { return Rc::new(RefCell::new(Configuration::empty())); } let mut new_values = Arc::clone(&(*config).borrow().values); // Only allow variables that are visible through the `@forward` to be // configured. These views support [Map.remove] so we can mark when a // configuration variable is used by removing it even when the underlying // map is wrapped. if let Some(prefix) = &forward.prefix { new_values = Arc::new(UnprefixedMapView(new_values, prefix.clone())); } if let Some(shown_variables) = &forward.shown_variables { new_values = Arc::new(LimitedMapView::safelist(new_values, shown_variables)); } else if let Some(hidden_variables) = &forward.hidden_variables { new_values = Arc::new(LimitedMapView::blocklist(new_values, hidden_variables)); } Rc::new(RefCell::new(Self::with_values( config, Arc::clone(&new_values), ))) } fn with_values( config: Rc>, values: Arc>, ) -> Self { Self { values, original_config: Some(config), span: None, } } pub fn first(&self) -> Option> { let name = *self.values.keys().first()?; let value = self.values.get(name)?; Some(Spanned { node: name, span: value.configuration_span?, }) } pub fn remove(&mut self, name: Identifier) -> Option { self.values.remove(name) } pub fn is_implicit(&self) -> bool { self.span.is_none() } pub fn implicit(values: BTreeMap) -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))), original_config: None, span: None, } } pub fn explicit(values: BTreeMap, span: Span) -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(values)))), original_config: None, span: Some(span), } } pub fn empty() -> Self { Self { values: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), original_config: None, span: None, } } pub fn is_empty(&self) -> bool { self.values.is_empty() } #[allow(unused)] pub fn original_config(config: Rc>) -> Rc> { match (*config).borrow().original_config.as_ref() { Some(v) => Rc::clone(v), None => Rc::clone(&config), } } } #[derive(Debug, Clone)] pub struct ConfiguredValue { pub value: Value, pub configuration_span: Option, } impl ConfiguredValue { pub fn explicit(value: Value, configuration_span: Span) -> Self { Self { value, configuration_span: Some(configuration_span), } } pub fn implicit(value: Value) -> Self { Self { value, configuration_span: None, } } } #[derive(Debug, Clone)] pub struct AstForwardRule { pub url: PathBuf, pub shown_mixins_and_functions: Option>, pub shown_variables: Option>, pub hidden_mixins_and_functions: Option>, pub hidden_variables: Option>, pub prefix: Option, pub configuration: Vec, pub span: Span, } impl AstForwardRule { pub fn new( url: PathBuf, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: None, shown_variables: None, hidden_mixins_and_functions: None, hidden_variables: None, prefix, configuration: configuration.unwrap_or_default(), span, } } pub fn show( url: PathBuf, shown_mixins_and_functions: HashSet, shown_variables: HashSet, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: Some(shown_mixins_and_functions), shown_variables: Some(shown_variables), hidden_mixins_and_functions: None, hidden_variables: None, prefix, configuration: configuration.unwrap_or_default(), span, } } pub fn hide( url: PathBuf, hidden_mixins_and_functions: HashSet, hidden_variables: HashSet, prefix: Option, configuration: Option>, span: Span, ) -> Self { Self { url, shown_mixins_and_functions: None, shown_variables: None, hidden_mixins_and_functions: Some(hidden_mixins_and_functions), hidden_variables: Some(hidden_variables), prefix, configuration: configuration.unwrap_or_default(), span, } } } #[derive(Debug, Clone)] pub enum AstSupportsCondition { Anything { contents: Interpolation, }, Declaration { name: AstExpr, value: AstExpr, }, Function { name: Interpolation, args: Interpolation, }, Interpolation(AstExpr), Negation(Box), Operation { left: Box, operator: Option, right: Box, }, } #[derive(Debug, Clone)] pub struct AstSupportsRule { pub condition: AstSupportsCondition, pub body: Vec, pub span: Span, } #[derive(Debug, Clone)] pub enum AstStmt { If(AstIf), For(AstFor), Return(AstReturn), RuleSet(AstRuleSet), Style(AstStyle), Each(AstEach), Media(AstMedia), Include(AstInclude), While(AstWhile), VariableDecl(AstVariableDecl), LoudComment(AstLoudComment), SilentComment(AstSilentComment), FunctionDecl(AstFunctionDecl), Mixin(AstMixin), ContentRule(AstContentRule), Warn(AstWarn), UnknownAtRule(AstUnknownAtRule), ErrorRule(AstErrorRule), Extend(AstExtendRule), AtRootRule(AstAtRootRule), Debug(AstDebugRule), ImportRule(AstImportRule), Use(AstUseRule), Forward(AstForwardRule), Supports(AstSupportsRule), } #[derive(Debug, Clone)] pub struct StyleSheet { pub body: Vec, pub url: PathBuf, pub is_plain_css: bool, /// Array of indices into `body` pub uses: Vec, /// Array of indices into `body` pub forwards: Vec, } impl StyleSheet { pub fn new(is_plain_css: bool, url: PathBuf) -> Self { Self { body: Vec::new(), url, is_plain_css, uses: Vec::new(), forwards: Vec::new(), } } } grass_compiler-0.13.4/src/ast/style.rs000064400000000000000000000004141046102023000160050ustar 00000000000000use codemap::Spanned; use crate::{interner::InternedString, value::Value}; /// A style: `color: red` #[derive(Clone, Debug)] pub(crate) struct Style { pub property: InternedString, pub value: Box>, pub declared_as_custom_property: bool, } grass_compiler-0.13.4/src/ast/unknown.rs000064400000000000000000000005461046102023000163520ustar 00000000000000use crate::ast::CssStmt; #[derive(Debug, Clone)] #[allow(dead_code)] pub(crate) struct UnknownAtRule { pub name: String, // pub super_selector: Selector, pub params: String, pub body: Vec, /// Whether or not this @-rule was declared with curly /// braces. A body may not necessarily have contents pub has_body: bool, } grass_compiler-0.13.4/src/builtin/functions/color/hsl.rs000064400000000000000000000241641046102023000214500ustar 00000000000000use std::collections::{BTreeMap, BTreeSet}; use crate::{builtin::builtin_imports::*, serializer::serialize_number, value::SassNumber}; use super::{ angle_value, rgb::{function_string, parse_channels, percentage_or_unitless}, ParsedChannels, }; fn hsl_3_args( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { let span = args.span(); let hue = args.get_err(0, "hue")?; let saturation = args.get_err(1, "saturation")?; let lightness = args.get_err(2, "lightness")?; let alpha = args.default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0))); if [&hue, &saturation, &lightness, &alpha] .iter() .copied() .any(Value::is_special_function) { return Ok(Value::String( format!( "{}({})", name, Value::List( if args.len() == 4 { vec![hue, saturation, lightness, alpha] } else { vec![hue, saturation, lightness] }, ListSeparator::Comma, Brackets::None ) .to_css_string(args.span(), false)? ), QuoteKind::None, )); } let hue = angle_value(hue, "hue", span)?; let saturation = saturation.assert_number_with_name("saturation", span)?; let lightness = lightness.assert_number_with_name("lightness", span)?; let alpha = percentage_or_unitless( &alpha.assert_number_with_name("alpha", span)?, 1.0, "alpha", span, visitor, )?; Ok(Value::Color(Arc::new(Color::from_hsla_fn( Number(hue.rem_euclid(360.0)), saturation.num / Number(100.0), lightness.num / Number(100.0), Number(alpha), )))) } fn inner_hsl( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(4)?; let span = args.span(); let len = args.len(); if len == 1 || len == 0 { match parse_channels( name, &["hue", "saturation", "lightness"], args.get_err(0, "channels")?, visitor, args.span(), )? { ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, named: BTreeMap::new(), separator: ListSeparator::Comma, span: args.span(), touched: BTreeSet::new(), }; hsl_3_args(name, args, visitor) } } } else if len == 2 { let hue = args.get_err(0, "hue")?; let saturation = args.get_err(1, "saturation")?; if hue.is_var() || saturation.is_var() { Ok(Value::String( function_string(name, &[hue, saturation], visitor, span)?, QuoteKind::None, )) } else { Err(("Missing argument $lightness.", args.span()).into()) } } else { hsl_3_args(name, args, visitor) } } pub(crate) fn hsl(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_hsl("hsl", args, visitor) } pub(crate) fn hsla(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_hsl("hsla", args, visitor) } pub(crate) fn hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.hue(), unit: Unit::Deg, as_slash: None, })) } pub(crate) fn saturation(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.saturation(), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn lightness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.lightness(), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn adjust_hue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let degrees = angle_value(args.get_err(1, "degrees")?, "degrees", args.span())?; Ok(Value::Color(Arc::new(color.adjust_hue(degrees)))) } fn lighten(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.lighten(amount.num)))) } fn darken(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.darken(amount.num)))) } fn saturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; if args.len() == 1 { let amount = args .get_err(0, "amount")? .assert_number_with_name("amount", args.span())?; return Ok(Value::String( format!( "saturate({})", serialize_number(&amount, &Options::default(), args.span())?, ), QuoteKind::None, )); } let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Color(Arc::new(color.saturate(amount.num)))) } fn desaturate(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let mut amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds("amount", 0.0, 100.0, args.span())?; amount.num /= Number(100.0); Ok(Value::Color(Arc::new(color.desaturate(amount.num)))) } pub(crate) fn grayscale(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = match args.get_err(0, "color")? { Value::Color(c) => c, Value::Dimension(SassNumber { num: n, unit: u, as_slash: _, }) => { return Ok(Value::String( format!("grayscale({}{})", n.inspect(), u), QuoteKind::None, )) } v => { return Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::Color(Arc::new(color.desaturate(Number::one())))) } pub(crate) fn complement(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Color(Arc::new(color.complement()))) } pub(crate) fn invert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let weight = args .get(1, "weight") .map::, _>(|weight| { let mut weight = weight.node.assert_number_with_name("weight", span)?; weight.assert_bounds("weight", 0.0, 100.0, span)?; weight.num /= Number(100.0); Ok(weight.num) }) .transpose()?; match args.get_err(0, "color")? { Value::Color(c) => Ok(Value::Color(Arc::new( c.invert(weight.unwrap_or_else(Number::one)), ))), Value::Dimension(SassNumber { num: n, unit: u, as_slash: _, }) => { if weight.is_some() { return Err(( "Only one argument may be passed to the plain-CSS invert() function.", args.span(), ) .into()); } Ok(Value::String( format!("invert({}{})", n.inspect(), u), QuoteKind::None, )) } v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()), } } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("hsl", Builtin::new(hsl)); f.insert("hsla", Builtin::new(hsla)); f.insert("hue", Builtin::new(hue)); f.insert("saturation", Builtin::new(saturation)); f.insert("adjust-hue", Builtin::new(adjust_hue)); f.insert("lightness", Builtin::new(lightness)); f.insert("lighten", Builtin::new(lighten)); f.insert("darken", Builtin::new(darken)); f.insert("saturate", Builtin::new(saturate)); f.insert("desaturate", Builtin::new(desaturate)); f.insert("grayscale", Builtin::new(grayscale)); f.insert("complement", Builtin::new(complement)); f.insert("invert", Builtin::new(invert)); } grass_compiler-0.13.4/src/builtin/functions/color/hwb.rs000064400000000000000000000057331046102023000214430ustar 00000000000000use crate::builtin::builtin_imports::*; use super::{ angle_value, rgb::{parse_channels, percentage_or_unitless}, ParsedChannels, }; pub(crate) fn blackness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.blackness() * 100, unit: Unit::Percent, as_slash: None, })) } pub(crate) fn whiteness(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber { num: color.whiteness() * 100, unit: Unit::Percent, as_slash: None, })) } fn hwb_inner(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let hue = angle_value(args.get_err(0, "hue")?, "hue", args.span())?; let whiteness = args .get_err(1, "whiteness")? .assert_number_with_name("whiteness", span)?; whiteness.assert_unit(&Unit::Percent, "whiteness", span)?; whiteness.assert_bounds("whiteness", 0.0, 100.0, args.span())?; let blackness = args .get_err(2, "blackness")? .assert_number_with_name("blackness", span)?; blackness.assert_unit(&Unit::Percent, "blackness", span)?; blackness.assert_bounds("blackness", 0.0, 100.0, args.span())?; let alpha = args .default_arg(3, "alpha", Value::Dimension(SassNumber::new_unitless(1.0))) .assert_number_with_name("alpha", args.span())?; let alpha = percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?; Ok(Value::Color(Arc::new(Color::from_hwb( hue, whiteness.num, blackness.num, Number(alpha), )))) } pub(crate) fn hwb(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(4)?; if args.len() == 0 || args.len() == 1 { match parse_channels( "hwb", &["hue", "whiteness", "blackness"], args.get_err(0, "channels")?, visitor, args.span(), )? { ParsedChannels::String(s) => Err(( format!("Expected numeric channels, got \"{}\".", s), args.span(), ) .into()), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, named: BTreeMap::new(), separator: ListSeparator::Comma, span: args.span(), touched: BTreeSet::new(), }; hwb_inner(args, visitor) } } } else if args.len() == 3 || args.len() == 4 { hwb_inner(args, visitor) } else { args.max_args(1)?; unreachable!() } } grass_compiler-0.13.4/src/builtin/functions/color/mod.rs000064400000000000000000000015021046102023000214300ustar 00000000000000use codemap::Span; use crate::{ builtin::builtin_imports::Unit, error::SassResult, value::{conversion_factor, Number, Value}, }; use super::GlobalFunctionMap; pub mod hsl; pub mod hwb; pub mod opacity; pub mod other; pub mod rgb; #[derive(Debug, Clone)] pub(crate) enum ParsedChannels { String(String), List(Vec), } pub(crate) fn angle_value(num: Value, name: &str, span: Span) -> SassResult { let angle = num.assert_number_with_name(name, span)?; if angle.has_compatible_units(&Unit::Deg) { let factor = conversion_factor(&angle.unit, &Unit::Deg).unwrap(); return Ok(angle.num * Number(factor)); } Ok(angle.num) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { hsl::declare(f); opacity::declare(f); other::declare(f); rgb::declare(f); } grass_compiler-0.13.4/src/builtin/functions/color/opacity.rs000064400000000000000000000073541046102023000223340ustar 00000000000000use crate::builtin::builtin_imports::*; /// Check if `s` matches the regex `^[a-zA-Z]+\s*=` fn is_ms_filter(s: &str) -> bool { let mut bytes = s.bytes(); if !bytes.next().map_or(false, |c| c.is_ascii_alphabetic()) { return false; } bytes .skip_while(u8::is_ascii_alphabetic) .find(|c| !matches!(c, b' ' | b'\t' | b'\n')) == Some(b'=') } #[cfg(test)] mod test { use super::is_ms_filter; #[test] fn test_is_ms_filter() { assert!(is_ms_filter("a=a")); assert!(is_ms_filter("a=")); assert!(is_ms_filter("a \t\n =a")); assert!(!is_ms_filter("a \t\n a=a")); assert!(!is_ms_filter("aa")); assert!(!is_ms_filter(" aa")); assert!(!is_ms_filter("=a")); assert!(!is_ms_filter("1=a")); } } pub(crate) fn alpha(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { if args.len() <= 1 { let color = args.get_err(0, "color")?; if let Value::String(s, QuoteKind::None) = &color { if is_ms_filter(s) { return Ok(Value::String(format!("alpha({})", s), QuoteKind::None)); } } let color = color.assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.alpha()))) } else { let err = args.max_args(1); let args = args .get_variadic()? .into_iter() .map(|arg| match arg.node { Value::String(s, QuoteKind::None) if is_ms_filter(&s) => Ok(s), _ => { err.clone()?; unreachable!() } }) .collect::>>()?; Ok(Value::String( format!("alpha({})", args.join(", "),), QuoteKind::None, )) } } pub(crate) fn opacity(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; match args.get_err(0, "color")? { Value::Color(c) => Ok(Value::Dimension(SassNumber::new_unitless(c.alpha()))), Value::Dimension(SassNumber { num, unit, as_slash: _, }) => Ok(Value::String( format!("opacity({}{})", num.inspect(), unit), QuoteKind::None, )), v => Err(( format!("$color: {} is not a color.", v.inspect(args.span())?), args.span(), ) .into()), } } fn opacify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_in(amount.num)))) } fn transparentize(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; let amount = args .get_err(1, "amount")? .assert_number_with_name("amount", args.span())?; amount.assert_bounds_with_unit("amount", 0.0, 1.0, &Unit::None, args.span())?; Ok(Value::Color(Arc::new(color.fade_out(amount.num)))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("alpha", Builtin::new(alpha)); f.insert("opacity", Builtin::new(opacity)); f.insert("opacify", Builtin::new(opacify)); f.insert("fade-in", Builtin::new(opacify)); f.insert("transparentize", Builtin::new(transparentize)); f.insert("fade-out", Builtin::new(transparentize)); } grass_compiler-0.13.4/src/builtin/functions/color/other.rs000064400000000000000000000176361046102023000220110ustar 00000000000000use crate::{ builtin::{builtin_imports::*, color::angle_value}, utils::to_sentence, value::fuzzy_round, }; #[derive(Debug, Copy, Clone, Eq, PartialEq)] enum UpdateComponents { Change, Adjust, Scale, } fn update_components( mut args: ArgumentResult, visitor: &mut Visitor, update: UpdateComponents, ) -> SassResult { let span = args.span(); let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; // todo: what if color is also passed by name if args.positional.len() > 1 { return Err(( "Only one positional argument is allowed. All other arguments must be passed by name.", span, ) .into()); } let check_num = |num: Spanned, name: &str, mut max: f64, assert_percent: bool, check_percent: bool| -> SassResult { let span = num.span; let mut num = num.node.assert_number_with_name(name, span)?; if update == UpdateComponents::Scale { max = 100.0; } if assert_percent || update == UpdateComponents::Scale { num.assert_unit(&Unit::Percent, name, span)?; num.assert_bounds( name, if update == UpdateComponents::Change { 0.0 } else { -max }, max, span, )?; } else { num.assert_bounds_with_unit( name, if update == UpdateComponents::Change { 0.0 } else { -max }, max, if check_percent { &Unit::Percent } else { &Unit::None }, span, )?; } // todo: hack to check if rgb channel if max == 100.0 { num.num /= Number(100.0); } Ok(num.num) }; let get_arg = |args: &mut ArgumentResult, name: &str, max: f64, assert_percent: bool, check_percent: bool| -> SassResult> { Ok(match args.get(usize::MAX, name) { Some(v) => Some(check_num(v, name, max, assert_percent, check_percent)?), None => None, }) }; let red = get_arg(&mut args, "red", 255.0, false, false)?; let green = get_arg(&mut args, "green", 255.0, false, false)?; let blue = get_arg(&mut args, "blue", 255.0, false, false)?; let alpha = get_arg(&mut args, "alpha", 1.0, false, false)?; let hue = if update == UpdateComponents::Scale { None } else { args.get(usize::MAX, "hue") .map(|v| angle_value(v.node, "hue", v.span)) .transpose()? }; let saturation = get_arg(&mut args, "saturation", 100.0, false, true)?; let lightness = get_arg(&mut args, "lightness", 100.0, false, true)?; let whiteness = get_arg(&mut args, "whiteness", 100.0, true, true)?; let blackness = get_arg(&mut args, "blackness", 100.0, true, true)?; if !args.named.is_empty() { let argument_word = if args.named.len() == 1 { "argument" } else { "arguments" }; let argument_names = to_sentence( args.named .keys() .map(|key| format!("${key}", key = key)) .collect(), "or", ); return Err(( format!( "No {argument_word} named {argument_names}.", argument_word = argument_word, argument_names = argument_names ), span, ) .into()); } let has_rgb = red.is_some() || green.is_some() || blue.is_some(); let has_sl = saturation.is_some() || lightness.is_some(); let has_wb = whiteness.is_some() || blackness.is_some(); if has_rgb && (has_sl || has_wb || hue.is_some()) { let param_type = if has_wb { "HWB" } else { "HSL" }; return Err(( format!( "RGB parameters may not be passed along with {} parameters.", param_type ), span, ) .into()); } if has_sl && has_wb { return Err(( "HSL parameters may not be passed along with HWB parameters.", span, ) .into()); } fn update_value( current: Number, param: Option, max: f64, update: UpdateComponents, ) -> Number { let param = match param { Some(p) => p, None => return current, }; match update { UpdateComponents::Change => param, UpdateComponents::Adjust => (param + current).clamp(0.0, max), UpdateComponents::Scale => { current + if param > Number(0.0) { Number(max) - current } else { current } * param } } } fn update_rgb(current: Number, param: Option, update: UpdateComponents) -> Number { Number(fuzzy_round(update_value(current, param, 255.0, update).0)) } let color = if has_rgb { Arc::new(Color::from_rgba( update_rgb(color.red(), red, update), update_rgb(color.green(), green, update), update_rgb(color.blue(), blue, update), update_value(color.alpha(), alpha, 1.0, update), )) } else if has_wb { Arc::new(Color::from_hwb( if update == UpdateComponents::Change { hue.unwrap_or_else(|| color.hue()) } else { color.hue() + hue.unwrap_or_else(Number::zero) }, update_value(color.whiteness(), whiteness, 1.0, update) * Number(100.0), update_value(color.blackness(), blackness, 1.0, update) * Number(100.0), update_value(color.alpha(), alpha, 1.0, update), )) } else if hue.is_some() || has_sl { let (this_hue, this_saturation, this_lightness, this_alpha) = color.as_hsla(); Arc::new(Color::from_hsla( if update == UpdateComponents::Change { hue.unwrap_or(this_hue) } else { this_hue + hue.unwrap_or_else(Number::zero) }, update_value(this_saturation, saturation, 1.0, update), update_value(this_lightness, lightness, 1.0, update), update_value(this_alpha, alpha, 1.0, update), )) } else if alpha.is_some() { Arc::new(color.with_alpha(update_value(color.alpha(), alpha, 1.0, update))) } else { color }; Ok(Value::Color(color)) } pub(crate) fn scale_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Scale) } pub(crate) fn change_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Change) } pub(crate) fn adjust_color(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { update_components(args, visitor, UpdateComponents::Adjust) } pub(crate) fn ie_hex_str(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::String(color.to_ie_hex_str(), QuoteKind::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("change-color", Builtin::new(change_color)); f.insert("adjust-color", Builtin::new(adjust_color)); f.insert("scale-color", Builtin::new(scale_color)); f.insert("ie-hex-str", Builtin::new(ie_hex_str)); } grass_compiler-0.13.4/src/builtin/functions/color/rgb.rs000064400000000000000000000311621046102023000214300ustar 00000000000000use crate::{builtin::builtin_imports::*, serializer::inspect_number, value::fuzzy_round}; use super::ParsedChannels; pub(crate) fn function_string( name: &'static str, args: &[Value], visitor: &mut Visitor, span: Span, ) -> SassResult { let args = args .iter() .map(|arg| arg.to_css_string(span, visitor.options.is_compressed())) .collect::>>()? .join(", "); Ok(format!("{}({})", name, args)) } fn inner_rgb_2_arg( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { // rgba(var(--foo), 0.5) is valid CSS because --foo might be `123, 456, 789` // and functions are parsed after variable substitution. let color = args.get_err(0, "color")?; let alpha = args.get_err(1, "alpha")?; let is_compressed = visitor.options.is_compressed(); if color.is_var() { return Ok(Value::String( function_string(name, &[color, alpha], visitor, args.span())?, QuoteKind::None, )); } else if alpha.is_var() { match &color { Value::Color(color) => { return Ok(Value::String( format!( "{}({}, {}, {}, {})", name, color.red().to_string(is_compressed), color.green().to_string(is_compressed), color.blue().to_string(is_compressed), alpha.to_css_string(args.span(), is_compressed)? ), QuoteKind::None, )); } _ => { return Ok(Value::String( function_string(name, &[color, alpha], visitor, args.span())?, QuoteKind::None, )) } } } else if alpha.is_special_function() { let color = color.assert_color_with_name("color", args.span())?; return Ok(Value::String( format!( "{}({}, {}, {}, {})", name, color.red().to_string(is_compressed), color.green().to_string(is_compressed), color.blue().to_string(is_compressed), alpha.to_css_string(args.span(), is_compressed)? ), QuoteKind::None, )); } let color = color.assert_color_with_name("color", args.span())?; let alpha = alpha.assert_number_with_name("alpha", args.span())?; Ok(Value::Color(Arc::new(color.with_alpha(Number( percentage_or_unitless(&alpha, 1.0, "alpha", args.span(), visitor)?, ))))) } fn inner_rgb_3_arg( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { let alpha = if args.len() > 3 { args.get(3, "alpha") } else { None }; let red = args.get_err(0, "red")?; let green = args.get_err(1, "green")?; let blue = args.get_err(2, "blue")?; if red.is_special_function() || green.is_special_function() || blue.is_special_function() || alpha .as_ref() .map(|alpha| alpha.node.is_special_function()) .unwrap_or(false) { let fn_string = if alpha.is_some() { function_string( name, &[red, green, blue, alpha.unwrap().node], visitor, args.span(), )? } else { function_string(name, &[red, green, blue], visitor, args.span())? }; return Ok(Value::String(fn_string, QuoteKind::None)); } let span = args.span(); let red = red.assert_number_with_name("red", span)?; let green = green.assert_number_with_name("green", span)?; let blue = blue.assert_number_with_name("blue", span)?; Ok(Value::Color(Arc::new(Color::from_rgba_fn( Number(fuzzy_round(percentage_or_unitless( &red, 255.0, "red", span, visitor, )?)), Number(fuzzy_round(percentage_or_unitless( &green, 255.0, "green", span, visitor, )?)), Number(fuzzy_round(percentage_or_unitless( &blue, 255.0, "blue", span, visitor, )?)), Number( alpha .map(|alpha| { percentage_or_unitless( &alpha.node.assert_number_with_name("alpha", span)?, 1.0, "alpha", span, visitor, ) }) .transpose()? .unwrap_or(1.0), ), )))) } pub(crate) fn percentage_or_unitless( number: &SassNumber, max: f64, name: &str, span: Span, visitor: &mut Visitor, ) -> SassResult { let value = if number.unit == Unit::None { number.num } else if number.unit == Unit::Percent { (number.num * Number(max)) / Number(100.0) } else { return Err(( format!( "${name}: Expected {} to have no units or \"%\".", inspect_number(number, visitor.options, span)?, name = name, ), span, ) .into()); }; Ok(value.clamp(0.0, max).0) } fn is_var_slash(value: &Value) -> bool { match value { Value::String(text, QuoteKind::Quoted) => { text.to_ascii_lowercase().starts_with("var(") && text.contains('/') } _ => false, } } pub(crate) fn parse_channels( name: &'static str, arg_names: &[&'static str], mut channels: Value, visitor: &mut Visitor, span: Span, ) -> SassResult { if channels.is_var() { let fn_string = function_string(name, &[channels], visitor, span)?; return Ok(ParsedChannels::String(fn_string)); } let original_channels = channels.clone(); let mut alpha_from_slash_list = None; if channels.separator() == ListSeparator::Slash { let list = channels.clone().as_list(); if list.len() != 2 { return Err(( format!( "Only 2 slash-separated elements allowed, but {} {} passed.", list.len(), if list.len() == 1 { "was" } else { "were" } ), span, ) .into()); } channels = list[0].clone(); let inner_alpha_from_slash_list = list[1].clone(); if !inner_alpha_from_slash_list.is_special_function() { inner_alpha_from_slash_list .clone() .assert_number_with_name("alpha", span)?; } alpha_from_slash_list = Some(inner_alpha_from_slash_list); if list[0].is_var() { let fn_string = function_string(name, &[original_channels], visitor, span)?; return Ok(ParsedChannels::String(fn_string)); } } let is_comma_separated = channels.separator() == ListSeparator::Comma; let is_bracketed = matches!(channels, Value::List(_, _, Brackets::Bracketed)); if is_comma_separated || is_bracketed { let mut err_buffer = "$channels must be".to_owned(); if is_bracketed { err_buffer.push_str(" an unbracketed"); } if is_comma_separated { if is_bracketed { err_buffer.push(','); } else { err_buffer.push_str(" a"); } err_buffer.push_str(" space-separated"); } err_buffer.push_str(" list."); return Err((err_buffer, span).into()); } let mut list = channels.clone().as_list(); if list.len() > 3 { return Err(( format!("Only 3 elements allowed, but {} were passed.", list.len()), span, ) .into()); } else if list.len() < 3 { if list.iter().any(Value::is_var) || (!list.is_empty() && is_var_slash(list.last().unwrap())) { let fn_string = function_string(name, &[original_channels], visitor, span)?; return Ok(ParsedChannels::String(fn_string)); } else { let argument = arg_names[list.len()]; return Err(( format!("Missing element ${argument}.", argument = argument), span, ) .into()); } } if let Some(alpha_from_slash_list) = alpha_from_slash_list { list.push(alpha_from_slash_list); return Ok(ParsedChannels::List(list)); } #[allow(clippy::collapsible_match)] match &list[2] { Value::Dimension(SassNumber { as_slash, .. }) => match as_slash { Some(slash) => Ok(ParsedChannels::List(vec![ list[0].clone(), list[1].clone(), // todo: superfluous clones Value::Dimension(slash.0.clone()), Value::Dimension(slash.1.clone()), ])), None => Ok(ParsedChannels::List(list)), }, Value::String(text, QuoteKind::None) if text.contains('/') => { let fn_string = function_string(name, &[channels], visitor, span)?; Ok(ParsedChannels::String(fn_string)) } _ => Ok(ParsedChannels::List(list)), } } fn inner_rgb( name: &'static str, mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(4)?; match args.len() { 0 | 1 => { match parse_channels( name, &["red", "green", "blue"], args.get_err(0, "channels")?, visitor, args.span(), )? { ParsedChannels::String(s) => Ok(Value::String(s, QuoteKind::None)), ParsedChannels::List(list) => { let args = ArgumentResult { positional: list, named: BTreeMap::new(), separator: ListSeparator::Comma, span: args.span(), touched: BTreeSet::new(), }; inner_rgb_3_arg(name, args, visitor) } } } 2 => inner_rgb_2_arg(name, args, visitor), _ => inner_rgb_3_arg(name, args, visitor), } } pub(crate) fn rgb(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_rgb("rgb", args, visitor) } pub(crate) fn rgba(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { inner_rgb("rgba", args, visitor) } pub(crate) fn red(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.red()))) } pub(crate) fn green(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.green()))) } pub(crate) fn blue(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let color = args .get_err(0, "color")? .assert_color_with_name("color", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless(color.blue()))) } pub(crate) fn mix(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let color1 = args .get_err(0, "color1")? .assert_color_with_name("color1", args.span())?; let color2 = args .get_err(1, "color2")? .assert_color_with_name("color2", args.span())?; let weight = match args.default_arg( 2, "weight", Value::Dimension(SassNumber::new_unitless(50.0)), ) { Value::Dimension(mut num) => { num.assert_bounds("weight", 0.0, 100.0, args.span())?; num.num /= Number(100.0); num.num } v => { return Err(( format!( "$weight: {} is not a number.", v.to_css_string(args.span(), visitor.options.is_compressed())? ), args.span(), ) .into()) } }; Ok(Value::Color(Arc::new(color1.mix(&color2, weight)))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("rgb", Builtin::new(rgb)); f.insert("rgba", Builtin::new(rgba)); f.insert("red", Builtin::new(red)); f.insert("green", Builtin::new(green)); f.insert("blue", Builtin::new(blue)); f.insert("mix", Builtin::new(mix)); } grass_compiler-0.13.4/src/builtin/functions/list.rs000064400000000000000000000203741046102023000205160ustar 00000000000000use crate::builtin::builtin_imports::*; pub(crate) fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let len = args.get_err(0, "list")?.as_list().len(); Ok(Value::Dimension(SassNumber::new_unitless(len))) } pub(crate) fn nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let mut list = args.get_err(0, "list")?.as_list(); let index = args .get_err(1, "n")? .assert_number_with_name("n", args.span())?; if index.num.is_zero() { return Err(("$n: List index may not be 0.", args.span()).into()); } if index.num.abs() > Number::from(list.len()) { return Err(( format!( "$n: Invalid index {}{} for a list with {} elements.", index.num.inspect(), index.unit, list.len() ), args.span(), ) .into()); } let index_int = index.assert_int_with_name("n", args.span())?; Ok(list.remove(if index.num.is_positive() { debug_assert!(index_int > 0); index_int as usize - 1 } else { list.len() - index_int.unsigned_abs() as usize })) } pub(crate) fn list_separator(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::String( args.get_err(0, "list")?.separator().name().to_owned(), QuoteKind::None, )) } pub(crate) fn set_nth(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), Value::ArgList(v) => ( v.elems.into_iter().collect(), ListSeparator::Comma, Brackets::None, ), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let index = args .get_err(1, "n")? .assert_number_with_name("n", args.span())?; if index.num.is_zero() { return Err(("$n: List index may not be 0.", args.span()).into()); } let index_int = index.assert_int_with_name("n", args.span())?; let len = list.len(); if index.num.abs() > Number::from(len) { return Err(( format!( "$n: Invalid index {}{} for a list with {} elements.", index.num.inspect(), index.unit, len ), args.span(), ) .into()); } let val = args.get_err(2, "value")?; if index_int.is_positive() { list[index_int as usize - 1] = val; } else { list[len - index_int.unsigned_abs() as usize] = val; } Ok(Value::List(list, sep, brackets)) } pub(crate) fn append(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let (mut list, sep, brackets) = match args.get_err(0, "list")? { Value::List(v, sep, b) => (v, sep, b), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let val = args.get_err(1, "val")?; let sep = match args.default_arg( 2, "separator", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => { if sep == ListSeparator::Undecided { ListSeparator::Space } else { sep } } "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, "slash" => ListSeparator::Slash, _ => { return Err(( "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".", args.span(), ) .into()) } }, v => { return Err(( format!("$separator: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; list.push(val); Ok(Value::List(list, sep, brackets)) } pub(crate) fn join(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(4)?; let (mut list1, sep1, brackets) = match args.get_err(0, "list1")? { Value::List(v, sep, brackets) => (v, sep, brackets), Value::Map(m) => (m.as_list(), ListSeparator::Comma, Brackets::None), v => (vec![v], ListSeparator::Undecided, Brackets::None), }; let (list2, sep2) = match args.get_err(1, "list2")? { Value::List(v, sep, ..) => (v, sep), Value::Map(m) => (m.as_list(), ListSeparator::Comma), v => (vec![v], ListSeparator::Undecided), }; let sep = match args.default_arg( 2, "separator", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => { if sep1 != ListSeparator::Undecided { sep1 } else if sep2 != ListSeparator::Undecided { sep2 } else { ListSeparator::Space } } "comma" => ListSeparator::Comma, "space" => ListSeparator::Space, "slash" => ListSeparator::Slash, _ => { return Err(( "$separator: Must be \"space\", \"comma\", \"slash\", or \"auto\".", args.span(), ) .into()) } }, v => { return Err(( format!("$separator: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let brackets = match args.default_arg( 3, "bracketed", Value::String("auto".to_owned(), QuoteKind::None), ) { Value::String(s, ..) => match s.as_str() { "auto" => brackets, _ => Brackets::Bracketed, }, v => { if v.is_truthy() { Brackets::Bracketed } else { Brackets::None } } }; list1.extend(list2); Ok(Value::List(list1, sep, brackets)) } pub(crate) fn is_bracketed(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::bool(match args.get_err(0, "list")? { Value::List(.., brackets) => match brackets { Brackets::Bracketed => true, Brackets::None => false, }, _ => false, })) } pub(crate) fn index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let list = args.get_err(0, "list")?.as_list(); let value = args.get_err(1, "value")?; let index = match list.into_iter().position(|v| v == value) { Some(v) => v + 1, None => return Ok(Value::Null), }; Ok(Value::Dimension(SassNumber::new_unitless(index))) } pub(crate) fn zip(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let lists = args .get_variadic()? .into_iter() .map(|x| x.node.as_list()) .collect::>>(); let len = lists.iter().map(Vec::len).min().unwrap_or(0); if len == 0 { return Ok(Value::List( Vec::new(), ListSeparator::Comma, Brackets::None, )); } let result = (0..len) .map(|i| { let items = lists.iter().map(|v| v[i].clone()).collect(); Value::List(items, ListSeparator::Space, Brackets::None) }) .collect(); Ok(Value::List(result, ListSeparator::Comma, Brackets::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("length", Builtin::new(length)); f.insert("nth", Builtin::new(nth)); f.insert("list-separator", Builtin::new(list_separator)); f.insert("set-nth", Builtin::new(set_nth)); f.insert("append", Builtin::new(append)); f.insert("join", Builtin::new(join)); f.insert("is-bracketed", Builtin::new(is_bracketed)); f.insert("index", Builtin::new(index)); f.insert("zip", Builtin::new(zip)); } grass_compiler-0.13.4/src/builtin/functions/map.rs000064400000000000000000000155531046102023000203230ustar 00000000000000use crate::builtin::builtin_imports::*; /// map.get($map, $key, $keys...) /// map-get($map, $key, $keys...) /// /// If $keys is empty, returns the value in $map associated with $key. /// If $map doesn’t have a value associated with $key, returns null. /// If $keys is not empty, follows the set of keys including $key and /// excluding the last key in $keys, from left to right, to find the /// nested map targeted for searching. /// Returns the value in the targeted map associated with the last key /// in $keys. /// Returns null if the map does not have a value associated with the /// key, or if any key in $keys is missing from a map or references a /// value that is not a map. /// /// https://sass-lang.com/documentation/modules/map/ pub(crate) fn map_get(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key = args.get_err(1, "key")?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; // since we already extracted the map and first key, // neither will be returned in the variadic args list let keys = args.get_variadic()?; let mut val = map.get(&key).unwrap_or(Value::Null); for key in keys { // if at any point we find a value that's not a map, // we return null let val_map = match val.try_map() { Some(val_map) => val_map, None => return Ok(Value::Null), }; val = val_map.get(&key).unwrap_or(Value::Null); } Ok(val) } pub(crate) fn map_has_key(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key = args.get_err(1, "key")?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; // since we already extracted the map and first key, // neither will be returned in the variadic args list let keys = args.get_variadic()?; let mut val = match map.get(&key) { Some(v) => v, None => return Ok(Value::False), }; for key in keys { // if at any point we find a value that's not a map, // we return null let val_map = match val.try_map() { Some(val_map) => val_map, None => return Ok(Value::False), }; val = match val_map.get(&key) { Some(v) => v, None => return Ok(Value::False), }; } Ok(Value::True) } pub(crate) fn map_keys(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; Ok(Value::List( map.keys(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn map_values(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; Ok(Value::List( map.values(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn map_merge(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { if args.len() == 1 { return Err(("Expected $args to contain a key.", args.span()).into()); } let map2_position = args.len().saturating_sub(1); let mut map1 = args .get_err(0, "map1")? .assert_map_with_name("map1", args.span())?; let map2 = args .get_err(map2_position, "map2")? .assert_map_with_name("map2", args.span())?; let keys = args.get_variadic()?; if keys.is_empty() { map1.merge(map2); } else { let mut current_map = map1.clone(); let mut map_queue = Vec::new(); for key in keys { match current_map.get(&key) { Some(Value::Map(m1)) => { current_map = m1.clone(); map_queue.push((key, m1)); } Some(..) | None => { current_map = SassMap::new(); map_queue.push((key, SassMap::new())); } } } match map_queue.last_mut() { Some((_, m)) => { m.merge(map2); } None => unreachable!(), }; while let Some((key, queued_map)) = map_queue.pop() { match map_queue.last_mut() { Some((_, map)) => { map.insert(key, Value::Map(queued_map)); } None => { map1.insert(key, Value::Map(queued_map)); break; } } } } Ok(Value::Map(map1)) } pub(crate) fn map_remove(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let mut map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; let keys = args.get_variadic()?; for key in keys { map.remove(&key); } Ok(Value::Map(map)) } pub(crate) fn map_set(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let key_position = args.len().saturating_sub(2); let value_position = args.len().saturating_sub(1); let mut map = args .get_err(0, "map")? .assert_map_with_name("map", args.span())?; let key = Spanned { node: args.get_err(key_position, "key")?, span: args.span(), }; let value = args.get_err(value_position, "value")?; let keys = args.get_variadic()?; if keys.is_empty() { map.insert(key, value); } else { let mut current_map = map.clone(); let mut map_queue = Vec::new(); for key in keys { match current_map.get(&key) { Some(Value::Map(m1)) => { current_map = m1.clone(); map_queue.push((key, m1)); } Some(..) | None => { current_map = SassMap::new(); map_queue.push((key, SassMap::new())); } } } match map_queue.last_mut() { Some((_, m)) => m.insert(key, value), None => unreachable!(), }; while let Some((key, queued_map)) = map_queue.pop() { match map_queue.last_mut() { Some((_, next_map)) => { next_map.insert(key, Value::Map(queued_map)); } None => { map.insert(key, Value::Map(queued_map)); break; } } } } Ok(Value::Map(map)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("map-get", Builtin::new(map_get)); f.insert("map-has-key", Builtin::new(map_has_key)); f.insert("map-keys", Builtin::new(map_keys)); f.insert("map-values", Builtin::new(map_values)); f.insert("map-merge", Builtin::new(map_merge)); f.insert("map-remove", Builtin::new(map_remove)); } grass_compiler-0.13.4/src/builtin/functions/math.rs000064400000000000000000000151611046102023000204720ustar 00000000000000use crate::{builtin::builtin_imports::*, evaluate::div}; pub(crate) fn percentage(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let num = args .get_err(0, "number")? .assert_number_with_name("number", args.span)?; num.assert_no_units("number", args.span)?; Ok(Value::Dimension(SassNumber { num: Number(num.num.0 * 100.0), unit: Unit::Percent, as_slash: None, })) } pub(crate) fn round(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.round(); Ok(Value::Dimension(number)) } pub(crate) fn ceil(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.ceil(); Ok(Value::Dimension(number)) } pub(crate) fn floor(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; if !number.num.is_finite() { return Err(("Infinity or NaN toInt", args.span()).into()); } number.num = number.num.floor(); Ok(Value::Dimension(number)) } pub(crate) fn abs(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let mut num = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; num.num = num.num.abs(); Ok(Value::Dimension(num)) } pub(crate) fn comparable(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let unit1 = args .get_err(0, "number1")? .assert_number_with_name("number1", args.span())? .unit; let unit2 = args .get_err(1, "number2")? .assert_number_with_name("number2", args.span())? .unit; Ok(Value::bool(unit1.comparable(&unit2))) } #[cfg(feature = "random")] pub(crate) fn random(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let limit = args.default_arg(0, "limit", Value::Null); if matches!(limit, Value::Null) { let mut rng = rand::thread_rng(); return Ok(Value::Dimension(SassNumber::new_unitless( rng.gen_range(0.0..1.0), ))); } let limit = limit.assert_number_with_name("limit", args.span())?; let limit_int = limit.assert_int_with_name("limit", args.span())?; let limit = limit.num; if limit.is_one() { return Ok(Value::Dimension(SassNumber::new_unitless(1.0))); } if limit.is_zero() || limit.is_negative() { return Err(( format!("$limit: Must be greater than 0, was {}.", limit.inspect()), args.span(), ) .into()); } let mut rng = rand::thread_rng(); Ok(Value::Dimension(SassNumber::new_unitless( rng.gen_range(0..limit_int) + 1, ))) } pub(crate) fn min(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { Value::Dimension(SassNumber { num: number, unit, as_slash: _, }) => Ok((number, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? .into_iter(); let mut min = match nums.next() { Some((n, u)) => (n, u), None => unreachable!(), }; for (num, unit) in nums { let lhs = Value::Dimension(SassNumber { num, unit: unit.clone(), as_slash: None, }); let rhs = Value::Dimension(SassNumber { num: (min.0), unit: min.1.clone(), as_slash: None, }); if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::LessThan)?.is_truthy() { min = (num, unit); } } Ok(Value::Dimension(SassNumber { num: (min.0), unit: min.1, as_slash: None, })) } pub(crate) fn max(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut nums = args .get_variadic()? .into_iter() .map(|val| match val.node { Value::Dimension(SassNumber { num: number, unit, as_slash: _, }) => Ok((number, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), }) .collect::>>()? .into_iter(); let mut max = match nums.next() { Some((n, u)) => (n, u), None => unreachable!(), }; for (num, unit) in nums { let lhs = Value::Dimension(SassNumber { num, unit: unit.clone(), as_slash: None, }); let rhs = Value::Dimension(SassNumber { num: (max.0), unit: max.1.clone(), as_slash: None, }); if crate::evaluate::cmp(&lhs, &rhs, visitor.options, span, BinaryOp::GreaterThan)? .is_truthy() { max = (num, unit); } } Ok(Value::Dimension(SassNumber { num: (max.0), unit: max.1, as_slash: None, })) } pub(crate) fn divide(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let number1 = args.get_err(0, "number1")?; let number2 = args.get_err(1, "number2")?; div(number1, number2, visitor.options, args.span()) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("percentage", Builtin::new(percentage)); f.insert("round", Builtin::new(round)); f.insert("ceil", Builtin::new(ceil)); f.insert("floor", Builtin::new(floor)); f.insert("abs", Builtin::new(abs)); f.insert("min", Builtin::new(min)); f.insert("max", Builtin::new(max)); f.insert("comparable", Builtin::new(comparable)); #[cfg(feature = "random")] f.insert("random", Builtin::new(random)); } grass_compiler-0.13.4/src/builtin/functions/meta.rs000064400000000000000000000251641046102023000204730ustar 00000000000000use crate::builtin::builtin_imports::*; // todo: this should be a constant of some sort. we shouldn't be allocating this // every time pub(crate) fn if_arguments() -> ArgumentDeclaration { ArgumentDeclaration { args: vec![ Argument { name: Identifier::from("condition"), default: None, }, Argument { name: Identifier::from("if-true"), default: None, }, Argument { name: Identifier::from("if-false"), default: None, }, ], rest: None, } } fn if_(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; if args.get_err(0, "condition")?.is_truthy() { Ok(args.get_err(1, "if-true")?) } else { Ok(args.get_err(2, "if-false")?) } } pub(crate) fn feature_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let feature = args .get_err(0, "feature")? .assert_string_with_name("feature", args.span())? .0; #[allow(clippy::match_same_arms)] Ok(match feature.as_str() { // A local variable will shadow a global variable unless // `!global` is used. "global-variable-shadowing" => Value::True, // the @extend rule will affect selectors nested in pseudo-classes // like :not() "extend-selector-pseudoclass" => Value::True, // Full support for unit arithmetic using units defined in the // [Values and Units Level 3][] spec. "units-level-3" => Value::True, // The Sass `@error` directive is supported. "at-error" => Value::True, // The "Custom Properties Level 1" spec is supported. This means // that custom properties are parsed statically, with only // interpolation treated as SassScript. "custom-property" => Value::True, _ => Value::False, }) } pub(crate) fn unit(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; Ok(Value::String(number.unit.to_string(), QuoteKind::Quoted)) } pub(crate) fn type_of(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let value = args.get_err(0, "value")?; Ok(Value::String(value.kind().to_owned(), QuoteKind::None)) } pub(crate) fn unitless(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; Ok(Value::bool(number.unit == Unit::None)) } pub(crate) fn inspect(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(Value::String( args.get_err(0, "value")?.inspect(args.span())?, QuoteKind::None, )) } pub(crate) fn variable_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(1)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); Ok(Value::bool(visitor.env.var_exists(name, None)?)) } pub(crate) fn global_variable_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .var_exists(name) } else { (*visitor.env.global_vars()).borrow().contains_key(&name) })) } pub(crate) fn mixin_exists(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .mixin_exists(name) } else { visitor.env.mixin_exists(name) })) } pub(crate) fn function_exists( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let name = Identifier::from( args.get_err(0, "name")? .assert_string_with_name("name", args.span())? .0, ); let module = match args.default_arg(1, "module", Value::Null) { Value::String(s, _) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::bool(if let Some(module_name) = module { (*(*visitor.env.modules) .borrow() .get(module_name.into(), args.span())?) .borrow() .fn_exists(name) } else { visitor.env.fn_exists(name) })) } pub(crate) fn get_function(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let name: Identifier = match args.get_err(0, "name")? { Value::String(s, _) => s.into(), v => { return Err(( format!("$name: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; let css = args.default_arg(1, "css", Value::False).is_truthy(); let module = match args.default_arg(2, "module", Value::Null) { Value::String(s, ..) => Some(s), Value::Null => None, v => { return Err(( format!("$module: {} is not a string.", v.inspect(args.span())?), args.span(), ) .into()) } }; if css && module.is_some() { return Err(( "$css and $module may not both be passed at once.", args.span(), ) .into()); } let func = if css { Some(SassFunction::Plain { name }) } else if let Some(module_name) = module { visitor.env.get_fn( name, Some(Spanned { node: module_name.into(), span: args.span(), }), )? } else { match visitor.env.get_fn(name, None)? { Some(f) => Some(f), None => GLOBAL_FUNCTIONS .get(name.as_str()) .map(|f| SassFunction::Builtin(f.clone(), name)), } }; match func { Some(func) => Ok(Value::FunctionRef(Box::new(func))), None => Err((format!("Function not found: {}", name), args.span()).into()), } } pub(crate) fn call(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let func = match args.get_err(0, "function")? { Value::FunctionRef(f) => *f, Value::String(name, ..) => { let name = Identifier::from(name); match visitor.env.get_fn(name, None)? { Some(f) => f, None => match GLOBAL_FUNCTIONS.get(name.as_str()) { Some(f) => SassFunction::Builtin(f.clone(), name), None => SassFunction::Plain { name }, }, } } v => { return Err(( format!( "$function: {} is not a function reference.", v.inspect(span)? ), span, ) .into()) } }; args.remove_positional(0); visitor.run_function_callable_with_maybe_evaled(func, MaybeEvaledArguments::Evaled(args), span) } #[allow(clippy::needless_pass_by_value)] pub(crate) fn content_exists(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(0)?; if !visitor.flags.in_mixin() { return Err(( "content-exists() may only be called within a mixin.", args.span(), ) .into()); } Ok(Value::bool(visitor.env.content.is_some())) } pub(crate) fn keywords(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let args = match args.get_err(0, "args")? { Value::ArgList(args) => args, v => { return Err(( format!("$args: {} is not an argument list.", v.inspect(span)?), span, ) .into()) } }; Ok(Value::Map(SassMap::new_with( args.into_keywords() .into_iter() .map(|(name, val)| { ( Value::String(name.to_string(), QuoteKind::None).span(span), val, ) }) .collect(), ))) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("if", Builtin::new(if_)); f.insert("feature-exists", Builtin::new(feature_exists)); f.insert("unit", Builtin::new(unit)); f.insert("type-of", Builtin::new(type_of)); f.insert("unitless", Builtin::new(unitless)); f.insert("inspect", Builtin::new(inspect)); f.insert("variable-exists", Builtin::new(variable_exists)); f.insert( "global-variable-exists", Builtin::new(global_variable_exists), ); f.insert("mixin-exists", Builtin::new(mixin_exists)); f.insert("function-exists", Builtin::new(function_exists)); f.insert("get-function", Builtin::new(get_function)); f.insert("call", Builtin::new(call)); f.insert("content-exists", Builtin::new(content_exists)); f.insert("keywords", Builtin::new(keywords)); } grass_compiler-0.13.4/src/builtin/functions/mod.rs000064400000000000000000000057011046102023000203170ustar 00000000000000// A reference to the parser is only necessary for some functions #![allow(unused_variables)] use std::{ collections::{BTreeSet, HashMap}, fmt, sync::atomic::{AtomicUsize, Ordering}, }; use once_cell::sync::Lazy; use crate::{ast::ArgumentResult, error::SassResult, evaluate::Visitor, value::Value}; pub mod color; pub mod list; pub mod map; pub mod math; pub mod meta; pub mod selector; pub mod string; // todo: maybe Identifier instead of str? pub(crate) type GlobalFunctionMap = HashMap<&'static str, Builtin>; static FUNCTION_COUNT: AtomicUsize = AtomicUsize::new(0); /// A function implemented in rust that is accessible from within Sass /// /// /// #### Usage /// ```rust /// use grass_compiler::{ /// sass_value::{ArgumentResult, SassNumber, Value}, /// Builtin, Options, Result as SassResult, Visitor, /// }; /// /// // An example function that looks up the length of an array or map and adds 2 to it /// fn length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { /// args.max_args(1)?; /// /// let len = args.get_err(0, "list")?.as_list().len(); /// /// Ok(Value::Dimension(SassNumber::new_unitless(len + 2))) /// } /// /// fn main() { /// let options = Options::default().add_custom_fn("length", Builtin::new(length)); /// let css = grass_compiler::from_string("a { color: length([a, b]); }", &options).unwrap(); /// /// assert_eq!(css, "a {\n color: 4;\n}\n"); /// } /// ``` #[derive(Clone)] pub struct Builtin( pub(crate) fn(ArgumentResult, &mut Visitor) -> SassResult, usize, ); impl fmt::Debug for Builtin { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("Builtin") .field("id", &self.1) .field("fn_ptr", &(self.0 as usize)) .finish() } } impl Builtin { pub fn new(body: fn(ArgumentResult, &mut Visitor) -> SassResult) -> Builtin { let count = FUNCTION_COUNT.fetch_add(1, Ordering::Relaxed); Self(body, count) } } impl PartialEq for Builtin { fn eq(&self, other: &Self) -> bool { self.1 == other.1 } } impl Eq for Builtin {} pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { let mut m = HashMap::new(); color::declare(&mut m); list::declare(&mut m); map::declare(&mut m); math::declare(&mut m); meta::declare(&mut m); selector::declare(&mut m); string::declare(&mut m); m }); pub(crate) static DISALLOWED_PLAIN_CSS_FUNCTION_NAMES: Lazy> = Lazy::new(|| { GLOBAL_FUNCTIONS .keys() .copied() .filter(|&name| { !matches!( name, "rgb" | "rgba" | "hsl" | "hsla" | "grayscale" | "invert" | "alpha" | "opacity" | "saturate" ) }) .collect() }); grass_compiler-0.13.4/src/builtin/functions/selector.rs000064400000000000000000000176271046102023000213720ustar 00000000000000use crate::builtin::builtin_imports::*; use crate::selector::{ ComplexSelector, ComplexSelectorComponent, ExtensionStore, Selector, SelectorList, }; use crate::serializer::serialize_selector_list; pub(crate) fn is_superselector( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(2)?; let parent_selector = args.get_err(0, "super")? .to_selector(visitor, "super", false, args.span())?; let child_selector = args .get_err(1, "sub")? .to_selector(visitor, "sub", false, args.span())?; Ok(Value::bool( parent_selector.is_super_selector(&child_selector), )) } pub(crate) fn simple_selectors( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(1)?; // todo: Value::to_compound_selector let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span())?; if selector.0.components.len() != 1 { return Err(("$selector: expected selector.", args.span()).into()); } let compound = if let Some(ComplexSelectorComponent::Compound(compound)) = selector.0.components[0].components.first().cloned() { compound } else { todo!() }; Ok(Value::List( compound .components .into_iter() .map(|simple| Value::String(simple.to_string(), QuoteKind::None)) .collect(), ListSeparator::Comma, Brackets::None, )) } pub(crate) fn selector_parse(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; Ok(args .get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span()) .map_err(|_| ("$selector: expected selector.", args.span()))? .into_value()) } pub(crate) fn selector_nest(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { return Err(("$selectors: At least one selector must be passed.", span).into()); } Ok(selectors .into_iter() .map(|sel| sel.node.to_selector(visitor, "selectors", true, span)) .collect::>>()? .into_iter() .try_fold( Selector::new(span), |parent, child| -> SassResult { child.resolve_parent_selectors(&parent, true) }, )? .into_value()) } pub(crate) fn selector_append(args: ArgumentResult, visitor: &mut Visitor) -> SassResult { let span = args.span(); let selectors = args.get_variadic()?; if selectors.is_empty() { return Err(("$selectors: At least one selector must be passed.", span).into()); } let mut parsed_selectors = selectors .into_iter() .map(|s| s.node.to_selector(visitor, "selectors", false, span)) .collect::>>()?; let first = parsed_selectors.remove(0); Ok(parsed_selectors .into_iter() .try_fold(first, |parent, child| -> SassResult { Selector(SelectorList { components: child .0 .components .into_iter() .map(|complex| -> SassResult { let compound = complex.components.first(); if let Some(ComplexSelectorComponent::Compound(compound)) = compound { let mut components = vec![match compound.clone().prepend_parent() { Some(v) => ComplexSelectorComponent::Compound(v), None => { return Err(( format!( "Can't append {} to {}.", complex, serialize_selector_list( &parent.0, visitor.options, span ) ), span, ) .into()) } }]; components.extend(complex.components.into_iter().skip(1)); Ok(ComplexSelector::new(components, false)) } else { Err(( format!( "Can't append {} to {}.", complex, serialize_selector_list(&parent.0, visitor.options, span) ), span, ) .into()) } }) .collect::>>()?, span, }) .resolve_parent_selectors(&parent, false) })? .into_value()) } pub(crate) fn selector_extend( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(3)?; let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", false, args.span())?; let target = args.get_err(1, "extendee")? .to_selector(visitor, "extendee", false, args.span())?; let source = args.get_err(2, "extender")? .to_selector(visitor, "extender", false, args.span())?; Ok(ExtensionStore::extend(selector.0, source.0, target.0, args.span())?.to_sass_list()) } pub(crate) fn selector_replace( mut args: ArgumentResult, visitor: &mut Visitor, ) -> SassResult { args.max_args(3)?; let selector = args.get_err(0, "selector")? .to_selector(visitor, "selector", true, args.span())?; let target = args.get_err(1, "original")? .to_selector(visitor, "original", true, args.span())?; let source = args.get_err(2, "replacement")? .to_selector(visitor, "replacement", true, args.span())?; Ok(ExtensionStore::replace(selector.0, source.0, target.0, args.span())?.to_sass_list()) } pub(crate) fn selector_unify(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let selector1 = args.get_err(0, "selector1")? .to_selector(visitor, "selector1", true, args.span())?; if selector1.contains_parent_selector() { return Err(( "$selector1: Parent selectors aren't allowed here.", args.span(), ) .into()); } let selector2 = args.get_err(1, "selector2")? .to_selector(visitor, "selector2", true, args.span())?; if selector2.contains_parent_selector() { return Err(( "$selector2: Parent selectors aren't allowed here.", args.span(), ) .into()); } Ok(match selector1.unify(&selector2) { Some(sel) => sel.into_value(), None => Value::Null, }) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("is-superselector", Builtin::new(is_superselector)); f.insert("simple-selectors", Builtin::new(simple_selectors)); f.insert("selector-parse", Builtin::new(selector_parse)); f.insert("selector-nest", Builtin::new(selector_nest)); f.insert("selector-append", Builtin::new(selector_append)); f.insert("selector-extend", Builtin::new(selector_extend)); f.insert("selector-replace", Builtin::new(selector_replace)); f.insert("selector-unify", Builtin::new(selector_unify)); } grass_compiler-0.13.4/src/builtin/functions/string.rs000064400000000000000000000171271046102023000210530ustar 00000000000000use crate::builtin::builtin_imports::*; pub(crate) fn to_upper_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let (mut s, q) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; s.make_ascii_uppercase(); Ok(Value::String(s, q)) } pub(crate) fn to_lower_case(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let (mut s, q) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; s.make_ascii_lowercase(); Ok(Value::String(s, q)) } pub(crate) fn str_length(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::Dimension(SassNumber::new_unitless( s.chars().count(), ))) } pub(crate) fn quote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::String(s, QuoteKind::Quoted)) } pub(crate) fn unquote(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let s = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; Ok(Value::String(s, QuoteKind::None)) } pub(crate) fn str_slice(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let (string, quotes) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; let str_len = string.chars().count(); let start = args .get_err(1, "start-at")? .assert_number_with_name("start-at", span)?; start.assert_no_units("start-at", span)?; let start = start.num.assert_int(span)?; let start = if start == 0 { 1 } else if start > 0 { (start as usize).min(str_len + 1) } else { (start + str_len as i64 + 1).max(1) as usize }; let end = args .default_arg( 2, "end-at", Value::Dimension(SassNumber::new_unitless(-1.0)), ) .assert_number_with_name("end-at", span)?; end.assert_no_units("end-at", span)?; let mut end = end.num.assert_int(span)?; if end < 0 { end += str_len as i64 + 1; } let end = (end.max(0) as usize).min(str_len + 1); if start > end || start > str_len { Ok(Value::String(String::new(), quotes)) } else { Ok(Value::String( string .chars() .skip(start - 1) .take(end - start + 1) .collect(), quotes, )) } } /// https://sass-lang.com/documentation/modules/string/#split /// /// Returns a bracketed, comma-separated list of substrings of $string /// that are separated by $separator. The $separators aren’t included /// in these substrings. /// /// If $limit is a number 1 or higher, this splits on at most that many /// $separators (and so returns at most $limit + 1 strings). The last /// substring contains the rest of the string, including any remaining /// $separators. pub(crate) fn str_split(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let s1 = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; let separator = args .get_err(1, "separator")? .assert_string_with_name("separator", args.span())? .0; let limit = args.default_arg(2, "limit", Value::Null); let vec = if matches!(limit, Value::Null) { s1.split(&separator) .map(|s| Value::String(s.to_string(), QuoteKind::Quoted)) .collect() } else { let limit = limit.assert_number_with_name("limit", args.span())?; let limit_int = limit.assert_int_with_name("limit", args.span())?; if limit_int < 1 { return Err(( format!("$limit: Must be 1 or greater, was {}.", limit_int), args.span(), ) .into()); } // note: `1 + limit_int` is required to match dart-sass s1.splitn(1 + limit_int as usize, &separator) .map(|s| Value::String(s.to_string(), QuoteKind::Quoted)) .collect() }; Ok(Value::List(vec, ListSeparator::Comma, Brackets::Bracketed)) } pub(crate) fn str_index(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(2)?; let s1 = args .get_err(0, "string")? .assert_string_with_name("string", args.span())? .0; let substr = args .get_err(1, "substring")? .assert_string_with_name("substring", args.span())? .0; let char_position = match s1.find(&substr) { Some(i) => s1[0..i].chars().count() + 1, None => return Ok(Value::Null), }; Ok(Value::Dimension(SassNumber::new_unitless(char_position))) } pub(crate) fn str_insert(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let (s1, quotes) = args .get_err(0, "string")? .assert_string_with_name("string", args.span())?; let substr = args .get_err(1, "insert")? .assert_string_with_name("insert", args.span())? .0; let index = args .get_err(2, "index")? .assert_number_with_name("index", span)?; index.assert_no_units("index", span)?; let index_int = index.assert_int_with_name("index", span)?; if s1.is_empty() { return Ok(Value::String(substr, quotes)); } let len = s1.chars().count(); // Insert substring at char position, rather than byte position let insert = |idx, s1: String, s2| { s1.chars() .enumerate() .map(|(i, c)| { if i + 1 == idx { c.to_string() + s2 } else if idx == 0 && i == 0 { s2.to_owned() + &c.to_string() } else { c.to_string() } }) .collect::() }; let string = if index_int > 0 { insert((index_int as usize - 1).min(len), s1, &substr) } else if index_int == 0 { insert(0, s1, &substr) } else { let idx = (len as i64 + index_int + 1).max(0) as usize; insert(idx, s1, &substr) }; Ok(Value::String(string, quotes)) } #[cfg(feature = "random")] #[allow(clippy::needless_pass_by_value)] pub(crate) fn unique_id(args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(0)?; let mut rng = thread_rng(); let string: String = std::iter::repeat(()) .map(|()| rng.sample(Alphanumeric)) .map(char::from) .take(12) .collect(); Ok(Value::String(format!("id-{}", string), QuoteKind::None)) } pub(crate) fn declare(f: &mut GlobalFunctionMap) { f.insert("to-upper-case", Builtin::new(to_upper_case)); f.insert("to-lower-case", Builtin::new(to_lower_case)); f.insert("str-length", Builtin::new(str_length)); f.insert("quote", Builtin::new(quote)); f.insert("unquote", Builtin::new(unquote)); f.insert("str-slice", Builtin::new(str_slice)); f.insert("str-index", Builtin::new(str_index)); f.insert("str-insert", Builtin::new(str_insert)); #[cfg(feature = "random")] f.insert("unique-id", Builtin::new(unique_id)); } grass_compiler-0.13.4/src/builtin/mod.rs000064400000000000000000000017751046102023000163160ustar 00000000000000mod functions; pub(crate) mod modules; pub(crate) use functions::{ color, list, map, math, meta, selector, string, DISALLOWED_PLAIN_CSS_FUNCTION_NAMES, GLOBAL_FUNCTIONS, }; pub use functions::Builtin; /// Imports common to all builtin fns mod builtin_imports { pub(crate) use super::functions::{Builtin, GlobalFunctionMap, GLOBAL_FUNCTIONS}; pub(crate) use codemap::{Span, Spanned}; #[cfg(feature = "random")] pub(crate) use rand::{distributions::Alphanumeric, thread_rng, Rng}; pub(crate) use crate::{ ast::{Argument, ArgumentDeclaration, ArgumentResult, MaybeEvaledArguments}, color::Color, common::{BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind}, error::SassResult, evaluate::Visitor, unit::Unit, value::{CalculationArg, Number, SassFunction, SassMap, SassNumber, Value}, Options, }; pub(crate) use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, sync::Arc, }; } grass_compiler-0.13.4/src/builtin/modules/color.rs000064400000000000000000000021251046102023000203130ustar 00000000000000use crate::builtin::{ color::{ hsl::{complement, grayscale, hue, invert, lightness, saturation}, hwb::{blackness, hwb, whiteness}, opacity::alpha, other::{adjust_color, change_color, ie_hex_str, scale_color}, rgb::{blue, green, mix, red}, }, modules::Module, }; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("adjust", adjust_color); f.insert_builtin("alpha", alpha); f.insert_builtin("blue", blue); f.insert_builtin("change", change_color); f.insert_builtin("complement", complement); f.insert_builtin("grayscale", grayscale); f.insert_builtin("green", green); f.insert_builtin("hue", hue); f.insert_builtin("ie-hex-str", ie_hex_str); f.insert_builtin("invert", invert); f.insert_builtin("lightness", lightness); f.insert_builtin("mix", mix); f.insert_builtin("red", red); f.insert_builtin("saturation", saturation); f.insert_builtin("scale", scale_color); f.insert_builtin("blackness", blackness); f.insert_builtin("whiteness", whiteness); f.insert_builtin("hwb", hwb); } grass_compiler-0.13.4/src/builtin/modules/list.rs000064400000000000000000000022301046102023000201450ustar 00000000000000use crate::builtin::builtin_imports::*; use crate::builtin::{ list::{append, index, is_bracketed, join, length, list_separator, nth, set_nth, zip}, modules::Module, }; // todo: write tests for this fn slash(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let list = if args.len() == 1 { args.get_err(0, "elements")?.as_list() } else { args.get_variadic()? .into_iter() .map(|arg| arg.node) .collect() }; if list.len() < 2 { return Err(("At least two elements are required.", span).into()); } Ok(Value::List(list, ListSeparator::Slash, Brackets::None)) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("append", append); f.insert_builtin("index", index); f.insert_builtin("is-bracketed", is_bracketed); f.insert_builtin("join", join); f.insert_builtin("length", length); f.insert_builtin("separator", list_separator); f.insert_builtin("nth", nth); f.insert_builtin("set-nth", set_nth); f.insert_builtin("zip", zip); f.insert_builtin("slash", slash); } grass_compiler-0.13.4/src/builtin/modules/map.rs000064400000000000000000000077061046102023000177640ustar 00000000000000use std::iter::Peekable; use crate::builtin::builtin_imports::*; use crate::builtin::{ map::{map_get, map_has_key, map_keys, map_merge, map_remove, map_set, map_values}, modules::Module, }; fn deep_merge_impl(map1: SassMap, map2: SassMap) -> SassMap { if map1.is_empty() { return map2; } if map2.is_empty() { return map1; } let mut result = map1; for (key, value) in map2 { match result.get_ref(&key.node).and_then(Value::try_map) { Some(result_map) => match value.try_map() { Some(value_map) => { let merged = deep_merge_impl(result_map, value_map); result.insert(key, Value::Map(merged)); } None => { result.insert(key, value); } }, None => { result.insert(key, value); } } } result } fn deep_merge(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let map1 = args .get_err(0, "map1")? .assert_map_with_name("map1", span)?; let map2 = args .get_err(1, "map2")? .assert_map_with_name("map2", span)?; Ok(Value::Map(deep_merge_impl(map1, map2))) } fn deep_remove(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { let span = args.span(); let map = args.get_err(0, "map")?.assert_map_with_name("map", span)?; let key = args.get_err(1, "key")?; let keys = args.get_variadic()?.into_iter().map(|arg| arg.node); let mut keys = std::iter::once(key).chain(keys).collect::>(); let last = keys.pop(); let map = modify_map( map, keys.into_iter(), |value| { let last = match last.as_ref() { Some(v) => v, None => return value, }; match value.try_map() { Some(mut nested_map) if nested_map.contains(last) => { nested_map.remove(last); Value::Map(nested_map) } Some(..) | None => value, } }, false, span, ); Ok(map) } fn modify_map( map: SassMap, keys: impl Iterator, modify: impl Fn(Value) -> Value, // default=true add_nesting: bool, span: Span, ) -> Value { let mut keys = keys.peekable(); fn modify_nested_map( mut mutable_map: SassMap, mut keys: Peekable>, add_nesting: bool, span: Span, modify: impl Fn(Value) -> Value, ) -> SassMap { let key = keys.next().unwrap(); if keys.peek().is_none() { let value = modify(mutable_map.get_ref(&key).cloned().unwrap_or(Value::Null)); mutable_map.insert(key.span(span), value); return mutable_map; } let nested_map = mutable_map.get_ref(&key).and_then(|v| v.try_map()); if nested_map.is_none() && !add_nesting { return mutable_map; } mutable_map.insert( key.span(span), Value::Map(modify_nested_map( nested_map.unwrap_or_default(), keys, add_nesting, span, modify, )), ); mutable_map } if keys.peek().is_some() { Value::Map(modify_nested_map(map, keys, add_nesting, span, modify)) } else { modify(Value::Map(map)) } } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("get", map_get); f.insert_builtin("has-key", map_has_key); f.insert_builtin("keys", map_keys); f.insert_builtin("merge", map_merge); f.insert_builtin("remove", map_remove); f.insert_builtin("values", map_values); f.insert_builtin("set", map_set); f.insert_builtin("deep-merge", deep_merge); f.insert_builtin("deep-remove", deep_remove); } grass_compiler-0.13.4/src/builtin/modules/math.rs000064400000000000000000000346231046102023000201360ustar 00000000000000use crate::builtin::builtin_imports::*; use crate::builtin::{ math::{abs, ceil, comparable, divide, floor, max, min, percentage, round}, meta::{unit, unitless}, modules::Module, }; #[cfg(feature = "random")] use crate::builtin::math::random; use crate::value::{conversion_factor, SassNumber}; fn coerce_to_rad(num: f64, unit: Unit) -> f64 { debug_assert!(matches!( unit, Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn )); if unit == Unit::None { return num; } let factor = conversion_factor(&unit, &Unit::Rad).unwrap(); num * factor } fn clamp(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(3)?; let span = args.span(); let min = match args.get_err(0, "min")? { v @ Value::Dimension(SassNumber { .. }) => v, v => { return Err(( format!("$min: {} is not a number.", v.inspect(args.span())?), span, ) .into()) } }; let number = match args.get_err(1, "number")? { v @ Value::Dimension(SassNumber { .. }) => v, v => { return Err(( format!("$number: {} is not a number.", v.inspect(span)?), span, ) .into()) } }; let max = match args.get_err(2, "max")? { v @ Value::Dimension(SassNumber { .. }) => v, v => return Err((format!("$max: {} is not a number.", v.inspect(span)?), span).into()), }; // ensure that `min` and `max` are compatible min.cmp(&max, span, BinaryOp::LessThan)?; let min_unit = match min { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; let number_unit = match number { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; let max_unit = match max { Value::Dimension(SassNumber { num: _, unit: ref u, as_slash: _, }) => u, _ => unreachable!(), }; if min_unit == &Unit::None && number_unit != &Unit::None { return Err(( format!( "$min is unitless but $number has unit {}. Arguments must all have units or all be unitless.", number_unit ), span).into()); } else if min_unit != &Unit::None && number_unit == &Unit::None { return Err(( format!( "$min has unit {} but $number is unitless. Arguments must all have units or all be unitless.", min_unit ), span).into()); } else if min_unit != &Unit::None && max_unit == &Unit::None { return Err(( format!( "$min has unit {} but $max is unitless. Arguments must all have units or all be unitless.", min_unit ), span).into()); } match min.cmp(&number, span, BinaryOp::LessThan)? { Some(Ordering::Greater) => return Ok(min), Some(Ordering::Equal) => return Ok(number), Some(Ordering::Less) | None => {} } match max.cmp(&number, span, BinaryOp::GreaterThan)? { Some(Ordering::Less) => return Ok(max), Some(Ordering::Equal) => return Ok(number), Some(Ordering::Greater) | None => {} } Ok(number) } fn hypot(args: ArgumentResult, _: &mut Visitor) -> SassResult { args.min_args(1)?; let span = args.span(); let mut numbers = args.get_variadic()?.into_iter().map(|v| -> SassResult<_> { match v.node { Value::Dimension(SassNumber { num, unit, .. }) => Ok((num, unit)), v => Err((format!("{} is not a number.", v.inspect(span)?), span).into()), } }); let (n, u) = numbers.next().unwrap()?; let first: (Number, Unit) = (n * n, u); let rest = numbers .enumerate() .map(|(idx, val)| -> SassResult { let (number, unit) = val?; if first.1 == Unit::None { if unit == Unit::None { Ok(number * number) } else { Err(( format!( "Argument 1 is unitless but argument {} has unit {}. \ Arguments must all have units or all be unitless.", idx + 2, unit ), span, ) .into()) } } else if unit == Unit::None { Err(( format!( "Argument 1 has unit {} but argument {} is unitless. \ Arguments must all have units or all be unitless.", first.1, idx + 2, ), span, ) .into()) } else if first.1.comparable(&unit) { let n = number.convert(&unit, &first.1); Ok(n * n) } else { Err(( format!("Incompatible units {} and {}.", first.1, unit), span, ) .into()) } }) .collect::>>()?; let sum = first.0 + rest.into_iter().fold(Number::zero(), |a, b| a + b); Ok(Value::Dimension(SassNumber { num: sum.sqrt(), unit: first.1, as_slash: None, })) } fn log(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; let base = match args.default_arg(1, "base", Value::Null) { Value::Null => None, v => { let base = v.assert_number_with_name("base", span)?; base.assert_no_units("base", span)?; Some(base.num) } }; Ok(Value::Dimension(SassNumber::new_unitless( if let Some(base) = base { if base.is_zero() { Number::zero() } else { number.log(base) } // todo: test with negative 0 } else if number.is_negative() && !number.is_zero() { Number(f64::NAN) } else if number.is_zero() { Number(f64::NEG_INFINITY) } else { number.ln() }, ))) } fn pow(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let span = args.span(); let base = args .get_err(0, "base")? .assert_number_with_name("base", span)?; base.assert_no_units("base", span)?; let exponent = args .get_err(1, "exponent")? .assert_number_with_name("exponent", span)?; exponent.assert_no_units("exponent", span)?; Ok(Value::Dimension(SassNumber::new_unitless( base.num.pow(exponent.num), ))) } fn sqrt(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args .get_err(0, "number")? .assert_number_with_name("number", args.span())?; number.assert_no_units("number", args.span())?; Ok(Value::Dimension(SassNumber::new_unitless( number.num.sqrt(), ))) } macro_rules! trig_fn { ($name:ident) => { fn $name(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let number = args.get_err(0, "number")?; Ok(match number { Value::Dimension(SassNumber { num, unit: unit @ (Unit::None | Unit::Rad | Unit::Deg | Unit::Grad | Unit::Turn), .. }) => { Value::Dimension(SassNumber::new_unitless(coerce_to_rad(num.0, unit).$name())) } v @ Value::Dimension(..) => { return Err(( format!( "$number: Expected {} to have an angle unit (deg, grad, rad, turn).", v.inspect(args.span())? ), args.span(), ) .into()) } v => { return Err(( format!("$number: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }) } }; } trig_fn!(cos); trig_fn!(sin); trig_fn!(tan); fn acos(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; Ok(Value::Dimension(SassNumber { num: if number > Number(1.0) || number < Number(-1.0) { Number(f64::NAN) } else if number.is_one() { Number::zero() } else { number.acos() }, unit: Unit::Deg, as_slash: None, })) } fn asin(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; let number = number.num; if number > Number(1.0) || number < Number(-1.0) { return Ok(Value::Dimension(SassNumber { num: Number(f64::NAN), unit: Unit::Deg, as_slash: None, })); } else if number.is_zero() { return Ok(Value::Dimension(SassNumber { num: Number::zero(), unit: Unit::Deg, as_slash: None, })); } Ok(Value::Dimension(SassNumber { num: number.asin(), unit: Unit::Deg, as_slash: None, })) } fn atan(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(1)?; let span = args.span(); let number = args .get_err(0, "number")? .assert_number_with_name("number", span)?; number.assert_no_units("number", span)?; if number.num.is_zero() { return Ok(Value::Dimension(SassNumber { num: (Number::zero()), unit: Unit::Deg, as_slash: None, })); } Ok(Value::Dimension(SassNumber { num: number.num.atan(), unit: Unit::Deg, as_slash: None, })) } fn atan2(mut args: ArgumentResult, _: &mut Visitor) -> SassResult { args.max_args(2)?; let (y_num, y_unit) = match args.get_err(0, "y")? { Value::Dimension(SassNumber { num: n, unit: u, .. }) => (n, u), v => { return Err(( format!("$y: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; let (x_num, x_unit) = match args.get_err(1, "x")? { Value::Dimension(SassNumber { num: n, unit: u, .. }) => (n, u), v => { return Err(( format!("$x: {} is not a number.", v.inspect(args.span())?), args.span(), ) .into()) } }; let (x_num, y_num) = if x_unit == Unit::None && y_unit == Unit::None { (x_num, y_num) } else if y_unit == Unit::None { return Err(( format!( "$y is unitless but $x has unit {}. \ Arguments must all have units or all be unitless.", x_unit ), args.span(), ) .into()); } else if x_unit == Unit::None { return Err(( format!( "$y has unit {} but $x is unitless. \ Arguments must all have units or all be unitless.", y_unit ), args.span(), ) .into()); } else if x_unit.comparable(&y_unit) { (x_num, y_num.convert(&y_unit, &x_unit)) } else { return Err(( format!("Incompatible units {} and {}.", y_unit, x_unit), args.span(), ) .into()); }; Ok(Value::Dimension(SassNumber { num: Number(y_num.0.atan2(x_num.0).to_degrees()), unit: Unit::Deg, as_slash: None, })) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("ceil", ceil); f.insert_builtin("floor", floor); f.insert_builtin("max", max); f.insert_builtin("min", min); f.insert_builtin("round", round); f.insert_builtin("abs", abs); f.insert_builtin("compatible", comparable); f.insert_builtin("is-unitless", unitless); f.insert_builtin("unit", unit); f.insert_builtin("percentage", percentage); f.insert_builtin("clamp", clamp); f.insert_builtin("sqrt", sqrt); f.insert_builtin("cos", cos); f.insert_builtin("sin", sin); f.insert_builtin("tan", tan); f.insert_builtin("acos", acos); f.insert_builtin("asin", asin); f.insert_builtin("atan", atan); f.insert_builtin("log", log); f.insert_builtin("pow", pow); f.insert_builtin("hypot", hypot); f.insert_builtin("div", divide); f.insert_builtin("atan2", atan2); #[cfg(feature = "random")] f.insert_builtin("random", random); f.insert_builtin_var( "e", Value::Dimension(SassNumber::new_unitless(std::f64::consts::E)), ); f.insert_builtin_var( "pi", Value::Dimension(SassNumber::new_unitless(std::f64::consts::PI)), ); f.insert_builtin_var( "epsilon", Value::Dimension(SassNumber::new_unitless(f64::EPSILON)), ); f.insert_builtin_var( "max-safe-integer", Value::Dimension(SassNumber::new_unitless(9007199254740991.0)), ); f.insert_builtin_var( "min-safe-integer", Value::Dimension(SassNumber::new_unitless(-9007199254740991.0)), ); f.insert_builtin_var( "max-number", Value::Dimension(SassNumber::new_unitless(f64::MAX)), ); f.insert_builtin_var( "min-number", Value::Dimension(SassNumber::new_unitless(f64::MIN_POSITIVE)), ); } grass_compiler-0.13.4/src/builtin/modules/meta.rs000064400000000000000000000135171046102023000201320ustar 00000000000000use std::cell::RefCell; use std::collections::BTreeMap; use std::sync::Arc; use crate::ast::{Configuration, ConfiguredValue}; use crate::builtin::builtin_imports::*; use crate::builtin::{ meta::{ call, content_exists, feature_exists, function_exists, get_function, global_variable_exists, inspect, keywords, mixin_exists, type_of, variable_exists, }, modules::Module, }; use crate::serializer::serialize_calculation_arg; fn load_css(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult<()> { args.max_args(2)?; let span = args.span(); let url = args .get_err(0, "module")? .assert_string_with_name("module", args.span())? .0; let with = match args.default_arg(1, "with", Value::Null) { Value::Map(map) => Some(map), Value::List(v, ..) if v.is_empty() => Some(SassMap::new()), Value::ArgList(v) if v.is_empty() => Some(SassMap::new()), Value::Null => None, v => return Err((format!("$with: {} is not a map.", v.inspect(span)?), span).into()), }; let mut configuration = Configuration::empty(); if let Some(with) = with { visitor.emit_warning("`grass` does not currently support the $with parameter of load-css. This file will be imported the same way it would using `@import`.", args.span()); let mut values = BTreeMap::new(); for (key, value) in with { let name = Identifier::from(key.node.assert_string_with_name("with key", args.span())?.0); if values.contains_key(&name) { // todo: write test for this return Err(( format!("The variable {name} was configured twice.", name = name), key.span, ) .into()); } values.insert(name, ConfiguredValue::explicit(value, args.span())); } configuration = Configuration::explicit(values, args.span()); } let _configuration = Arc::new(RefCell::new(configuration)); let style_sheet = visitor.load_style_sheet(url.as_ref(), false, args.span())?; visitor.visit_stylesheet(style_sheet)?; // todo: support the $with argument to load-css // visitor.load_module( // url.as_ref(), // Some(Arc::clone(&configuration)), // true, // args.span(), // |visitor, module, stylesheet| { // // (*module).borrow() // Ok(()) // }, // )?; // Visitor::assert_configuration_is_empty(&configuration, true)?; Ok(()) } fn module_functions(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let module = Identifier::from( args.get_err(0, "module")? .assert_string_with_name("module", args.span())? .0, ); Ok(Value::Map( (*(*visitor.env.modules).borrow().get(module, args.span())?) .borrow() .functions(args.span()), )) } fn module_variables(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let module = Identifier::from( args.get_err(0, "module")? .assert_string_with_name("module", args.span())? .0, ); Ok(Value::Map( (*(*visitor.env.modules).borrow().get(module, args.span())?) .borrow() .variables(args.span()), )) } fn calc_args(mut args: ArgumentResult, visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let calc = match args.get_err(0, "calc")? { Value::Calculation(calc) => calc, v => { return Err(( format!("$calc: {} is not a calculation.", v.inspect(args.span())?), args.span(), ) .into()) } }; let args = calc .args .into_iter() .map(|arg| { Ok(match arg { CalculationArg::Number(num) => Value::Dimension(num), CalculationArg::Calculation(calc) => Value::Calculation(calc), CalculationArg::String(s) | CalculationArg::Interpolation(s) => { Value::String(s, QuoteKind::None) } CalculationArg::Operation { .. } => Value::String( serialize_calculation_arg(&arg, visitor.options, args.span())?, QuoteKind::None, ), }) }) .collect::>>()?; Ok(Value::List(args, ListSeparator::Comma, Brackets::None)) } fn calc_name(mut args: ArgumentResult, _visitor: &mut Visitor) -> SassResult { args.max_args(1)?; let calc = match args.get_err(0, "calc")? { Value::Calculation(calc) => calc, v => { return Err(( format!("$calc: {} is not a calculation.", v.inspect(args.span())?), args.span(), ) .into()) } }; Ok(Value::String(calc.name.to_string(), QuoteKind::Quoted)) } pub(crate) fn declare(f: &mut Module) { f.insert_builtin("feature-exists", feature_exists); f.insert_builtin("inspect", inspect); f.insert_builtin("type-of", type_of); f.insert_builtin("keywords", keywords); f.insert_builtin("global-variable-exists", global_variable_exists); f.insert_builtin("variable-exists", variable_exists); f.insert_builtin("function-exists", function_exists); f.insert_builtin("mixin-exists", mixin_exists); f.insert_builtin("content-exists", content_exists); f.insert_builtin("module-variables", module_variables); f.insert_builtin("module-functions", module_functions); f.insert_builtin("get-function", get_function); f.insert_builtin("call", call); f.insert_builtin("calc-args", calc_args); f.insert_builtin("calc-name", calc_name); f.insert_builtin_mixin("load-css", load_css); } grass_compiler-0.13.4/src/builtin/modules/mod.rs000064400000000000000000000361661046102023000177700ustar 00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, fmt, sync::Arc, }; use codemap::{Span, Spanned}; use crate::{ ast::{ArgumentResult, AstForwardRule, BuiltinMixin, Mixin}, builtin::Builtin, common::Identifier, error::SassResult, evaluate::{Environment, Visitor}, selector::ExtensionStore, utils::{ BaseMapView, LimitedMapView, MapView, MergedMapView, PrefixedMapView, PublicMemberMapView, }, value::{SassFunction, SassMap, Value}, }; use super::builtin_imports::QuoteKind; mod color; mod list; mod map; mod math; mod meta; mod selector; mod string; /// A [Module] that only exposes members that aren't shadowed by a given /// blocklist of member names. #[derive(Debug, Clone)] pub(crate) struct ShadowedModule { #[allow(dead_code)] inner: Arc>, scope: ModuleScope, } impl ShadowedModule { pub fn new( module: Arc>, variables: Option<&HashSet>, functions: Option<&HashSet>, mixins: Option<&HashSet>, ) -> Self { let module_scope = module.borrow().scope(); let variables = Self::shadowed_map(Arc::clone(&module_scope.variables), variables); let functions = Self::shadowed_map(Arc::clone(&module_scope.functions), functions); let mixins = Self::shadowed_map(Arc::clone(&module_scope.mixins), mixins); let new_scope = ModuleScope { variables, functions, mixins, }; Self { inner: module, scope: new_scope, } } fn needs_blocklist( map: Arc>, blocklist: Option<&HashSet>, ) -> bool { blocklist.is_some() && !map.is_empty() && blocklist.unwrap().iter().any(|key| map.contains_key(*key)) } fn shadowed_map( map: Arc>, blocklist: Option<&HashSet>, ) -> Arc> { match blocklist { Some(..) if !Self::needs_blocklist(Arc::clone(&map), blocklist) => map, Some(blocklist) => Arc::new(LimitedMapView::blocklist(map, blocklist)), None => map, } } pub fn if_necessary( module: Arc>, variables: Option<&HashSet>, functions: Option<&HashSet>, mixins: Option<&HashSet>, ) -> Option>> { let module_scope = module.borrow().scope(); let needs_blocklist = Self::needs_blocklist(Arc::clone(&module_scope.variables), variables) || Self::needs_blocklist(Arc::clone(&module_scope.functions), functions) || Self::needs_blocklist(Arc::clone(&module_scope.mixins), mixins); if needs_blocklist { Some(Arc::new(RefCell::new(Module::Shadowed(Self::new( module, variables, functions, mixins, ))))) } else { None } } } #[derive(Debug, Clone)] pub(crate) struct ForwardedModule { scope: ModuleScope, #[allow(dead_code)] inner: Arc>, #[allow(dead_code)] forward_rule: AstForwardRule, } impl ForwardedModule { pub fn new(module: Arc>, rule: AstForwardRule) -> Self { let scope = (*module).borrow().scope(); let variables = Self::forwarded_map( scope.variables, rule.prefix.as_deref(), rule.shown_variables.as_ref(), rule.hidden_variables.as_ref(), ); let functions = Self::forwarded_map( scope.functions, rule.prefix.as_deref(), rule.shown_mixins_and_functions.as_ref(), rule.hidden_mixins_and_functions.as_ref(), ); let mixins = Self::forwarded_map( scope.mixins, rule.prefix.as_deref(), rule.shown_mixins_and_functions.as_ref(), rule.hidden_mixins_and_functions.as_ref(), ); let scope = ModuleScope { variables, mixins, functions, }; ForwardedModule { inner: module, forward_rule: rule, scope, } } fn forwarded_map( mut map: Arc>, prefix: Option<&str>, safelist: Option<&HashSet>, blocklist: Option<&HashSet>, ) -> Arc> { debug_assert!(safelist.is_none() || blocklist.is_none()); if prefix.is_none() && safelist.is_none() && blocklist.is_none() { return map; } if let Some(prefix) = prefix { map = Arc::new(PrefixedMapView(map, prefix.to_owned())); } map } pub fn if_necessary( module: Arc>, rule: AstForwardRule, ) -> Arc> { if rule.prefix.is_none() && rule.shown_mixins_and_functions.is_none() && rule.shown_variables.is_none() && rule .hidden_mixins_and_functions .as_ref() .map_or(false, HashSet::is_empty) && rule .hidden_variables .as_ref() .map_or(false, HashSet::is_empty) { module } else { Arc::new(RefCell::new(Module::Forwarded(ForwardedModule::new( module, rule, )))) } } } #[derive(Debug, Clone)] pub(crate) struct ModuleScope { pub variables: Arc>, pub mixins: Arc>, pub functions: Arc>, } impl ModuleScope { pub fn new() -> Self { Self { variables: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), mixins: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), functions: Arc::new(BaseMapView(Arc::new(RefCell::new(BTreeMap::new())))), } } } #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum Module { Environment { scope: ModuleScope, #[allow(dead_code)] upstream: Vec, #[allow(dead_code)] extension_store: ExtensionStore, #[allow(dead_code)] env: Environment, }, Builtin { scope: ModuleScope, }, Forwarded(ForwardedModule), Shadowed(ShadowedModule), } #[derive(Debug, Clone)] pub(crate) struct Modules(pub BTreeMap>>); impl Modules { pub fn new() -> Self { Self(BTreeMap::new()) } pub fn insert( &mut self, name: Identifier, module: Arc>, span: Span, ) -> SassResult<()> { if self.0.contains_key(&name) { return Err(( format!("There's already a module with namespace \"{}\".", name), span, ) .into()); } self.0.insert(name, module); Ok(()) } pub fn get(&self, name: Identifier, span: Span) -> SassResult>> { match self.0.get(&name) { Some(v) => Ok(Arc::clone(v)), None => Err(( format!( "There is no module with the namespace \"{}\".", name.as_str() ), span, ) .into()), } } pub fn get_mut( &mut self, name: Identifier, span: Span, ) -> SassResult<&mut Arc>> { match self.0.get_mut(&name) { Some(v) => Ok(v), None => Err(( format!( "There is no module with the namespace \"{}\".", name.as_str() ), span, ) .into()), } } } fn member_map( local: Arc>, others: Vec>>, ) -> Arc> { let local_map = PublicMemberMapView(local); if others.is_empty() { return Arc::new(local_map); } let mut all_maps: Vec>> = others.into_iter().filter(|map| !map.is_empty()).collect(); all_maps.push(Arc::new(local_map)); // todo: potential optimization when all_maps.len() == 1 Arc::new(MergedMapView::new(all_maps)) } impl Module { pub fn new_env(env: Environment, extension_store: ExtensionStore) -> Self { let variables = { let variables = (*env.forwarded_modules).borrow(); let variables = variables .iter() .map(|module| Arc::clone(&(*module).borrow().scope().variables)); let this = Arc::new(BaseMapView(env.global_vars())); member_map(this, variables.collect()) }; let mixins = { let mixins = (*env.forwarded_modules).borrow(); let mixins = mixins .iter() .map(|module| Arc::clone(&(*module).borrow().scope().mixins)); let this = Arc::new(BaseMapView(env.global_mixins())); member_map(this, mixins.collect()) }; let functions = { let functions = (*env.forwarded_modules).borrow(); let functions = functions .iter() .map(|module| Arc::clone(&(*module).borrow().scope().functions)); let this = Arc::new(BaseMapView(env.global_functions())); member_map(this, functions.collect()) }; let scope = ModuleScope { variables, mixins, functions, }; Module::Environment { scope, upstream: Vec::new(), extension_store, env, } } pub fn new_builtin() -> Self { Module::Builtin { scope: ModuleScope::new(), } } pub(crate) fn scope(&self) -> ModuleScope { match self { Self::Builtin { scope } | Self::Environment { scope, .. } | Self::Forwarded(ForwardedModule { scope, .. }) | Self::Shadowed(ShadowedModule { scope, .. }) => scope.clone(), } } pub fn get_var(&self, name: Spanned) -> SassResult { let scope = self.scope(); match scope.variables.get(name.node) { Some(v) => Ok(v), None => Err(("Undefined variable.", name.span).into()), } } pub fn get_var_no_err(&self, name: Identifier) -> Option { let scope = self.scope(); scope.variables.get(name) } pub fn get_mixin_no_err(&self, name: Identifier) -> Option { let scope = self.scope(); scope.mixins.get(name) } pub fn update_var(&mut self, name: Spanned, value: Value) -> SassResult<()> { let scope = match self { Self::Builtin { .. } => { return Err(("Cannot modify built-in variable.", name.span).into()) } Self::Environment { scope, .. } | Self::Forwarded(ForwardedModule { scope, .. }) | Self::Shadowed(ShadowedModule { scope, .. }) => scope.clone(), }; if scope.variables.insert(name.node, value).is_none() { return Err(("Undefined variable.", name.span).into()); } Ok(()) } pub fn get_mixin(&self, name: Spanned) -> SassResult { let scope = self.scope(); match scope.mixins.get(name.node) { Some(v) => Ok(v), None => Err(("Undefined mixin.", name.span).into()), } } pub fn insert_builtin_mixin(&mut self, name: &'static str, mixin: BuiltinMixin) { let scope = self.scope(); scope.mixins.insert(name.into(), Mixin::Builtin(mixin)); } pub fn insert_builtin_var(&mut self, name: &'static str, value: Value) { let ident = name.into(); let scope = self.scope(); scope.variables.insert(ident, value); } pub fn get_fn(&self, name: Identifier) -> Option { let scope = self.scope(); scope.functions.get(name) } pub fn var_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.variables.get(name).is_some() } pub fn mixin_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.mixins.get(name).is_some() } pub fn fn_exists(&self, name: Identifier) -> bool { let scope = self.scope(); scope.functions.get(name).is_some() } pub fn insert_builtin( &mut self, name: &'static str, function: fn(ArgumentResult, &mut Visitor) -> SassResult, ) { let ident = name.into(); let scope = match self { Self::Builtin { scope } => scope, _ => unreachable!(), }; scope .functions .insert(ident, SassFunction::Builtin(Builtin::new(function), ident)); } pub fn functions(&self, span: Span) -> SassMap { SassMap::new_with( self.scope() .functions .iter() .into_iter() .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted).span(span), Value::FunctionRef(Box::new(value)), ) }) .collect::>(), ) } pub fn variables(&self, span: Span) -> SassMap { SassMap::new_with( self.scope() .variables .iter() .into_iter() .filter(|(key, _)| !key.as_str().starts_with('-')) .map(|(key, value)| { ( Value::String(key.to_string(), QuoteKind::Quoted).span(span), value, ) }) .collect::>(), ) } } pub(crate) fn declare_module_color() -> Module { let mut module = Module::new_builtin(); color::declare(&mut module); module } pub(crate) fn declare_module_list() -> Module { let mut module = Module::new_builtin(); list::declare(&mut module); module } pub(crate) fn declare_module_map() -> Module { let mut module = Module::new_builtin(); map::declare(&mut module); module } pub(crate) fn declare_module_math() -> Module { let mut module = Module::new_builtin(); math::declare(&mut module); module } pub(crate) fn declare_module_meta() -> Module { let mut module = Module::new_builtin(); meta::declare(&mut module); module } pub(crate) fn declare_module_selector() -> Module { let mut module = Module::new_builtin(); selector::declare(&mut module); module } pub(crate) fn declare_module_string() -> Module { let mut module = Module::new_builtin(); string::declare(&mut module); module } grass_compiler-0.13.4/src/builtin/modules/selector.rs000064400000000000000000000012501046102023000210130ustar 00000000000000use crate::{ builtin::modules::Module, builtin::selector::{ is_superselector, selector_append, selector_extend, selector_nest, selector_parse, selector_replace, selector_unify, simple_selectors, }, }; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("is-superselector", is_superselector); f.insert_builtin("append", selector_append); f.insert_builtin("extend", selector_extend); f.insert_builtin("nest", selector_nest); f.insert_builtin("parse", selector_parse); f.insert_builtin("replace", selector_replace); f.insert_builtin("unify", selector_unify); f.insert_builtin("simple-selectors", simple_selectors); } grass_compiler-0.13.4/src/builtin/modules/string.rs000064400000000000000000000014071046102023000205050ustar 00000000000000use crate::builtin::{ modules::Module, string::{ quote, str_index, str_insert, str_length, str_slice, str_split, to_lower_case, to_upper_case, unquote, }, }; #[cfg(feature = "random")] use crate::builtin::string::unique_id; pub(crate) fn declare(f: &mut Module) { f.insert_builtin("quote", quote); f.insert_builtin("index", str_index); f.insert_builtin("insert", str_insert); f.insert_builtin("length", str_length); f.insert_builtin("slice", str_slice); f.insert_builtin("split", str_split); f.insert_builtin("to-lower-case", to_lower_case); f.insert_builtin("to-upper-case", to_upper_case); #[cfg(feature = "random")] f.insert_builtin("unique-id", unique_id); f.insert_builtin("unquote", unquote); } grass_compiler-0.13.4/src/color/mod.rs000064400000000000000000000356051046102023000157650ustar 00000000000000//! A color is internally represented as either RGBA or HSLA. //! //! Colors can be constructed in Sass through names (e.g. red, blue, aqua) //! or the builtin functions `rgb()`, `rgba()`, `hsl()`, and `hsla()`, //! all of which can accept 1-4 arguments. //! //! It is necessary to retain the original values with which the //! color was constructed. //! E.g. `hsla(.999999999999, 100, 100, 1)` should retain its full HSLA //! values to an arbitrary precision. //! //! Color values matching named colors are implicitly converted to named colors //! E.g. `rgba(255, 0, 0, 1)` => `red` //! //! Named colors retain their original casing, //! so `rEd` should be emitted as `rEd`. use crate::value::{fuzzy_round, Number}; pub(crate) use name::NAMED_COLORS; mod name; // todo: only store alpha once on color #[derive(Debug, Clone)] pub struct Color { rgba: Rgb, hsla: Option, alpha: Number, pub(crate) format: ColorFormat, } #[derive(Debug, Clone, Eq, PartialEq)] pub(crate) enum ColorFormat { Rgb, Hsl, /// Literal string from source text. Either a named color like `red` or a hex color // todo: make this is a span and lookup text from codemap Literal(String), /// Use the most appropriate format Infer, } impl PartialEq for Color { fn eq(&self, other: &Self) -> bool { if self.alpha != other.alpha && !(self.alpha >= Number::one() && other.alpha >= Number::one()) { return false; } self.rgba == other.rgba } } impl Eq for Color {} impl Color { pub(crate) const fn new_rgba( red: Number, green: Number, blue: Number, alpha: Number, format: ColorFormat, ) -> Color { Color { rgba: Rgb::new(red, green, blue), alpha, hsla: None, format, } } const fn new_hsla(red: Number, green: Number, blue: Number, alpha: Number, hsla: Hsl) -> Color { Color { rgba: Rgb::new(red, green, blue), alpha, hsla: Some(hsla), format: ColorFormat::Infer, } } } #[derive(Debug, Clone)] struct Rgb { red: Number, green: Number, blue: Number, } impl PartialEq for Rgb { fn eq(&self, other: &Self) -> bool { if self.red != other.red && !(self.red >= Number(255.0) && other.red >= Number(255.0)) { return false; } if self.green != other.green && !(self.green >= Number(255.0) && other.green >= Number(255.0)) { return false; } if self.blue != other.blue && !(self.blue >= Number(255.0) && other.blue >= Number(255.0)) { return false; } true } } impl Eq for Rgb {} impl Rgb { pub const fn new(red: Number, green: Number, blue: Number) -> Self { Rgb { red, green, blue } } } #[derive(Debug, Clone)] struct Hsl { hue: Number, saturation: Number, luminance: Number, } impl Hsl { pub const fn new(hue: Number, saturation: Number, luminance: Number) -> Self { Hsl { hue, saturation, luminance, } } pub fn hue(&self) -> Number { self.hue } pub fn saturation(&self) -> Number { self.saturation } pub fn luminance(&self) -> Number { self.luminance } } // RGBA color functions impl Color { pub fn new(red: u8, green: u8, blue: u8, alpha: u8, format: String) -> Self { Color { rgba: Rgb::new(red.into(), green.into(), blue.into()), hsla: None, alpha: alpha.into(), format: ColorFormat::Literal(format), } } /// Create a new `Color` with just RGBA values. /// Color representation is created automatically. pub fn from_rgba( mut red: Number, mut green: Number, mut blue: Number, mut alpha: Number, ) -> Self { red = red.clamp(0.0, 255.0); green = green.clamp(0.0, 255.0); blue = blue.clamp(0.0, 255.0); alpha = alpha.clamp(0.0, 1.0); Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer) } pub fn from_rgba_fn( mut red: Number, mut green: Number, mut blue: Number, mut alpha: Number, ) -> Self { red = red.clamp(0.0, 255.0); green = green.clamp(0.0, 255.0); blue = blue.clamp(0.0, 255.0); alpha = alpha.clamp(0.0, 1.0); Color::new_rgba(red, green, blue, alpha, ColorFormat::Rgb) } pub fn red(&self) -> Number { self.rgba.red.round() } pub fn blue(&self) -> Number { self.rgba.blue.round() } pub fn green(&self) -> Number { self.rgba.green.round() } /// Mix two colors together with weight /// Algorithm adapted from /// pub fn mix(&self, other: &Color, weight: Number) -> Self { let weight = weight.clamp(0.0, 100.0); let normalized_weight = weight * Number(2.0) - Number::one(); let alpha_distance = self.alpha() - other.alpha(); let combined_weight1 = if normalized_weight * alpha_distance == Number(-1.0) { normalized_weight } else { (normalized_weight + alpha_distance) / (Number::one() + normalized_weight * alpha_distance) }; let weight1 = (combined_weight1 + Number::one()) / Number(2.0); let weight2 = Number::one() - weight1; Color::from_rgba( self.red() * weight1 + other.red() * weight2, self.green() * weight1 + other.green() * weight2, self.blue() * weight1 + other.blue() * weight2, self.alpha() * weight + other.alpha() * (Number::one() - weight), ) } } /// HSLA color functions /// Algorithms adapted from impl Color { /// Calculate hue from RGBA values pub fn hue(&self) -> Number { if let Some(h) = &self.hsla { return h.hue(); } let red = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); let delta = max - min; let hue = if min == max { Number::zero() } else if max == red { Number(60.0) * (green - blue) / delta } else if max == green { Number(120.0) + Number(60.0) * (blue - red) / delta } else { Number(240.0) + Number(60.0) * (red - green) / delta }; hue % Number(360.0) } /// Calculate saturation from RGBA values pub fn saturation(&self) -> Number { if let Some(h) = &self.hsla { return h.saturation() * Number(100.0); } let red: Number = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); if min == max { return Number::zero(); } let delta = max - min; let sum = max + min; let s = delta / if sum > Number::one() { Number(2.0) - sum } else { sum }; s * Number(100.0) } /// Calculate luminance from RGBA values pub fn lightness(&self) -> Number { if let Some(h) = &self.hsla { return h.luminance() * Number(100.0); } let red: Number = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); (((min + max) / Number(2.0)) * Number(100.0)).round() } pub fn as_hsla(&self) -> (Number, Number, Number, Number) { if let Some(h) = &self.hsla { return (h.hue(), h.saturation(), h.luminance(), self.alpha()); } let red = self.red() / Number(255.0); let green = self.green() / Number(255.0); let blue = self.blue() / Number(255.0); let min = red.min(green.min(blue)); let max = red.max(green.max(blue)); let lightness = (min + max) / Number(2.0); let saturation = if min == max { Number::zero() } else { let d = max - min; let mm = max + min; d / if mm > Number::one() { Number(2.0) - mm } else { mm } }; let mut hue = if min == max { Number::zero() } else if blue == max { Number(4.0) + (red - green) / (max - min) } else if green == max { Number(2.0) + (blue - red) / (max - min) } else { (green - blue) / (max - min) }; if hue.is_negative() { hue += Number(360.0); } hue *= Number(60.0); (hue % Number(360.0), saturation, lightness, self.alpha()) } pub fn adjust_hue(&self, degrees: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue + degrees, saturation, luminance, alpha) } pub fn lighten(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, saturation, luminance + amount, alpha) } pub fn darken(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, saturation, luminance - amount, alpha) } pub fn saturate(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, (saturation + amount).clamp(0.0, 1.0), luminance, alpha) } pub fn desaturate(&self, amount: Number) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue, (saturation - amount).clamp(0.0, 1.0), luminance, alpha) } pub fn from_hsla_fn(hue: Number, saturation: Number, luminance: Number, alpha: Number) -> Self { let mut color = Self::from_hsla(hue, saturation, luminance, alpha); color.format = ColorFormat::Hsl; color } /// Create RGBA representation from HSLA values pub fn from_hsla(hue: Number, saturation: Number, lightness: Number, alpha: Number) -> Self { let hue = hue % Number(360.0); let hsla = Hsl::new(hue, saturation.clamp(0.0, 1.0), lightness.clamp(0.0, 1.0)); let scaled_hue = hue.0 / 360.0; let scaled_saturation = saturation.0.clamp(0.0, 1.0); let scaled_lightness = lightness.0.clamp(0.0, 1.0); let m2 = if scaled_lightness <= 0.5 { scaled_lightness * (scaled_saturation + 1.0) } else { scaled_lightness.mul_add(-scaled_saturation, scaled_lightness + scaled_saturation) }; let m1 = scaled_lightness.mul_add(2.0, -m2); let red = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue + 1.0 / 3.0) * 255.0); let green = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue) * 255.0); let blue = fuzzy_round(Self::hue_to_rgb(m1, m2, scaled_hue - 1.0 / 3.0) * 255.0); Color::new_hsla(Number(red), Number(green), Number(blue), alpha, hsla) } fn hue_to_rgb(m1: f64, m2: f64, mut hue: f64) -> f64 { if hue < 0.0 { hue += 1.0; } if hue > 1.0 { hue -= 1.0; } if hue < 1.0 / 6.0 { ((m2 - m1) * hue).mul_add(6.0, m1) } else if hue < 1.0 / 2.0 { m2 } else if hue < 2.0 / 3.0 { ((m2 - m1) * (2.0 / 3.0 - hue)).mul_add(6.0, m1) } else { m1 } } pub fn invert(&self, weight: Number) -> Self { if weight.is_zero() { return self.clone(); } let red = Number(255.0) - self.red(); let green = Number(255.0) - self.green(); let blue = Number(255.0) - self.blue(); let inverse = Color::new_rgba(red, green, blue, self.alpha(), ColorFormat::Infer); inverse.mix(self, weight) } pub fn complement(&self) -> Self { let (hue, saturation, luminance, alpha) = self.as_hsla(); Color::from_hsla(hue + Number(180.0), saturation, luminance, alpha) } } /// Opacity color functions impl Color { pub fn alpha(&self) -> Number { if self.alpha > Number::one() { self.alpha / Number(255.0) } else { self.alpha } } /// Change `alpha` to value given pub fn with_alpha(&self, alpha: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), alpha) } /// Makes a color more opaque. /// Takes a color and a number between 0 and 1, /// and returns a color with the opacity increased by that amount. pub fn fade_in(&self, amount: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), self.alpha() + amount) } /// Makes a color more transparent. /// Takes a color and a number between 0 and 1, /// and returns a color with the opacity decreased by that amount. pub fn fade_out(&self, amount: Number) -> Self { Color::from_rgba(self.red(), self.green(), self.blue(), self.alpha() - amount) } } /// Other color functions impl Color { pub fn to_ie_hex_str(&self) -> String { format!( "#{:02X}{:02X}{:02X}{:02X}", fuzzy_round(self.alpha().0 * 255.0) as u8, self.red().0 as u8, self.green().0 as u8, self.blue().0 as u8 ) } } /// HWB color functions impl Color { pub fn from_hwb(hue: Number, white: Number, black: Number, mut alpha: Number) -> Color { let hue = Number(hue.rem_euclid(360.0) / 360.0); let mut scaled_white = white.0 / 100.0; let mut scaled_black = black.0 / 100.0; alpha = alpha.clamp(0.0, 1.0); let white_black_sum = scaled_white + scaled_black; if white_black_sum > 1.0 { scaled_white /= white_black_sum; scaled_black /= white_black_sum; } let factor = 1.0 - scaled_white - scaled_black; let to_rgb = |hue: f64| -> Number { let channel = Self::hue_to_rgb(0.0, 1.0, hue).mul_add(factor, scaled_white); Number(fuzzy_round(channel * 255.0)) }; let red = to_rgb(hue.0 + 1.0 / 3.0); let green = to_rgb(hue.0); let blue = to_rgb(hue.0 - 1.0 / 3.0); Color::new_rgba(red, green, blue, alpha, ColorFormat::Infer) } pub fn whiteness(&self) -> Number { self.red().min(self.green()).min(self.blue()) / Number(255.0) } pub fn blackness(&self) -> Number { Number(1.0) - (self.red().max(self.green()).max(self.blue()) / Number(255.0)) } } grass_compiler-0.13.4/src/color/name.rs000064400000000000000000000330521046102023000161200ustar 00000000000000//! A big dictionary of named colors and their corresponding RGBA values pub(crate) struct NamedColorMap { name_to_rgba: phf::Map<&'static str, [u8; 4]>, rgba_to_name: phf::Map<[u8; 3], &'static str>, } impl NamedColorMap { pub fn get_by_name(&self, name: &str) -> Option<&[u8; 4]> { self.name_to_rgba.get(name) } pub fn get_by_rgba(&self, rgba: [u8; 3]) -> Option<&&str> { self.rgba_to_name.get(&rgba) } } pub(crate) static NAMED_COLORS: NamedColorMap = NamedColorMap { name_to_rgba: phf::phf_map! { "aliceblue" => [0xF0, 0xF8, 0xFF, 0xFF], "antiquewhite" => [0xFA, 0xEB, 0xD7, 0xFF], "aqua" => [0x00, 0xFF, 0xFF, 0xFF], "cyan" => [0x00, 0xFF, 0xFF, 0xFF], "aquamarine" => [0x7F, 0xFF, 0xD4, 0xFF], "azure" => [0xF0, 0xFF, 0xFF, 0xFF], "beige" => [0xF5, 0xF5, 0xDC, 0xFF], "bisque" => [0xFF, 0xE4, 0xC4, 0xFF], "black" => [0x00, 0x00, 0x00, 0xFF], "blanchedalmond" => [0xFF, 0xEB, 0xCD, 0xFF], "blue" => [0x00, 0x00, 0xFF, 0xFF], "blueviolet" => [0x8A, 0x2B, 0xE2, 0xFF], "brown" => [0xA5, 0x2A, 0x2A, 0xFF], "burlywood" => [0xDE, 0xB8, 0x87, 0xFF], "cadetblue" => [0x5F, 0x9E, 0xA0, 0xFF], "chartreuse" => [0x7F, 0xFF, 0x00, 0xFF], "chocolate" => [0xD2, 0x69, 0x1E, 0xFF], "coral" => [0xFF, 0x7F, 0x50, 0xFF], "cornflowerblue" => [0x64, 0x95, 0xED, 0xFF], "cornsilk" => [0xFF, 0xF8, 0xDC, 0xFF], "crimson" => [0xDC, 0x14, 0x3C, 0xFF], "darkblue" => [0x00, 0x00, 0x8B, 0xFF], "darkcyan" => [0x00, 0x8B, 0x8B, 0xFF], "darkgoldenrod" => [0xB8, 0x86, 0x0B, 0xFF], "darkgray" => [0xA9, 0xA9, 0xA9, 0xFF], "darkgrey" => [0xA9, 0xA9, 0xA9, 0xFF], "darkgreen" => [0x00, 0x64, 0x00, 0xFF], "darkkhaki" => [0xBD, 0xB7, 0x6B, 0xFF], "darkmagenta" => [0x8B, 0x00, 0x8B, 0xFF], "darkolivegreen" => [0x55, 0x6B, 0x2F, 0xFF], "darkorange" => [0xFF, 0x8C, 0x00, 0xFF], "darkorchid" => [0x99, 0x32, 0xCC, 0xFF], "darkred" => [0x8B, 0x00, 0x00, 0xFF], "darksalmon" => [0xE9, 0x96, 0x7A, 0xFF], "darkseagreen" => [0x8F, 0xBC, 0x8F, 0xFF], "darkslateblue" => [0x48, 0x3D, 0x8B, 0xFF], "darkslategray" => [0x2F, 0x4F, 0x4F, 0xFF], "darkslategrey" => [0x2F, 0x4F, 0x4F, 0xFF], "darkturquoise" => [0x00, 0xCE, 0xD1, 0xFF], "darkviolet" => [0x94, 0x00, 0xD3, 0xFF], "deeppink" => [0xFF, 0x14, 0x93, 0xFF], "deepskyblue" => [0x00, 0xBF, 0xFF, 0xFF], "dimgray" => [0x69, 0x69, 0x69, 0xFF], "dimgrey" => [0x69, 0x69, 0x69, 0xFF], "dodgerblue" => [0x1E, 0x90, 0xFF, 0xFF], "firebrick" => [0xB2, 0x22, 0x22, 0xFF], "floralwhite" => [0xFF, 0xFA, 0xF0, 0xFF], "forestgreen" => [0x22, 0x8B, 0x22, 0xFF], "fuchsia" => [0xFF, 0x00, 0xFF, 0xFF], "magenta" => [0xFF, 0x00, 0xFF, 0xFF], "gainsboro" => [0xDC, 0xDC, 0xDC, 0xFF], "ghostwhite" => [0xF8, 0xF8, 0xFF, 0xFF], "gold" => [0xFF, 0xD7, 0x00, 0xFF], "goldenrod" => [0xDA, 0xA5, 0x20, 0xFF], "gray" => [0x80, 0x80, 0x80, 0xFF], "grey" => [0x80, 0x80, 0x80, 0xFF], "green" => [0x00, 0x80, 0x00, 0xFF], "greenyellow" => [0xAD, 0xFF, 0x2F, 0xFF], "honeydew" => [0xF0, 0xFF, 0xF0, 0xFF], "hotpink" => [0xFF, 0x69, 0xB4, 0xFF], "indianred" => [0xCD, 0x5C, 0x5C, 0xFF], "indigo" => [0x4B, 0x00, 0x82, 0xFF], "ivory" => [0xFF, 0xFF, 0xF0, 0xFF], "khaki" => [0xF0, 0xE6, 0x8C, 0xFF], "lavender" => [0xE6, 0xE6, 0xFA, 0xFF], "lavenderblush" => [0xFF, 0xF0, 0xF5, 0xFF], "lawngreen" => [0x7C, 0xFC, 0x00, 0xFF], "lemonchiffon" => [0xFF, 0xFA, 0xCD, 0xFF], "lightblue" => [0xAD, 0xD8, 0xE6, 0xFF], "lightcoral" => [0xF0, 0x80, 0x80, 0xFF], "lightcyan" => [0xE0, 0xFF, 0xFF, 0xFF], "lightgoldenrodyellow" => [0xFA, 0xFA, 0xD2, 0xFF], "lightgray" => [0xD3, 0xD3, 0xD3, 0xFF], "lightgrey" => [0xD3, 0xD3, 0xD3, 0xFF], "lightgreen" => [0x90, 0xEE, 0x90, 0xFF], "lightpink" => [0xFF, 0xB6, 0xC1, 0xFF], "lightsalmon" => [0xFF, 0xA0, 0x7A, 0xFF], "lightseagreen" => [0x20, 0xB2, 0xAA, 0xFF], "lightskyblue" => [0x87, 0xCE, 0xFA, 0xFF], "lightslategray" => [0x77, 0x88, 0x99, 0xFF], "lightslategrey" => [0x77, 0x88, 0x99, 0xFF], "lightsteelblue" => [0xB0, 0xC4, 0xDE, 0xFF], "lightyellow" => [0xFF, 0xFF, 0xE0, 0xFF], "lime" => [0x00, 0xFF, 0x00, 0xFF], "limegreen" => [0x32, 0xCD, 0x32, 0xFF], "linen" => [0xFA, 0xF0, 0xE6, 0xFF], "maroon" => [0x80, 0x00, 0x00, 0xFF], "mediumaquamarine" => [0x66, 0xCD, 0xAA, 0xFF], "mediumblue" => [0x00, 0x00, 0xCD, 0xFF], "mediumorchid" => [0xBA, 0x55, 0xD3, 0xFF], "mediumpurple" => [0x93, 0x70, 0xDB, 0xFF], "mediumseagreen" => [0x3C, 0xB3, 0x71, 0xFF], "mediumslateblue" => [0x7B, 0x68, 0xEE, 0xFF], "mediumspringgreen" => [0x00, 0xFA, 0x9A, 0xFF], "mediumturquoise" => [0x48, 0xD1, 0xCC, 0xFF], "mediumvioletred" => [0xC7, 0x15, 0x85, 0xFF], "midnightblue" => [0x19, 0x19, 0x70, 0xFF], "mintcream" => [0xF5, 0xFF, 0xFA, 0xFF], "mistyrose" => [0xFF, 0xE4, 0xE1, 0xFF], "moccasin" => [0xFF, 0xE4, 0xB5, 0xFF], "navajowhite" => [0xFF, 0xDE, 0xAD, 0xFF], "navy" => [0x00, 0x00, 0x80, 0xFF], "oldlace" => [0xFD, 0xF5, 0xE6, 0xFF], "olive" => [0x80, 0x80, 0x00, 0xFF], "olivedrab" => [0x6B, 0x8E, 0x23, 0xFF], "orange" => [0xFF, 0xA5, 0x00, 0xFF], "orangered" => [0xFF, 0x45, 0x00, 0xFF], "orchid" => [0xDA, 0x70, 0xD6, 0xFF], "palegoldenrod" => [0xEE, 0xE8, 0xAA, 0xFF], "palegreen" => [0x98, 0xFB, 0x98, 0xFF], "paleturquoise" => [0xAF, 0xEE, 0xEE, 0xFF], "palevioletred" => [0xDB, 0x70, 0x93, 0xFF], "papayawhip" => [0xFF, 0xEF, 0xD5, 0xFF], "peachpuff" => [0xFF, 0xDA, 0xB9, 0xFF], "peru" => [0xCD, 0x85, 0x3F, 0xFF], "pink" => [0xFF, 0xC0, 0xCB, 0xFF], "plum" => [0xDD, 0xA0, 0xDD, 0xFF], "powderblue" => [0xB0, 0xE0, 0xE6, 0xFF], "purple" => [0x80, 0x00, 0x80, 0xFF], "rebeccapurple" => [0x66, 0x33, 0x99, 0xFF], "red" => [0xFF, 0x00, 0x00, 0xFF], "rosybrown" => [0xBC, 0x8F, 0x8F, 0xFF], "royalblue" => [0x41, 0x69, 0xE1, 0xFF], "saddlebrown" => [0x8B, 0x45, 0x13, 0xFF], "salmon" => [0xFA, 0x80, 0x72, 0xFF], "sandybrown" => [0xF4, 0xA4, 0x60, 0xFF], "seagreen" => [0x2E, 0x8B, 0x57, 0xFF], "seashell" => [0xFF, 0xF5, 0xEE, 0xFF], "sienna" => [0xA0, 0x52, 0x2D, 0xFF], "silver" => [0xC0, 0xC0, 0xC0, 0xFF], "skyblue" => [0x87, 0xCE, 0xEB, 0xFF], "slateblue" => [0x6A, 0x5A, 0xCD, 0xFF], "slategray" => [0x70, 0x80, 0x90, 0xFF], "slategrey" => [0x70, 0x80, 0x90, 0xFF], "snow" => [0xFF, 0xFA, 0xFA, 0xFF], "springgreen" => [0x00, 0xFF, 0x7F, 0xFF], "steelblue" => [0x46, 0x82, 0xB4, 0xFF], "tan" => [0xD2, 0xB4, 0x8C, 0xFF], "teal" => [0x00, 0x80, 0x80, 0xFF], "thistle" => [0xD8, 0xBF, 0xD8, 0xFF], "tomato" => [0xFF, 0x63, 0x47, 0xFF], "turquoise" => [0x40, 0xE0, 0xD0, 0xFF], "violet" => [0xEE, 0x82, 0xEE, 0xFF], "wheat" => [0xF5, 0xDE, 0xB3, 0xFF], "white" => [0xFF, 0xFF, 0xFF, 0xFF], "whitesmoke" => [0xF5, 0xF5, 0xF5, 0xFF], "yellow" => [0xFF, 0xFF, 0x00, 0xFF], "yellowgreen" => [0x9A, 0xCD, 0x32, 0xFF], "transparent" => [0x00, 0x00, 0x00, 0x00], }, rgba_to_name: phf::phf_map! { [0xF0, 0xF8, 0xFF] => "aliceblue", [0xFA, 0xEB, 0xD7] => "antiquewhite", [0x00, 0xFF, 0xFF] => "aqua", [0x7F, 0xFF, 0xD4] => "aquamarine", [0xF0, 0xFF, 0xFF] => "azure", [0xF5, 0xF5, 0xDC] => "beige", [0xFF, 0xE4, 0xC4] => "bisque", [0x00, 0x00, 0x00] => "black", [0xFF, 0xEB, 0xCD] => "blanchedalmond", [0x00, 0x00, 0xFF] => "blue", [0x8A, 0x2B, 0xE2] => "blueviolet", [0xA5, 0x2A, 0x2A] => "brown", [0xDE, 0xB8, 0x87] => "burlywood", [0x5F, 0x9E, 0xA0] => "cadetblue", [0x7F, 0xFF, 0x00] => "chartreuse", [0xD2, 0x69, 0x1E] => "chocolate", [0xFF, 0x7F, 0x50] => "coral", [0x64, 0x95, 0xED] => "cornflowerblue", [0xFF, 0xF8, 0xDC] => "cornsilk", [0xDC, 0x14, 0x3C] => "crimson", [0x00, 0x00, 0x8B] => "darkblue", [0x00, 0x8B, 0x8B] => "darkcyan", [0xB8, 0x86, 0x0B] => "darkgoldenrod", [0xA9, 0xA9, 0xA9] => "darkgray", [0x00, 0x64, 0x00] => "darkgreen", [0xBD, 0xB7, 0x6B] => "darkkhaki", [0x8B, 0x00, 0x8B] => "darkmagenta", [0x55, 0x6B, 0x2F] => "darkolivegreen", [0xFF, 0x8C, 0x00] => "darkorange", [0x99, 0x32, 0xCC] => "darkorchid", [0x8B, 0x00, 0x00] => "darkred", [0xE9, 0x96, 0x7A] => "darksalmon", [0x8F, 0xBC, 0x8F] => "darkseagreen", [0x48, 0x3D, 0x8B] => "darkslateblue", [0x2F, 0x4F, 0x4F] => "darkslategray", [0x00, 0xCE, 0xD1] => "darkturquoise", [0x94, 0x00, 0xD3] => "darkviolet", [0xFF, 0x14, 0x93] => "deeppink", [0x00, 0xBF, 0xFF] => "deepskyblue", [0x69, 0x69, 0x69] => "dimgray", [0x1E, 0x90, 0xFF] => "dodgerblue", [0xB2, 0x22, 0x22] => "firebrick", [0xFF, 0xFA, 0xF0] => "floralwhite", [0x22, 0x8B, 0x22] => "forestgreen", [0xFF, 0x00, 0xFF] => "fuchsia", [0xDC, 0xDC, 0xDC] => "gainsboro", [0xF8, 0xF8, 0xFF] => "ghostwhite", [0xFF, 0xD7, 0x00] => "gold", [0xDA, 0xA5, 0x20] => "goldenrod", [0x80, 0x80, 0x80] => "gray", [0x00, 0x80, 0x00] => "green", [0xAD, 0xFF, 0x2F] => "greenyellow", [0xF0, 0xFF, 0xF0] => "honeydew", [0xFF, 0x69, 0xB4] => "hotpink", [0xCD, 0x5C, 0x5C] => "indianred", [0x4B, 0x00, 0x82] => "indigo", [0xFF, 0xFF, 0xF0] => "ivory", [0xF0, 0xE6, 0x8C] => "khaki", [0xE6, 0xE6, 0xFA] => "lavender", [0xFF, 0xF0, 0xF5] => "lavenderblush", [0x7C, 0xFC, 0x00] => "lawngreen", [0xFF, 0xFA, 0xCD] => "lemonchiffon", [0xAD, 0xD8, 0xE6] => "lightblue", [0xF0, 0x80, 0x80] => "lightcoral", [0xE0, 0xFF, 0xFF] => "lightcyan", [0xFA, 0xFA, 0xD2] => "lightgoldenrodyellow", [0xD3, 0xD3, 0xD3] => "lightgray", [0x90, 0xEE, 0x90] => "lightgreen", [0xFF, 0xB6, 0xC1] => "lightpink", [0xFF, 0xA0, 0x7A] => "lightsalmon", [0x20, 0xB2, 0xAA] => "lightseagreen", [0x87, 0xCE, 0xFA] => "lightskyblue", [0x77, 0x88, 0x99] => "lightslategray", [0xB0, 0xC4, 0xDE] => "lightsteelblue", [0xFF, 0xFF, 0xE0] => "lightyellow", [0x00, 0xFF, 0x00] => "lime", [0x32, 0xCD, 0x32] => "limegreen", [0xFA, 0xF0, 0xE6] => "linen", [0x80, 0x00, 0x00] => "maroon", [0x66, 0xCD, 0xAA] => "mediumaquamarine", [0x00, 0x00, 0xCD] => "mediumblue", [0xBA, 0x55, 0xD3] => "mediumorchid", [0x93, 0x70, 0xDB] => "mediumpurple", [0x3C, 0xB3, 0x71] => "mediumseagreen", [0x7B, 0x68, 0xEE] => "mediumslateblue", [0x00, 0xFA, 0x9A] => "mediumspringgreen", [0x48, 0xD1, 0xCC] => "mediumturquoise", [0xC7, 0x15, 0x85] => "mediumvioletred", [0x19, 0x19, 0x70] => "midnightblue", [0xF5, 0xFF, 0xFA] => "mintcream", [0xFF, 0xE4, 0xE1] => "mistyrose", [0xFF, 0xE4, 0xB5] => "moccasin", [0xFF, 0xDE, 0xAD] => "navajowhite", [0x00, 0x00, 0x80] => "navy", [0xFD, 0xF5, 0xE6] => "oldlace", [0x80, 0x80, 0x00] => "olive", [0x6B, 0x8E, 0x23] => "olivedrab", [0xFF, 0xA5, 0x00] => "orange", [0xFF, 0x45, 0x00] => "orangered", [0xDA, 0x70, 0xD6] => "orchid", [0xEE, 0xE8, 0xAA] => "palegoldenrod", [0x98, 0xFB, 0x98] => "palegreen", [0xAF, 0xEE, 0xEE] => "paleturquoise", [0xDB, 0x70, 0x93] => "palevioletred", [0xFF, 0xEF, 0xD5] => "papayawhip", [0xFF, 0xDA, 0xB9] => "peachpuff", [0xCD, 0x85, 0x3F] => "peru", [0xFF, 0xC0, 0xCB] => "pink", [0xDD, 0xA0, 0xDD] => "plum", [0xB0, 0xE0, 0xE6] => "powderblue", [0x80, 0x00, 0x80] => "purple", [0x66, 0x33, 0x99] => "rebeccapurple", [0xFF, 0x00, 0x00] => "red", [0xBC, 0x8F, 0x8F] => "rosybrown", [0x41, 0x69, 0xE1] => "royalblue", [0x8B, 0x45, 0x13] => "saddlebrown", [0xFA, 0x80, 0x72] => "salmon", [0xF4, 0xA4, 0x60] => "sandybrown", [0x2E, 0x8B, 0x57] => "seagreen", [0xFF, 0xF5, 0xEE] => "seashell", [0xA0, 0x52, 0x2D] => "sienna", [0xC0, 0xC0, 0xC0] => "silver", [0x87, 0xCE, 0xEB] => "skyblue", [0x6A, 0x5A, 0xCD] => "slateblue", [0x70, 0x80, 0x90] => "slategray", [0xFF, 0xFA, 0xFA] => "snow", [0x00, 0xFF, 0x7F] => "springgreen", [0x46, 0x82, 0xB4] => "steelblue", [0xD2, 0xB4, 0x8C] => "tan", [0x00, 0x80, 0x80] => "teal", [0xD8, 0xBF, 0xD8] => "thistle", [0xFF, 0x63, 0x47] => "tomato", [0x40, 0xE0, 0xD0] => "turquoise", [0xEE, 0x82, 0xEE] => "violet", [0xF5, 0xDE, 0xB3] => "wheat", [0xFF, 0xFF, 0xFF] => "white", [0xF5, 0xF5, 0xF5] => "whitesmoke", [0xFF, 0xFF, 0x00] => "yellow", [0x9A, 0xCD, 0x32] => "yellowgreen", }, }; grass_compiler-0.13.4/src/common.rs000064400000000000000000000107101046102023000153460ustar 00000000000000use std::fmt::{self, Display, Write}; use crate::interner::InternedString; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum UnaryOp { Plus, Neg, Div, Not, } #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub enum BinaryOp { SingleEq, Equal, NotEqual, GreaterThan, GreaterThanEqual, LessThan, LessThanEqual, Plus, Minus, Mul, Div, Rem, And, Or, } impl BinaryOp { pub fn precedence(self) -> u8 { match self { Self::SingleEq => 0, Self::Or => 1, Self::And => 2, Self::Equal | Self::NotEqual => 3, Self::GreaterThan | Self::GreaterThanEqual | Self::LessThan | Self::LessThanEqual => 4, Self::Plus | Self::Minus => 5, Self::Mul | Self::Div | Self::Rem => 6, } } } impl Display for BinaryOp { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { BinaryOp::SingleEq => write!(f, "="), BinaryOp::Equal => write!(f, "=="), BinaryOp::NotEqual => write!(f, "!="), BinaryOp::GreaterThanEqual => write!(f, ">="), BinaryOp::LessThanEqual => write!(f, "<="), BinaryOp::GreaterThan => write!(f, ">"), BinaryOp::LessThan => write!(f, "<"), BinaryOp::Plus => write!(f, "+"), BinaryOp::Minus => write!(f, "-"), BinaryOp::Mul => write!(f, "*"), BinaryOp::Div => write!(f, "/"), BinaryOp::Rem => write!(f, "%"), BinaryOp::And => write!(f, "and"), BinaryOp::Or => write!(f, "or"), } } } /// Strings can either have quotes or not #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum QuoteKind { Quoted, None, } impl Display for QuoteKind { #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Quoted => f.write_char('"'), Self::None => Ok(()), } } } /// Lists can either be bracketed or not #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum Brackets { None, Bracketed, } #[derive(Debug, Clone, Copy, Eq, PartialEq)] pub enum ListSeparator { Space, Comma, Slash, Undecided, } impl ListSeparator { pub fn as_str(self) -> &'static str { match self { Self::Space | Self::Undecided => " ", Self::Comma => ", ", Self::Slash => " / ", } } pub fn name(self) -> &'static str { match self { Self::Space | Self::Undecided => "space", Self::Comma => "comma", Self::Slash => "slash", } } } /// In Sass, underscores and hyphens are considered equal when inside identifiers. /// /// This struct protects that invariant by normalizing all underscores into hyphens. #[derive(Clone, Eq, PartialEq, Hash, PartialOrd, Ord, Copy)] pub struct Identifier(InternedString); impl fmt::Debug for Identifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_tuple("Identifier") .field(&self.0.to_string()) .finish() } } impl Identifier { fn from_str(s: &str) -> Self { if s.contains('_') { Identifier(InternedString::get_or_intern(s.replace('_', "-"))) } else { Identifier(InternedString::get_or_intern(s)) } } pub fn is_public(&self) -> bool { !self.as_str().starts_with('-') } } impl From for Identifier { fn from(s: String) -> Identifier { Self::from_str(&s) } } impl From<&String> for Identifier { fn from(s: &String) -> Identifier { Self::from_str(s) } } impl From<&str> for Identifier { fn from(s: &str) -> Identifier { Self::from_str(s) } } impl Display for Identifier { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } impl Identifier { pub fn as_str(&self) -> &str { self.0.resolve_ref() } } /// Returns `name` without a vendor prefix. /// /// If `name` has no vendor prefix, it's returned as-is. pub(crate) fn unvendor(name: &str) -> &str { let bytes = name.as_bytes(); if bytes.len() < 2 { return name; } if bytes.first() != Some(&b'-') || bytes.get(1_usize) == Some(&b'-') { return name; } for i in 2..bytes.len() { if bytes.get(i) == Some(&b'-') { return &name[i + 1..]; } } name } grass_compiler-0.13.4/src/context_flags.rs000064400000000000000000000060331046102023000167210ustar 00000000000000use std::ops::{BitAnd, BitOr, BitOrAssign}; #[derive(Debug, Copy, Clone)] pub(crate) struct ContextFlags(pub u16); #[derive(Debug, Copy, Clone)] pub(crate) struct ContextFlag(u16); impl ContextFlags { pub const IN_MIXIN: ContextFlag = ContextFlag(1); pub const IN_FUNCTION: ContextFlag = ContextFlag(1 << 1); pub const IN_CONTROL_FLOW: ContextFlag = ContextFlag(1 << 2); pub const IN_KEYFRAMES: ContextFlag = ContextFlag(1 << 3); pub const FOUND_CONTENT_RULE: ContextFlag = ContextFlag(1 << 4); pub const IN_STYLE_RULE: ContextFlag = ContextFlag(1 << 5); pub const IN_UNKNOWN_AT_RULE: ContextFlag = ContextFlag(1 << 6); pub const IN_CONTENT_BLOCK: ContextFlag = ContextFlag(1 << 7); pub const IS_USE_ALLOWED: ContextFlag = ContextFlag(1 << 8); pub const IN_PARENS: ContextFlag = ContextFlag(1 << 9); pub const AT_ROOT_EXCLUDING_STYLE_RULE: ContextFlag = ContextFlag(1 << 10); pub const IN_SUPPORTS_DECLARATION: ContextFlag = ContextFlag(1 << 11); pub const IN_SEMI_GLOBAL_SCOPE: ContextFlag = ContextFlag(1 << 12); pub const fn empty() -> Self { Self(0) } pub fn unset(&mut self, flag: ContextFlag) { self.0 &= !flag.0; } pub fn set(&mut self, flag: ContextFlag, v: bool) { if v { self.0 |= flag.0; } else { self.unset(flag); } } pub fn in_mixin(self) -> bool { (self.0 & Self::IN_MIXIN) != 0 } pub fn in_function(self) -> bool { (self.0 & Self::IN_FUNCTION) != 0 } pub fn in_control_flow(self) -> bool { (self.0 & Self::IN_CONTROL_FLOW) != 0 } pub fn in_keyframes(self) -> bool { (self.0 & Self::IN_KEYFRAMES) != 0 } pub fn in_style_rule(self) -> bool { (self.0 & Self::IN_STYLE_RULE) != 0 } pub fn in_unknown_at_rule(self) -> bool { (self.0 & Self::IN_UNKNOWN_AT_RULE) != 0 } pub fn in_content_block(self) -> bool { (self.0 & Self::IN_CONTENT_BLOCK) != 0 } pub fn in_parens(self) -> bool { (self.0 & Self::IN_PARENS) != 0 } pub fn at_root_excluding_style_rule(self) -> bool { (self.0 & Self::AT_ROOT_EXCLUDING_STYLE_RULE) != 0 } pub fn in_supports_declaration(self) -> bool { (self.0 & Self::IN_SUPPORTS_DECLARATION) != 0 } pub fn in_semi_global_scope(self) -> bool { (self.0 & Self::IN_SEMI_GLOBAL_SCOPE) != 0 } pub fn found_content_rule(self) -> bool { (self.0 & Self::FOUND_CONTENT_RULE) != 0 } pub fn is_use_allowed(self) -> bool { (self.0 & Self::IS_USE_ALLOWED) != 0 } } impl BitAnd for u16 { type Output = Self; #[inline] fn bitand(self, rhs: ContextFlag) -> Self::Output { self & rhs.0 } } impl BitOr for ContextFlags { type Output = Self; fn bitor(self, rhs: ContextFlag) -> Self::Output { Self(self.0 | rhs.0) } } impl BitOrAssign for ContextFlags { fn bitor_assign(&mut self, rhs: ContextFlag) { self.0 |= rhs.0; } } grass_compiler-0.13.4/src/error.rs000064400000000000000000000145401046102023000152140ustar 00000000000000use std::{ error::Error, fmt::{self, Display}, io, string::FromUtf8Error, sync::Arc, }; use codemap::{Span, SpanLoc}; pub type SassResult = Result>; /// `SassError`s can be either a structured error specific to `grass` or an /// `io::Error`. /// /// In the former case, the best way to interact with the error is to simply print /// it to the user. The `Display` implementation of this kind of error mirrors /// that of the errors `dart-sass` emits, e.g. ///```scss /// Error: $number: foo is not a number. /// | /// 308 | color: unit(foo); /// | ^^^ /// | /// ./input.scss:308:17 ///``` /// #[derive(Debug, Clone)] pub struct SassError { kind: SassErrorKind, } impl SassError { #[must_use] pub fn kind(self) -> PublicSassErrorKind { match self.kind { SassErrorKind::ParseError { message, loc, unicode, } => PublicSassErrorKind::ParseError { message, loc, unicode, }, SassErrorKind::FromUtf8Error(s) => PublicSassErrorKind::FromUtf8Error(s), SassErrorKind::IoError(io) => PublicSassErrorKind::IoError(io), SassErrorKind::Raw(..) => unreachable!("raw errors should not be accessible by users"), } } pub(crate) fn raw(self) -> (String, Span) { match self.kind { SassErrorKind::Raw(string, span) => (string, span), e => unreachable!("unable to get raw of {:?}", e), } } pub(crate) const fn from_loc(message: String, loc: SpanLoc, unicode: bool) -> Self { SassError { kind: SassErrorKind::ParseError { message, loc, unicode, }, } } } #[non_exhaustive] #[derive(Debug, Clone)] pub enum PublicSassErrorKind { ParseError { /// The message related to this parse error. /// /// Error messages should only be used to assist in debugging for the /// end user. They may change significantly between bugfix versions and /// should not be relied on to remain stable. /// /// Error messages do not contain the `Error: ` prefix or pretty-printed /// span and context information as is shown in the `Display` implementation. message: String, loc: SpanLoc, /// Whether or not the user allows unicode characters to be emitted in /// error messages. /// /// This is configurable with [`crate::Options::unicode_error_messages`] unicode: bool, }, /// Sass was unable to find the entry-point file. /// /// Files that cannot be found using `@import`, `@use`, and `@forward` will /// emit [`Self::ParseError`]s IoError(Arc), /// The entry-point file or an imported file was not valid UTF-8. FromUtf8Error(String), } #[derive(Debug, Clone)] 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, unicode: bool, }, // we put `IoError`s in an `Arc` to allow them to be cloneable IoError(Arc), FromUtf8Error(String), } impl Display for SassError { // TODO: trim whitespace from start of line shown in error // TODO: color errors // TODO: integrate with codemap-diagnostics #[inline] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let (message, loc, unicode) = match &self.kind { SassErrorKind::ParseError { message, loc, unicode, } => (message, loc, *unicode), SassErrorKind::FromUtf8Error(..) => return writeln!(f, "Error: Invalid UTF-8."), SassErrorKind::IoError(s) => return writeln!(f, "Error: {}", s), SassErrorKind::Raw(..) => unreachable!(), }; let first_bar = if unicode { '╷' } else { ',' }; let second_bar = if unicode { '│' } else { '|' }; let third_bar = if unicode { '│' } else { '|' }; let fourth_bar = if unicode { '╵' } else { '\'' }; 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, first_bar)?; writeln!( f, "{} {} {}", line, second_bar, loc.file.source_line(loc.begin.line) )?; writeln!( f, "{}{} {}{}", padding, third_bar, vec![' '; loc.begin.column].iter().collect::(), vec!['^'; loc.end.column.max(loc.begin.column) - loc.begin.column.min(loc.end.column)] .iter() .collect::() )?; writeln!(f, "{}{}", padding, fourth_bar)?; if unicode { writeln!(f, "./{}:{}:{}", loc.file.name(), line, col)?; } else { writeln!(f, " {} {}:{} root stylesheet", loc.file.name(), line, col)?; } Ok(()) } } impl From for Box { #[inline] fn from(error: io::Error) -> Box { Box::new(SassError { kind: SassErrorKind::IoError(Arc::new(error)), }) } } impl From for Box { #[inline] fn from(error: FromUtf8Error) -> Box { Box::new(SassError { kind: SassErrorKind::FromUtf8Error(format!( "Invalid UTF-8 character \"\\x{:X?}\"", error.as_bytes()[0] )), }) } } impl From<(&str, Span)> for Box { #[inline] fn from(error: (&str, Span)) -> Box { Box::new(SassError { kind: SassErrorKind::Raw(error.0.to_owned(), error.1), }) } } impl From<(String, Span)> for Box { #[inline] fn from(error: (String, Span)) -> Box { Box::new(SassError { kind: SassErrorKind::Raw(error.0, error.1), }) } } impl Error for SassError { #[inline] fn description(&self) -> &'static str { "Sass parsing error" } } grass_compiler-0.13.4/src/evaluate/bin_op.rs000064400000000000000000000421041046102023000171340ustar 00000000000000use std::cmp::Ordering; use codemap::Span; use crate::{ common::{BinaryOp, QuoteKind}, error::SassResult, unit::Unit, value::{SassNumber, Value}, Options, }; pub(crate) fn add(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match left { Value::Calculation(..) => match right { Value::String(s, quotes) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), quotes, ), _ => { return Err(( format!( "Undefined operation \"{} + {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }, Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", left.inspect(span)?), span, ) .into()) } Value::True | Value::False => match right { Value::String(s, QuoteKind::Quoted) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), QuoteKind::Quoted, ), _ => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, Value::Null => match right { Value::Null => Value::Null, _ => Value::String( right.to_css_string(span, options.is_compressed())?, QuoteKind::None, ), }, Value::Dimension(SassNumber { num, unit, as_slash: _, }) => match right { Value::Dimension(SassNumber { num: num2, unit: unit2, as_slash: _, }) => { if !unit.comparable(&unit2) { return Err( (format!("Incompatible units {} and {}.", unit2, unit), span).into(), ); } if unit == unit2 { Value::Dimension(SassNumber { num: num + num2, unit, as_slash: None, }) } else if unit == Unit::None { Value::Dimension(SassNumber { num: num + num2, unit: unit2, as_slash: None, }) } else if unit2 == Unit::None { Value::Dimension(SassNumber { num: num + num2, unit, as_slash: None, }) } else { Value::Dimension(SassNumber { num: num + num2.convert(&unit2, &unit), unit, as_slash: None, }) } } Value::String(s, q) => Value::String( format!("{}{}{}", num.to_string(options.is_compressed()), unit, s), q, ), Value::Null => Value::String( format!("{}{}", num.to_string(options.is_compressed()), unit), QuoteKind::None, ), Value::True | Value::False | Value::List(..) | Value::ArgList(..) => Value::String( format!( "{}{}{}", num.to_string(options.is_compressed()), unit, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", right.inspect(span)?), span, ) .into()) } Value::Color(..) | Value::Calculation(..) => { return Err(( format!( "Undefined operation \"{}{} + {}\".", num.inspect(), unit, right.inspect(span)? ), span, ) .into()) } }, c @ Value::Color(..) => match right { // todo: we really can't add to any other types? Value::String(..) | Value::Null | Value::List(..) => Value::String( format!( "{}{}", c.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())?, ), QuoteKind::None, ), _ => { return Err(( format!( "Undefined operation \"{} + {}\".", c.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }, Value::String(text, quotes) => match right { Value::String(text2, ..) => Value::String(text + &text2, quotes), _ => Value::String( text + &right.to_css_string(span, options.is_compressed())?, quotes, ), }, Value::List(..) | Value::ArgList(..) => match right { Value::String(s, q) => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, s ), q, ), _ => Value::String( format!( "{}{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, }) } pub(crate) fn sub(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match left { Value::Calculation(..) => { return Err(( format!( "Undefined operation \"{} - {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } Value::Null => Value::String( format!("-{}", right.to_css_string(span, options.is_compressed())?), QuoteKind::None, ), Value::Dimension(SassNumber { num, unit, as_slash: _, }) => match right { Value::Dimension(SassNumber { num: num2, unit: unit2, as_slash: _, }) => { if !unit.comparable(&unit2) { return Err( (format!("Incompatible units {} and {}.", unit2, unit), span).into(), ); } if unit == unit2 { Value::Dimension(SassNumber { num: num - num2, unit, as_slash: None, }) } else if unit == Unit::None { Value::Dimension(SassNumber { num: num - num2, unit: unit2, as_slash: None, }) } else if unit2 == Unit::None { Value::Dimension(SassNumber { num: num - num2, unit, as_slash: None, }) } else { Value::Dimension(SassNumber { num: num - num2.convert(&unit2, &unit), unit, as_slash: None, }) } } Value::List(..) | Value::String(..) | Value::True | Value::False | Value::ArgList(..) => Value::String( format!( "{}{}-{}", num.to_string(options.is_compressed()), unit, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), Value::Map(..) | Value::FunctionRef(..) => { return Err(( format!("{} isn't a valid CSS value.", right.inspect(span)?), span, ) .into()) } Value::Color(..) | Value::Calculation(..) => { return Err(( format!( "Undefined operation \"{}{} - {}\".", num.inspect(), unit, right.inspect(span)? ), span, ) .into()) } Value::Null => Value::String( format!("{}{}-", num.to_string(options.is_compressed()), unit), QuoteKind::None, ), }, c @ Value::Color(..) => match right { Value::Dimension(SassNumber { .. }) | Value::Color(..) => { return Err(( format!( "Undefined operation \"{} - {}\".", c.inspect(span)?, right.inspect(span)? ), span, ) .into()) } _ => Value::String( format!( "{}-{}", c.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, Value::String(..) => Value::String( format!( "{}-{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), // todo: can be greatly simplified _ => match right { Value::String(s, q) => Value::String( format!( "{}-{}{}{}", left.to_css_string(span, options.is_compressed())?, q, s, q ), QuoteKind::None, ), Value::Null => Value::String( format!("{}-", left.to_css_string(span, options.is_compressed())?), QuoteKind::None, ), _ => Value::String( format!( "{}-{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }, }) } pub(crate) fn mul(left: Value, right: Value, _: &Options, span: Span) -> SassResult { Ok(match left { Value::Dimension(SassNumber { num, unit, as_slash: _, }) => match right { Value::Dimension(SassNumber { num: num2, unit: unit2, as_slash: _, }) => { if unit2 == Unit::None { return Ok(Value::Dimension(SassNumber { num: num * num2, unit, as_slash: None, })); } let n = SassNumber { num, unit, as_slash: None, } * SassNumber { num: num2, unit: unit2, as_slash: None, }; Value::Dimension(n) } _ => { return Err(( format!( "Undefined operation \"{}{} * {}\".", num.inspect(), unit, right.inspect(span)? ), span, ) .into()) } }, _ => { return Err(( format!( "Undefined operation \"{} * {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }) } pub(crate) fn cmp( left: &Value, right: &Value, _: &Options, span: Span, op: BinaryOp, ) -> SassResult { let ordering = match left.cmp(right, span, op)? { Some(ord) => ord, None => return Ok(Value::False), }; Ok(match op { BinaryOp::GreaterThan => match ordering { Ordering::Greater => Value::True, Ordering::Less | Ordering::Equal => Value::False, }, BinaryOp::GreaterThanEqual => match ordering { Ordering::Greater | Ordering::Equal => Value::True, Ordering::Less => Value::False, }, BinaryOp::LessThan => match ordering { Ordering::Less => Value::True, Ordering::Greater | Ordering::Equal => Value::False, }, BinaryOp::LessThanEqual => match ordering { Ordering::Less | Ordering::Equal => Value::True, Ordering::Greater => Value::False, }, _ => unreachable!(), }) } pub(crate) fn single_eq( left: &Value, right: &Value, options: &Options, span: Span, ) -> SassResult { Ok(Value::String( format!( "{}={}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, )) } pub(crate) fn div(left: Value, right: Value, options: &Options, span: Span) -> SassResult { Ok(match (left, right) { (Value::Dimension(num1), Value::Dimension(num2)) => { if num2.unit == Unit::None { return Ok(Value::Dimension(SassNumber { num: num1.num / num2.num, unit: num1.unit, as_slash: None, })); } let n = SassNumber { num: num1.num, unit: num1.unit, as_slash: None, } / SassNumber { num: num2.num, unit: num2.unit, as_slash: None, }; Value::Dimension(n) } ( left @ Value::Color(..), right @ (Value::Dimension(SassNumber { .. }) | Value::Color(..)), ) => { return Err(( format!( "Undefined operation \"{} / {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } (left, right) => Value::String( format!( "{}/{}", left.to_css_string(span, options.is_compressed())?, right.to_css_string(span, options.is_compressed())? ), QuoteKind::None, ), }) } pub(crate) fn rem(left: Value, right: Value, _: &Options, span: Span) -> SassResult { Ok(match (left, right) { (Value::Dimension(num1), Value::Dimension(num2)) => { if !num1.unit.comparable(&num2.unit) { return Err(( format!("Incompatible units {} and {}.", num1.unit, num2.unit), span, ) .into()); } let new_num = num1.num % num2.num.convert(&num2.unit, &num1.unit); let new_unit = if num1.unit == num2.unit { num1.unit } else if num1.unit == Unit::None { num2.unit } else { num1.unit }; Value::Dimension(SassNumber { num: new_num, unit: new_unit, as_slash: None, }) } (left, right) => { return Err(( format!( "Undefined operation \"{} % {}\".", left.inspect(span)?, right.inspect(span)? ), span, ) .into()) } }) } grass_compiler-0.13.4/src/evaluate/css_tree.rs000064400000000000000000000105041046102023000174740ustar 00000000000000use std::{ cell::{Ref, RefCell, RefMut}, collections::BTreeMap, }; use crate::ast::CssStmt; #[derive(Debug, Clone)] pub(super) struct CssTree { // None is tombstone stmts: Vec>>, pub parent_to_child: BTreeMap>, pub child_to_parent: BTreeMap, } #[derive(Debug, Copy, Clone, Hash, Eq, PartialEq, PartialOrd, Ord)] #[repr(transparent)] pub(super) struct CssTreeIdx(usize); impl CssTree { pub const ROOT: CssTreeIdx = CssTreeIdx(0); pub fn new() -> Self { let mut tree = Self { stmts: Vec::new(), parent_to_child: BTreeMap::new(), child_to_parent: BTreeMap::new(), }; tree.stmts.push(RefCell::new(None)); tree } pub fn get(&self, idx: CssTreeIdx) -> Ref> { self.stmts[idx.0].borrow() } pub fn get_mut(&self, idx: CssTreeIdx) -> RefMut> { self.stmts[idx.0].borrow_mut() } pub fn finish(self) -> Vec { let mut idx = 1; while idx < self.stmts.len() - 1 { if self.stmts[idx].borrow().is_none() || !self.has_children(CssTreeIdx(idx)) { idx += 1; continue; } self.apply_children(CssTreeIdx(idx)); idx += 1; } self.stmts .into_iter() .filter_map(RefCell::into_inner) .collect() } fn apply_children(&self, parent: CssTreeIdx) { for &child in &self.parent_to_child[&parent] { if self.has_children(child) { self.apply_children(child); } match self.stmts[child.0].borrow_mut().take() { Some(child) => self.add_child_to_parent(child, parent), None => continue, }; } } fn has_children(&self, parent: CssTreeIdx) -> bool { self.parent_to_child.contains_key(&parent) } fn add_child_to_parent(&self, child: CssStmt, parent_idx: CssTreeIdx) { RefMut::map(self.stmts[parent_idx.0].borrow_mut(), |parent| { match parent { Some(CssStmt::RuleSet { body, .. }) => body.push(child), Some(CssStmt::Style(..) | CssStmt::Comment(..) | CssStmt::Import(..)) | None => { unreachable!() } Some(CssStmt::Media(media, ..)) => { media.body.push(child); } Some(CssStmt::UnknownAtRule(at_rule, ..)) => { at_rule.body.push(child); } Some(CssStmt::Supports(supports, ..)) => { supports.body.push(child); } Some(CssStmt::KeyframesRuleSet(keyframes)) => { keyframes.body.push(child); } }; parent }); } pub fn add_child(&mut self, child: CssStmt, parent_idx: CssTreeIdx) -> CssTreeIdx { let child_idx = self.add_stmt_inner(child); self.parent_to_child .entry(parent_idx) .or_default() .push(child_idx); self.child_to_parent.insert(child_idx, parent_idx); child_idx } pub fn link_child_to_parent(&mut self, child_idx: CssTreeIdx, parent_idx: CssTreeIdx) { self.parent_to_child .entry(parent_idx) .or_default() .push(child_idx); self.child_to_parent.insert(child_idx, parent_idx); } pub fn has_following_sibling(&self, child: CssTreeIdx) -> bool { if child == Self::ROOT { return false; } let parent_idx = self.child_to_parent.get(&child).unwrap(); let parent_children = self.parent_to_child.get(parent_idx).unwrap(); // todo: we shouldn't take into account children that are invisible parent_children.last() != Some(&child) } pub fn add_stmt(&mut self, child: CssStmt, parent: Option) -> CssTreeIdx { match parent { Some(parent) => self.add_child(child, parent), None => self.add_child(child, Self::ROOT), } } fn add_stmt_inner(&mut self, stmt: CssStmt) -> CssTreeIdx { let idx = CssTreeIdx(self.stmts.len()); self.stmts.push(RefCell::new(Some(stmt))); idx } } grass_compiler-0.13.4/src/evaluate/env.rs000064400000000000000000000425401046102023000164620ustar 00000000000000use codemap::{Span, Spanned}; use crate::{ ast::{AstForwardRule, Configuration, ConfiguredValue, Mixin}, builtin::modules::{ForwardedModule, Module, ModuleScope, Modules, ShadowedModule}, common::Identifier, error::SassResult, selector::ExtensionStore, value::{SassFunction, Value}, }; use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, sync::Arc, }; type Mutable = Arc>; use super::{scope::Scopes, visitor::CallableContentBlock}; #[derive(Debug, Clone)] pub(crate) struct Environment { pub scopes: Scopes, pub modules: Mutable, pub global_modules: Vec>, pub content: Option>, pub forwarded_modules: Mutable>>, pub imported_modules: Mutable>>, #[allow(clippy::type_complexity)] pub nested_forwarded_modules: Option>>>>>, } impl Environment { pub fn new() -> Self { Self { scopes: Scopes::new(), modules: Arc::new(RefCell::new(Modules::new())), global_modules: Vec::new(), content: None, forwarded_modules: Arc::new(RefCell::new(Vec::new())), imported_modules: Arc::new(RefCell::new(Vec::new())), nested_forwarded_modules: None, } } pub fn new_closure(&self) -> Self { Self { scopes: self.scopes.new_closure(), modules: Arc::clone(&self.modules), global_modules: self.global_modules.iter().map(Arc::clone).collect(), content: self.content.as_ref().map(Arc::clone), forwarded_modules: Arc::clone(&self.forwarded_modules), imported_modules: Arc::clone(&self.imported_modules), nested_forwarded_modules: self.nested_forwarded_modules.as_ref().map(Arc::clone), } } pub fn for_import(&self) -> Self { Self { scopes: self.scopes.new_closure(), modules: Arc::new(RefCell::new(Modules::new())), global_modules: Vec::new(), content: self.content.as_ref().map(Arc::clone), forwarded_modules: Arc::clone(&self.forwarded_modules), imported_modules: Arc::clone(&self.imported_modules), nested_forwarded_modules: self.nested_forwarded_modules.as_ref().map(Arc::clone), } } pub fn to_dummy_module(&self, span: Span) -> Module { Module::Environment { scope: ModuleScope::new(), upstream: Vec::new(), extension_store: ExtensionStore::new(span), env: self.clone(), } } /// Makes the members forwarded by [module] available in the current /// environment. /// /// This is called when [module] is `@import`ed. pub fn import_forwards(&mut self, _env: Module) { if let Module::Environment { env, .. } = _env { let mut forwarded = env.forwarded_modules; if (*forwarded).borrow().is_empty() { return; } // Omit modules from [forwarded] that are already globally available and // forwarded in this module. let forwarded_modules = Arc::clone(&self.forwarded_modules); if !(*forwarded_modules).borrow().is_empty() { // todo: intermediate name let mut x = Vec::new(); for entry in (*forwarded).borrow().iter() { if !forwarded_modules .borrow() .iter() .any(|module| Arc::ptr_eq(module, entry)) || !self .global_modules .iter() .any(|module| Arc::ptr_eq(module, entry)) { x.push(Arc::clone(entry)); } } forwarded = Arc::new(RefCell::new(x)); } let forwarded_var_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().variables.keys()) .collect::>(); let forwarded_fn_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().functions.keys()) .collect::>(); let forwarded_mixin_names = forwarded .borrow() .iter() .flat_map(|module| (*module).borrow().scope().mixins.keys()) .collect::>(); if self.at_root() { let mut to_remove = Vec::new(); // Hide members from modules that have already been imported or // forwarded that would otherwise conflict with the @imported members. for (idx, module) in (*self.imported_modules).borrow().iter().enumerate() { let shadowed = ShadowedModule::if_necessary( Arc::clone(module), Some(&forwarded_var_names), Some(&forwarded_fn_names), Some(&forwarded_mixin_names), ); if shadowed.is_some() { to_remove.push(idx); } } let mut imported_modules = (*self.imported_modules).borrow_mut(); for &idx in to_remove.iter().rev() { imported_modules.remove(idx); } to_remove.clear(); for (idx, module) in (*self.forwarded_modules).borrow().iter().enumerate() { let shadowed = ShadowedModule::if_necessary( Arc::clone(module), Some(&forwarded_var_names), Some(&forwarded_fn_names), Some(&forwarded_mixin_names), ); if shadowed.is_some() { to_remove.push(idx); } } let mut forwarded_modules = (*self.forwarded_modules).borrow_mut(); for &idx in to_remove.iter().rev() { forwarded_modules.remove(idx); } imported_modules.extend(forwarded.borrow().iter().map(Arc::clone)); forwarded_modules.extend(forwarded.borrow().iter().map(Arc::clone)); } else { self.nested_forwarded_modules .get_or_insert_with(|| { Arc::new(RefCell::new( (0..self.scopes.len()) .map(|_| Arc::new(RefCell::new(Vec::new()))) .collect(), )) }) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .extend(forwarded.borrow().iter().map(Arc::clone)); } // Remove existing member definitions that are now shadowed by the // forwarded modules. for variable in forwarded_var_names { (*self.scopes.variables) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&variable); } self.scopes.last_variable_index = None; for func in forwarded_fn_names { (*self.scopes.functions) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&func); } for mixin in forwarded_mixin_names { (*self.scopes.mixins) .borrow_mut() .last_mut() .unwrap() .borrow_mut() .remove(&mixin); } } } pub fn to_implicit_configuration(&self) -> Configuration { let mut configuration = BTreeMap::new(); let variables = (*self.scopes.variables).borrow(); for variables in variables.iter() { let entries = (**variables).borrow(); for (key, value) in entries.iter() { // Implicit configurations are never invalid, making [configurationSpan] // unnecessary, so we pass null here to avoid having to compute it. configuration.insert(*key, ConfiguredValue::implicit(value.clone())); } } Configuration::implicit(configuration) } pub fn forward_module(&mut self, module: Arc>, rule: AstForwardRule) { let view = ForwardedModule::if_necessary(module, rule); (*self.forwarded_modules).borrow_mut().push(view); // todo: assertnoconflicts } pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) { self.scopes.insert_mixin(name, mixin); } pub fn mixin_exists(&self, name: Identifier) -> bool { self.scopes.mixin_exists(name) } pub fn get_mixin( &self, name: Spanned, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return (*module).borrow().get_mixin(name); } match self.scopes.get_mixin(name) { Ok(v) => Ok(v), Err(e) => { if let Some(v) = self.get_mixin_from_global_modules(name.node) { return Ok(v); } Err(e) } } } pub fn insert_fn(&mut self, func: SassFunction) { self.scopes.insert_fn(func); } pub fn fn_exists(&self, name: Identifier) -> bool { self.scopes.fn_exists(name) } pub fn get_fn( &self, name: Identifier, namespace: Option>, ) -> SassResult> { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return Ok((*module).borrow().get_fn(name)); } Ok(self .scopes .get_fn(name) .or_else(|| self.get_function_from_global_modules(name))) } pub fn var_exists( &self, name: Identifier, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return Ok((*module).borrow().var_exists(name)); } Ok(self.scopes.var_exists(name)) } pub fn get_var( &mut self, name: Spanned, namespace: Option>, ) -> SassResult { if let Some(namespace) = namespace { let modules = (*self.modules).borrow(); let module = modules.get(namespace.node, namespace.span)?; return (*module).borrow().get_var(name); } match self.scopes.get_var(name) { Ok(v) => Ok(v), Err(e) => { if let Some(v) = self.get_variable_from_global_modules(name.node) { Ok(v) } else { Err(e) } } } } pub fn insert_var( &mut self, name: Spanned, namespace: Option>, value: Value, is_global: bool, in_semi_global_scope: bool, ) -> SassResult<()> { if let Some(namespace) = namespace { let mut modules = (*self.modules).borrow_mut(); let module = modules.get_mut(namespace.node, namespace.span)?; (*module).borrow_mut().update_var(name, value)?; return Ok(()); } if is_global || self.at_root() { // If this module doesn't already contain a variable named [name], try // setting it in a global module. if !self.scopes.global_var_exists(name.node) { let module_with_name = self.from_one_module(name.node, "variable", |module| { if module.borrow().var_exists(*name) { Some(Arc::clone(module)) } else { None } }); if let Some(module_with_name) = module_with_name { module_with_name.borrow_mut().update_var(name, value)?; return Ok(()); } } self.scopes.insert_var(0, name.node, value); return Ok(()); } let mut index = self .scopes .find_var(name.node) .unwrap_or(self.scopes.len() - 1); if !in_semi_global_scope && index == 0 { index = self.scopes.len() - 1; } self.scopes.last_variable_index = Some((name.node, index)); self.scopes.insert_var(index, name.node, value); Ok(()) } pub fn at_root(&self) -> bool { self.scopes.len() == 1 } pub fn scopes_mut(&mut self) -> &mut Scopes { &mut self.scopes } pub fn global_vars(&self) -> Arc>> { self.scopes.global_variables() } pub fn global_mixins(&self) -> Arc>> { self.scopes.global_mixins() } pub fn global_functions(&self) -> Arc>> { self.scopes.global_functions() } fn get_variable_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "variable", |module| { (**module).borrow().get_var_no_err(name) }) } fn get_function_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "function", |module| (**module).borrow().get_fn(name)) } fn get_mixin_from_global_modules(&self, name: Identifier) -> Option { self.from_one_module(name, "mixin", |module| { (**module).borrow().get_mixin_no_err(name) }) } pub fn add_module( &mut self, namespace: Option, module: Arc>, span: Span, ) -> SassResult<()> { match namespace { Some(namespace) => { (*self.modules) .borrow_mut() .insert(namespace, module, span)?; } None => { for name in (*self.scopes.global_variables()).borrow().keys() { if (*module).borrow().var_exists(*name) { return Err(( format!("This module and the new module both define a variable named \"${name}\".", name = name) , span).into()); } } self.global_modules.push(module); } } Ok(()) } pub fn to_module(self, extension_store: ExtensionStore) -> Arc> { debug_assert!(self.at_root()); Arc::new(RefCell::new(Module::new_env(self, extension_store))) } fn from_one_module( &self, _name: Identifier, _ty: &str, callback: impl Fn(&Arc>) -> Option, ) -> Option { if let Some(nested_forwarded_modules) = &self.nested_forwarded_modules { for modules in nested_forwarded_modules.borrow().iter().rev() { for module in modules.borrow().iter().rev() { if let Some(value) = callback(module) { return Some(value); } } } } for module in self.imported_modules.borrow().iter() { if let Some(value) = callback(module) { return Some(value); } } let mut value: Option = None; // Object? identity; for module in self.global_modules.iter() { let value_in_module = match callback(module) { Some(v) => v, None => continue, }; value = Some(value_in_module); // Object? identityFromModule = valueInModule is AsyncCallable // ? valueInModule // : module.variableIdentity(name); // if (identityFromModule == identity) continue; // if (value != null) { // var spans = _globalModules.entries.map( // (entry) => callback(entry.key).andThen((_) => entry.value.span)); // throw MultiSpanSassScriptException( // 'This $type is available from multiple global modules.', // '$type use', { // for (var span in spans) // if (span != null) span: 'includes $type' // }); // } // value = valueInModule; // identity = identityFromModule; } value } } grass_compiler-0.13.4/src/evaluate/mod.rs000064400000000000000000000002321046102023000164410ustar 00000000000000pub(crate) use bin_op::{cmp, div}; pub(crate) use env::Environment; pub use visitor::Visitor; mod bin_op; mod css_tree; mod env; mod scope; mod visitor; grass_compiler-0.13.4/src/evaluate/scope.rs000064400000000000000000000174071046102023000170070ustar 00000000000000use std::{ cell::{Cell, RefCell}, collections::BTreeMap, sync::Arc, }; use codemap::Spanned; use crate::{ ast::Mixin, builtin::GLOBAL_FUNCTIONS, common::Identifier, error::SassResult, value::{SassFunction, Value}, }; #[allow(clippy::type_complexity)] #[derive(Debug, Default, Clone)] pub(crate) struct Scopes { pub(crate) variables: Arc>>>>>, pub(crate) mixins: Arc>>>>>, pub(crate) functions: Arc>>>>>, len: Arc>, pub last_variable_index: Option<(Identifier, usize)>, } impl Scopes { pub fn new() -> Self { Self { variables: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])), mixins: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])), functions: Arc::new(RefCell::new(vec![Arc::new(RefCell::new(BTreeMap::new()))])), len: Arc::new(Cell::new(1)), last_variable_index: None, } } pub fn new_closure(&self) -> Self { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); Self { variables: Arc::new(RefCell::new( (*self.variables).borrow().iter().map(Arc::clone).collect(), )), mixins: Arc::new(RefCell::new( (*self.mixins).borrow().iter().map(Arc::clone).collect(), )), functions: Arc::new(RefCell::new( (*self.functions).borrow().iter().map(Arc::clone).collect(), )), len: Arc::new(Cell::new(self.len())), last_variable_index: self.last_variable_index, } } pub fn global_variables(&self) -> Arc>> { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); Arc::clone(&(*self.variables).borrow()[0]) } pub fn global_functions(&self) -> Arc>> { Arc::clone(&(*self.functions).borrow()[0]) } pub fn global_mixins(&self) -> Arc>> { Arc::clone(&(*self.mixins).borrow()[0]) } pub fn find_var(&mut self, name: Identifier) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); match self.last_variable_index { Some((prev_name, idx)) if prev_name == name => return Some(idx), _ => {} }; for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() { if (**scope).borrow().contains_key(&name) { self.last_variable_index = Some((name, idx)); return Some(idx); } } None } pub fn len(&self) -> usize { (*self.len).get() } pub fn enter_new_scope(&mut self) { let len = self.len(); debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*self.len).set(len + 1); (*self.variables) .borrow_mut() .push(Arc::new(RefCell::new(BTreeMap::new()))); (*self.mixins) .borrow_mut() .push(Arc::new(RefCell::new(BTreeMap::new()))); (*self.functions) .borrow_mut() .push(Arc::new(RefCell::new(BTreeMap::new()))); } pub fn exit_scope(&mut self) { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); let len = self.len(); (*self.len).set(len - 1); (*self.variables).borrow_mut().pop(); (*self.mixins).borrow_mut().pop(); (*self.functions).borrow_mut().pop(); self.last_variable_index = None; } } /// Variables impl Scopes { pub fn insert_var(&mut self, idx: usize, name: Identifier, v: Value) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.variables).borrow_mut()[idx]) .borrow_mut() .insert(name, v) } /// Always insert this variable into the innermost scope /// /// Used, for example, for variables from `@each` and `@for` pub fn insert_var_last(&mut self, name: Identifier, v: Value) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); let last_idx = self.len() - 1; self.last_variable_index = Some((name, last_idx)); (*(*self.variables).borrow_mut()[last_idx]) .borrow_mut() .insert(name, v) } pub fn get_var(&mut self, name: Spanned) -> SassResult { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); match self.last_variable_index { Some((prev_name, idx)) if prev_name == name.node => { return Ok((*(*self.variables).borrow()[idx]).borrow()[&name.node].clone()); } _ => {} }; for (idx, scope) in (*self.variables).borrow().iter().enumerate().rev() { match (**scope).borrow().get(&name.node) { Some(var) => { self.last_variable_index = Some((name.node, idx)); return Ok(var.clone()); } None => continue, } } Err(("Undefined variable.", name.span).into()) } pub fn var_exists(&self, name: Identifier) -> bool { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.variables).borrow().iter() { if (**scope).borrow().contains_key(&name) { return true; } } false } pub fn global_var_exists(&self, name: Identifier) -> bool { self.global_variables().borrow().contains_key(&name) } } /// Mixins impl Scopes { pub fn insert_mixin(&mut self, name: Identifier, mixin: Mixin) { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.mixins).borrow_mut().last_mut().unwrap()) .borrow_mut() .insert(name, mixin); } pub fn get_mixin(&self, name: Spanned) -> SassResult { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.mixins).borrow().iter().rev() { match (**scope).borrow().get(&name.node) { Some(mixin) => return Ok(mixin.clone()), None => continue, } } Err(("Undefined mixin.", name.span).into()) } pub fn mixin_exists(&self, name: Identifier) -> bool { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.mixins).borrow().iter() { if (**scope).borrow().contains_key(&name) { return true; } } false } } /// Functions impl Scopes { pub fn insert_fn(&mut self, func: SassFunction) { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); (*(*self.functions).borrow_mut().last_mut().unwrap()) .borrow_mut() .insert(func.name(), func); } pub fn get_fn(&self, name: Identifier) -> Option { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.functions).borrow().iter().rev() { let func = (**scope).borrow().get(&name).cloned(); if func.is_some() { return func; } } None } pub fn fn_exists(&self, name: Identifier) -> bool { debug_assert_eq!(self.len(), (*self.variables).borrow().len()); for scope in (*self.functions).borrow().iter() { if (**scope).borrow().contains_key(&name) { return true; } } GLOBAL_FUNCTIONS.contains_key(name.as_str()) } } grass_compiler-0.13.4/src/evaluate/visitor.rs000064400000000000000000003170441046102023000173750ustar 00000000000000use std::{ cell::{Cell, RefCell}, collections::{BTreeMap, BTreeSet, HashSet}, ffi::OsStr, fmt, iter::FromIterator, mem, path::{Path, PathBuf}, rc::Rc, sync::Arc, }; use codemap::{CodeMap, Span, Spanned}; use indexmap::IndexSet; use crate::{ ast::*, builtin::{ meta::if_arguments, modules::{ declare_module_color, declare_module_list, declare_module_map, declare_module_math, declare_module_meta, declare_module_selector, declare_module_string, Module, }, GLOBAL_FUNCTIONS, }, common::{unvendor, BinaryOp, Identifier, ListSeparator, QuoteKind, UnaryOp}, error::{SassError, SassResult}, interner::InternedString, lexer::Lexer, parse::{ AtRootQueryParser, CssParser, KeyframesSelectorParser, SassParser, ScssParser, StylesheetParser, }, selector::{ ComplexSelectorComponent, ExtendRule, ExtendedSelector, ExtensionStore, SelectorList, SelectorParser, }, utils::{to_sentence, trim_ascii}, value::{ ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction, SassMap, SassNumber, UserDefinedFunction, Value, }, ContextFlags, InputSyntax, Options, }; use super::{ bin_op::{add, cmp, div, mul, rem, single_eq, sub}, css_tree::{CssTree, CssTreeIdx}, env::Environment, }; trait UserDefinedCallable { fn name(&self) -> Identifier; fn arguments(&self) -> &ArgumentDeclaration; } impl UserDefinedCallable for AstFunctionDecl { fn name(&self) -> Identifier { self.name.node } fn arguments(&self) -> &ArgumentDeclaration { &self.arguments } } impl UserDefinedCallable for Arc { fn name(&self) -> Identifier { self.name.node } fn arguments(&self) -> &ArgumentDeclaration { &self.arguments } } impl UserDefinedCallable for AstMixin { fn name(&self) -> Identifier { self.name } fn arguments(&self) -> &ArgumentDeclaration { &self.args } } impl UserDefinedCallable for Arc { fn name(&self) -> Identifier { Identifier::from("@content") } fn arguments(&self) -> &ArgumentDeclaration { &self.content.args } } #[derive(Debug, Clone)] pub(crate) struct CallableContentBlock { content: AstContentBlock, env: Environment, } /// Evaluation context of the current execution #[derive(Debug)] pub struct Visitor<'a> { pub(crate) declaration_name: Option, pub(crate) flags: ContextFlags, pub(crate) env: Environment, pub(crate) style_rule_ignoring_at_root: Option, // avoid emitting duplicate warnings for the same span pub(crate) warnings_emitted: HashSet, pub(crate) media_queries: Option>, pub(crate) media_query_sources: Option>, pub(crate) extender: ExtensionStore, /// The complete file path of the current file being visited. Imports are /// resolved relative to this path pub current_import_path: PathBuf, pub(crate) is_plain_css: bool, pub(crate) modules: BTreeMap>>, pub(crate) active_modules: BTreeSet, css_tree: CssTree, parent: Option, configuration: Rc>, import_nodes: Vec, pub options: &'a Options<'a>, pub(crate) map: &'a mut CodeMap, // todo: remove empty_span: Span, import_cache: BTreeMap, /// As a simple heuristic, we don't cache the results of an import unless it /// has been seen in the past. In the majority of cases, files are imported /// at most once. files_seen: BTreeSet, } impl<'a> Visitor<'a> { pub fn new( path: &Path, options: &'a Options<'a>, map: &'a mut CodeMap, empty_span: Span, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, true); let extender = ExtensionStore::new(empty_span); let current_import_path = path.to_path_buf(); Self { declaration_name: None, style_rule_ignoring_at_root: None, flags, warnings_emitted: HashSet::new(), media_queries: None, media_query_sources: None, env: Environment::new(), extender, css_tree: CssTree::new(), parent: None, current_import_path, configuration: Rc::new(RefCell::new(Configuration::empty())), is_plain_css: false, import_nodes: Vec::new(), modules: BTreeMap::new(), active_modules: BTreeSet::new(), options, empty_span, map, import_cache: BTreeMap::new(), files_seen: BTreeSet::new(), } } pub(crate) fn visit_stylesheet(&mut self, mut style_sheet: StyleSheet) -> SassResult<()> { self.active_modules.insert(style_sheet.url.clone()); let was_in_plain_css = self.is_plain_css; self.is_plain_css = style_sheet.is_plain_css; mem::swap(&mut self.current_import_path, &mut style_sheet.url); for stmt in style_sheet.body { let result = self.visit_stmt(stmt)?; debug_assert!(result.is_none()); } mem::swap(&mut self.current_import_path, &mut style_sheet.url); self.is_plain_css = was_in_plain_css; self.active_modules.remove(&style_sheet.url); Ok(()) } pub(crate) fn finish(mut self) -> Vec { let mut finished_tree = self.css_tree.finish(); if self.import_nodes.is_empty() { finished_tree } else { self.import_nodes.append(&mut finished_tree); self.import_nodes } } fn visit_return_rule(&mut self, ret: AstReturn) -> SassResult> { let val = self.visit_expr(ret.val)?; Ok(Some(self.without_slash(val))) } // todo: we really don't have to return Option from all of these children pub(crate) fn visit_stmt(&mut self, stmt: AstStmt) -> SassResult> { match stmt { AstStmt::RuleSet(ruleset) => self.visit_ruleset(ruleset), AstStmt::Style(style) => self.visit_style(style), AstStmt::SilentComment(..) => Ok(None), AstStmt::If(if_stmt) => self.visit_if_stmt(if_stmt), AstStmt::For(for_stmt) => self.visit_for_stmt(for_stmt), AstStmt::Return(ret) => self.visit_return_rule(ret), AstStmt::Each(each_stmt) => self.visit_each_stmt(each_stmt), AstStmt::Media(media_rule) => self.visit_media_rule(media_rule), AstStmt::Include(include_stmt) => self.visit_include_stmt(include_stmt), AstStmt::While(while_stmt) => self.visit_while_stmt(&while_stmt), AstStmt::VariableDecl(decl) => self.visit_variable_decl(decl), AstStmt::LoudComment(comment) => self.visit_loud_comment(comment), AstStmt::ImportRule(import_rule) => self.visit_import_rule(import_rule), AstStmt::FunctionDecl(func) => { self.visit_function_decl(func); Ok(None) } AstStmt::Mixin(mixin) => { self.visit_mixin_decl(mixin); Ok(None) } AstStmt::ContentRule(content_rule) => self.visit_content_rule(content_rule), AstStmt::Warn(warn_rule) => { self.visit_warn_rule(warn_rule)?; Ok(None) } AstStmt::UnknownAtRule(unknown_at_rule) => self.visit_unknown_at_rule(unknown_at_rule), AstStmt::ErrorRule(error_rule) => Err(self.visit_error_rule(error_rule)?), AstStmt::Extend(extend_rule) => self.visit_extend_rule(extend_rule), AstStmt::AtRootRule(at_root_rule) => self.visit_at_root_rule(at_root_rule), AstStmt::Debug(debug_rule) => self.visit_debug_rule(debug_rule), AstStmt::Use(use_rule) => { self.visit_use_rule(use_rule)?; Ok(None) } AstStmt::Forward(forward_rule) => { self.visit_forward_rule(forward_rule)?; Ok(None) } AstStmt::Supports(supports_rule) => { self.visit_supports_rule(supports_rule)?; Ok(None) } } } fn visit_forward_rule(&mut self, forward_rule: AstForwardRule) -> SassResult<()> { let old_config = Rc::clone(&self.configuration); let adjusted_config = Configuration::through_forward(Rc::clone(&old_config), &forward_rule); if !forward_rule.configuration.is_empty() { let new_configuration = self.add_forward_configuration(Rc::clone(&adjusted_config), &forward_rule)?; self.load_module( forward_rule.url.as_path(), Some(Rc::clone(&new_configuration)), false, forward_rule.span, |visitor, module, _| { visitor.env.forward_module(module, forward_rule.clone()); Ok(()) }, )?; Self::remove_used_configuration( &adjusted_config, &new_configuration, &forward_rule .configuration .iter() .filter(|var| !var.is_guarded) .map(|var| var.name.node) .collect(), ); // Remove all the variables that weren't configured by this particular // `@forward` before checking that the configuration is empty. Errors for // outer `with` clauses will be thrown once those clauses finish // executing. let configured_variables: HashSet = forward_rule .configuration .iter() .map(|var| var.name.node) .collect(); let mut to_remove = Vec::new(); for name in (*new_configuration).borrow().values.keys() { if !configured_variables.contains(&name) { to_remove.push(name); } } for name in to_remove { (*new_configuration).borrow_mut().remove(name); } Self::assert_configuration_is_empty(&new_configuration, false)?; } else { self.configuration = adjusted_config; let url = forward_rule.url.clone(); self.load_module( url.as_path(), None, false, forward_rule.span, move |visitor, module, _| { visitor.env.forward_module(module, forward_rule.clone()); Ok(()) }, )?; self.configuration = old_config; } Ok(()) } #[allow(clippy::unnecessary_unwrap)] fn add_forward_configuration( &mut self, config: Rc>, forward_rule: &AstForwardRule, ) -> SassResult>> { let mut new_values = BTreeMap::from_iter((*config).borrow().values.iter()); for variable in &forward_rule.configuration { if variable.is_guarded { let old_value = (*config).borrow_mut().remove(variable.name.node); if old_value.is_some() && !matches!( old_value, Some(ConfiguredValue { value: Value::Null, .. }) ) { new_values.insert(variable.name.node, old_value.unwrap()); continue; } } // todo: superfluous clone? let value = self.visit_expr(variable.expr.node.clone())?; let value = self.without_slash(value); new_values.insert( variable.name.node, ConfiguredValue::explicit(value, variable.expr.span), ); } Ok(Rc::new(RefCell::new( if !(*config).borrow().is_implicit() || (*config).borrow().is_empty() { Configuration::explicit(new_values, forward_rule.span) } else { Configuration::implicit(new_values) }, ))) } /// Remove configured values from [upstream] that have been removed from /// [downstream], unless they match a name in [except]. fn remove_used_configuration( upstream: &Rc>, downstream: &Rc>, except: &HashSet, ) { let mut names_to_remove = Vec::new(); let downstream_keys = (*downstream).borrow().values.keys(); for name in (*upstream).borrow().values.keys() { if except.contains(&name) { continue; } if !downstream_keys.contains(&name) { names_to_remove.push(name); } } for name in names_to_remove { (*upstream).borrow_mut().remove(name); } } fn parenthesize_supports_condition( &mut self, condition: AstSupportsCondition, operator: Option<&str>, ) -> SassResult { match &condition { AstSupportsCondition::Negation(..) => { Ok(format!("({})", self.visit_supports_condition(condition)?)) } AstSupportsCondition::Operation { operator: operator2, .. } if operator2.is_none() || operator2.as_deref() != operator => { Ok(format!("({})", self.visit_supports_condition(condition)?)) } _ => self.visit_supports_condition(condition), } } fn visit_supports_condition(&mut self, condition: AstSupportsCondition) -> SassResult { match condition { AstSupportsCondition::Operation { left, operator, right, } => Ok(format!( "{} {} {}", self.parenthesize_supports_condition(*left, operator.as_deref())?, operator.as_ref().unwrap(), self.parenthesize_supports_condition(*right, operator.as_deref())? )), AstSupportsCondition::Negation(condition) => Ok(format!( "not {}", self.parenthesize_supports_condition(*condition, None)? )), AstSupportsCondition::Interpolation(expr) => { self.evaluate_to_css(expr, QuoteKind::None, self.empty_span) } AstSupportsCondition::Declaration { name, value } => { let old_in_supports_decl = self.flags.in_supports_declaration(); self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, true); let is_custom_property = match &name { AstExpr::String(StringExpr(text, QuoteKind::None), ..) => { text.initial_plain().starts_with("--") } _ => false, }; let result = format!( "({}:{}{})", self.evaluate_to_css(name, QuoteKind::Quoted, self.empty_span)?, if is_custom_property { "" } else { " " }, self.evaluate_to_css(value, QuoteKind::Quoted, self.empty_span)?, ); self.flags .set(ContextFlags::IN_SUPPORTS_DECLARATION, old_in_supports_decl); Ok(result) } AstSupportsCondition::Function { name, args } => Ok(format!( "{}({})", self.perform_interpolation(name, false)?, self.perform_interpolation(args, false)? )), AstSupportsCondition::Anything { contents } => Ok(format!( "({})", self.perform_interpolation(contents, false)?, )), } } fn visit_supports_rule(&mut self, supports_rule: AstSupportsRule) -> SassResult<()> { if self.declaration_name.is_some() { return Err(( "Supports rules may not be used within nested declarations.", supports_rule.span, ) .into()); } let condition = self.visit_supports_condition(supports_rule.condition)?; let css_supports_rule = CssStmt::Supports( SupportsRule { params: condition, body: Vec::new(), }, false, ); let children = supports_rule.body; self.with_parent( css_supports_rule, true, |visitor| { if !visitor.style_rule_exists() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the supports rule so that // declarations immediately inside @supports have somewhere to go. // // For example, "a {@supports (a: b) {b: c}}" should produce "@supports // (a: b) {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let ruleset = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( ruleset, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, CssStmt::is_style_rule, )?; Ok(()) } fn execute( &mut self, stylesheet: StyleSheet, configuration: Option>>, // todo: different errors based on this _names_in_errors: bool, ) -> SassResult>> { let url = stylesheet.url.clone(); // todo: use canonical url for modules if let Some(already_loaded) = self.modules.get(&stylesheet.url) { let current_configuration = configuration.unwrap_or_else(|| Rc::clone(&self.configuration)); if !current_configuration.borrow().is_implicit() { // if (!_moduleConfigurations[url]!.sameOriginal(currentConfiguration) && // currentConfiguration is ExplicitConfiguration) { // var message = namesInErrors // ? "${p.prettyUri(url)} was already loaded, so it can't be " // "configured using \"with\"." // : "This module was already loaded, so it can't be configured using " // "\"with\"."; // var existingSpan = _moduleNodes[url]?.span; // var configurationSpan = configuration == null // ? currentConfiguration.nodeWithSpan.span // : null; // var secondarySpans = { // if (existingSpan != null) existingSpan: "original load", // if (configurationSpan != null) configurationSpan: "configuration" // }; // throw secondarySpans.isEmpty // ? _exception(message) // : _multiSpanException(message, "new load", secondarySpans); // } } return Ok(Arc::clone(already_loaded)); } let env = Environment::new(); let mut extension_store = ExtensionStore::new(self.empty_span); self.with_environment::, _>(env.new_closure(), |visitor| { let old_parent = visitor.parent; mem::swap(&mut visitor.extender, &mut extension_store); let old_style_rule = visitor.style_rule_ignoring_at_root.take(); let old_media_queries = visitor.media_queries.take(); let old_declaration_name = visitor.declaration_name.take(); let old_in_unknown_at_rule = visitor.flags.in_unknown_at_rule(); let old_at_root_excluding_style_rule = visitor.flags.at_root_excluding_style_rule(); let old_in_keyframes = visitor.flags.in_keyframes(); let old_configuration = if let Some(new_config) = configuration { Some(mem::replace(&mut visitor.configuration, new_config)) } else { None }; visitor.parent = None; visitor.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, false); visitor .flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); visitor.flags.set(ContextFlags::IN_KEYFRAMES, false); visitor.visit_stylesheet(stylesheet)?; // visitor.importer = old_importer; // visitor.stylesheet = old_stylesheet; // visitor.root = old_root; visitor.parent = old_parent; // visitor.end_of_imports = old_end_of_imports; // visitor.out_of_order_imports = old_out_of_order_imports; mem::swap(&mut visitor.extender, &mut extension_store); visitor.style_rule_ignoring_at_root = old_style_rule; visitor.media_queries = old_media_queries; visitor.declaration_name = old_declaration_name; visitor .flags .set(ContextFlags::IN_UNKNOWN_AT_RULE, old_in_unknown_at_rule); visitor.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); visitor .flags .set(ContextFlags::IN_KEYFRAMES, old_in_keyframes); if let Some(old_config) = old_configuration { visitor.configuration = old_config; } Ok(()) })?; let module = env.to_module(extension_store); self.modules.insert(url, Arc::clone(&module)); Ok(module) } pub(crate) fn load_module( &mut self, url: &Path, configuration: Option>>, names_in_errors: bool, span: Span, callback: impl Fn(&mut Self, Arc>, StyleSheet) -> SassResult<()>, ) -> SassResult<()> { let builtin = match url.to_string_lossy().as_ref() { "sass:color" => Some(declare_module_color()), "sass:list" => Some(declare_module_list()), "sass:map" => Some(declare_module_map()), "sass:math" => Some(declare_module_math()), "sass:meta" => Some(declare_module_meta()), "sass:selector" => Some(declare_module_selector()), "sass:string" => Some(declare_module_string()), _ => None, }; if let Some(builtin) = builtin { // todo: lots of ugly unwraps here if configuration.is_some() && !(**configuration.as_ref().unwrap()).borrow().is_implicit() { let msg = if names_in_errors { format!( "Built-in module {} can't be configured.", url.to_string_lossy() ) } else { "Built-in modules can't be configured.".to_owned() }; return Err(( msg, (**configuration.as_ref().unwrap()).borrow().span.unwrap(), ) .into()); } callback( self, Arc::new(RefCell::new(builtin)), StyleSheet::new(false, url.to_path_buf()), )?; return Ok(()); } // todo: decide on naming convention for style_sheet vs stylesheet let stylesheet = self.load_style_sheet(url.to_string_lossy().as_ref(), false, span)?; let canonical_url = self .options .fs .canonicalize(&stylesheet.url) .unwrap_or_else(|_| stylesheet.url.clone()); if self.active_modules.contains(&canonical_url) { return Err(("Module loop: this module is already being loaded.", span).into()); } self.active_modules.insert(canonical_url.clone()); let module = self.execute(stylesheet.clone(), configuration, names_in_errors)?; self.active_modules.remove(&canonical_url); callback(self, module, stylesheet)?; Ok(()) } fn visit_use_rule(&mut self, use_rule: AstUseRule) -> SassResult<()> { let configuration = if use_rule.configuration.is_empty() { Rc::new(RefCell::new(Configuration::empty())) } else { let mut values = BTreeMap::new(); for var in use_rule.configuration { let value = self.visit_expr(var.expr.node)?; let value = self.without_slash(value); values.insert( var.name.node, ConfiguredValue::explicit(value, var.name.span.merge(var.expr.span)), ); } Rc::new(RefCell::new(Configuration::explicit(values, use_rule.span))) }; let span = use_rule.span; let namespace = use_rule .namespace .as_ref() .map(|s| Identifier::from(s.trim_start_matches("sass:"))); self.load_module( &use_rule.url, Some(Rc::clone(&configuration)), false, span, |visitor, module, _| { visitor.env.add_module(namespace, module, span)?; Ok(()) }, )?; Self::assert_configuration_is_empty(&configuration, false)?; Ok(()) } pub(crate) fn assert_configuration_is_empty( config: &Rc>, name_in_error: bool, ) -> SassResult<()> { let config = (**config).borrow(); // By definition, implicit configurations are allowed to only use a subset // of their values. if config.is_empty() || config.is_implicit() { return Ok(()); } let Spanned { node: name, span } = config.first().unwrap(); let msg = if name_in_error { format!( "${name} was not declared with !default in the @used module.", name = name ) } else { "This variable was not declared with !default in the @used module.".to_owned() }; Err((msg, span).into()) } fn visit_import_rule(&mut self, import_rule: AstImportRule) -> SassResult> { for import in import_rule.imports { match import { AstImport::Sass(dynamic_import) => { self.visit_dynamic_import_rule(&dynamic_import)?; } AstImport::Plain(static_import) => self.visit_static_import_rule(static_import)?, } } Ok(None) } /// Searches the current directory of the file then searches in `load_paths` directories /// if the import has not yet been found. /// /// /// #[allow(clippy::cognitive_complexity, clippy::redundant_clone)] pub fn find_import(&self, path: &Path) -> Option { let path_buf = if path.is_absolute() { path.into() } else { self.current_import_path .parent() .unwrap_or_else(|| Path::new("")) .join(path) }; macro_rules! try_path { ($path:expr) => { let path = $path; let dirname = path.parent().unwrap_or_else(|| Path::new("")); let basename = path.file_name().unwrap_or_else(|| OsStr::new("..")); let partial = dirname.join(format!("_{}", basename.to_str().unwrap())); if self.options.fs.is_file(&path) { return Some(path.to_path_buf()); } if self.options.fs.is_file(&partial) { return Some(partial); } }; } if path_buf.extension() == Some(OsStr::new("scss")) || path_buf.extension() == Some(OsStr::new("sass")) || path_buf.extension() == Some(OsStr::new("css")) { let extension = path_buf.extension().unwrap(); try_path!(path_buf.with_extension(format!(".import{}", extension.to_str().unwrap()))); try_path!(path_buf); // todo: consider load paths return None; } macro_rules! try_path_with_extensions { ($path:expr) => { let path = $path; try_path!(path.with_extension("import.sass")); try_path!(path.with_extension("import.scss")); try_path!(path.with_extension("import.css")); try_path!(path.with_extension("sass")); try_path!(path.with_extension("scss")); try_path!(path.with_extension("css")); }; } try_path_with_extensions!(path_buf.clone()); if self.options.fs.is_dir(&path_buf) { try_path_with_extensions!(path_buf.join("index")); } for load_path in &self.options.load_paths { let path_buf = load_path.join(path); try_path_with_extensions!(&path_buf); if self.options.fs.is_dir(&path_buf) { try_path_with_extensions!(path_buf.join("index")); } } None } fn parse_file( &mut self, lexer: Lexer, path: &Path, empty_span: Span, ) -> SassResult { match InputSyntax::for_path(path) { InputSyntax::Scss => ScssParser::new(lexer, self.options, empty_span, path).__parse(), InputSyntax::Sass => SassParser::new(lexer, self.options, empty_span, path).__parse(), InputSyntax::Css => CssParser::new(lexer, self.options, empty_span, path).__parse(), } } fn import_like_node( &mut self, url: &str, _for_import: bool, span: Span, ) -> SassResult { if let Some(name) = self.find_import(url.as_ref()) { let name = self.options.fs.canonicalize(&name).unwrap_or(name); if let Some(style_sheet) = self.import_cache.get(&name) { return Ok(style_sheet.clone()); } let file = self.map.add_file( name.to_string_lossy().into(), String::from_utf8(self.options.fs.read(&name)?)?, ); let old_is_use_allowed = self.flags.is_use_allowed(); self.flags.set(ContextFlags::IS_USE_ALLOWED, true); let style_sheet = self.parse_file(Lexer::new_from_file(&file), &name, file.span.subspan(0, 0))?; self.flags .set(ContextFlags::IS_USE_ALLOWED, old_is_use_allowed); if self.files_seen.contains(&name) { self.import_cache.insert(name, style_sheet.clone()); } else { self.files_seen.insert(name); } return Ok(style_sheet); } Err(("Can't find stylesheet to import.", span).into()) } pub(crate) fn load_style_sheet( &mut self, url: &str, // default=false for_import: bool, span: Span, ) -> SassResult { // todo: import cache self.import_like_node(url, for_import, span) } fn visit_dynamic_import_rule(&mut self, dynamic_import: &AstSassImport) -> SassResult<()> { let stylesheet = self.load_style_sheet(&dynamic_import.url, true, dynamic_import.span)?; let url = stylesheet.url.clone(); if self.active_modules.contains(&url) { return Err(("This file is already being loaded.", dynamic_import.span).into()); } self.active_modules.insert(url.clone()); // If the imported stylesheet doesn't use any modules, we can inject its // CSS directly into the current stylesheet. If it does use modules, we // need to put its CSS into an intermediate [ModifiableCssStylesheet] so // that we can hermetically resolve `@extend`s before injecting it. if stylesheet.uses.is_empty() && stylesheet.forwards.is_empty() { self.visit_stylesheet(stylesheet)?; return Ok(()); } // todo: let loads_user_defined_modules = true; // this todo should be unreachable, as we currently do not push // to stylesheet.uses or stylesheet.forwards // let mut children = Vec::new(); let env = self.env.for_import(); self.with_environment::, _>(env.clone(), |visitor| { let old_parent = visitor.parent; let old_configuration = Rc::clone(&visitor.configuration); if loads_user_defined_modules { visitor.parent = Some(CssTree::ROOT); } // This configuration is only used if it passes through a `@forward` // rule, so we avoid creating unnecessary ones for performance reasons. if !stylesheet.forwards.is_empty() { visitor.configuration = Rc::new(RefCell::new(env.to_implicit_configuration())); } visitor.visit_stylesheet(stylesheet)?; if loads_user_defined_modules { visitor.parent = old_parent; } visitor.configuration = old_configuration; Ok(()) })?; // Create a dummy module with empty CSS and no extensions to make forwarded // members available in the current import context and to combine all the // CSS from modules used by [stylesheet]. let module = env.to_dummy_module(self.empty_span); self.env.import_forwards(module); if loads_user_defined_modules { // todo: // if (module.transitivelyContainsCss) { // // If any transitively used module contains extensions, we need to // // clone all modules' CSS. Otherwise, it's possible that they'll be // // used or imported from another location that shouldn't have the same // // extensions applied. // await _combineCss(module, // clone: module.transitivelyContainsExtensions) // .accept(this); // } // var visitor = _ImportedCssVisitor(this); // for (var child in children) { // child.accept(visitor); // } } self.active_modules.remove(&url); Ok(()) } fn visit_static_import_rule(&mut self, static_import: AstPlainCssImport) -> SassResult<()> { let import = self.interpolation_to_value(static_import.url, false, false)?; let modifiers = static_import .modifiers .map(|modifiers| self.interpolation_to_value(modifiers, false, false)) .transpose()?; let node = CssStmt::Import(import, modifiers); if self.parent.is_some() && self.parent != Some(CssTree::ROOT) { self.css_tree.add_stmt(node, self.parent); } else { self.import_nodes.push(node); } Ok(()) } fn visit_debug_rule(&mut self, debug_rule: AstDebugRule) -> SassResult> { if self.options.quiet { return Ok(None); } let message = self.visit_expr(debug_rule.value)?; let message = message.inspect(debug_rule.span)?; let loc = self.map.look_up_span(debug_rule.span); self.options.logger.debug(loc, message.as_str()); Ok(None) } fn visit_content_rule(&mut self, content_rule: AstContentRule) -> SassResult> { let span = content_rule.args.span; if let Some(content) = &self.env.content { #[allow(mutable_borrow_reservation_conflict)] self.run_user_defined_callable( MaybeEvaledArguments::Invocation(content_rule.args), Arc::clone(content), &content.env.clone(), span, |content, visitor| { for stmt in content.content.body.clone() { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, )?; } Ok(None) } fn trim_included(&self, nodes: &[CssTreeIdx]) -> CssTreeIdx { if nodes.is_empty() { return CssTree::ROOT; } let mut parent = self.parent; let mut innermost_contiguous: Option = None; for i in 0..nodes.len() { while parent != nodes.get(i).copied() { innermost_contiguous = None; let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); if grandparent.is_none() { unreachable!( "Expected {:?} to be an ancestor of {:?}.", nodes[i], grandparent ) } parent = grandparent; } innermost_contiguous = innermost_contiguous.or(Some(i)); let grandparent = self.css_tree.child_to_parent.get(&parent.unwrap()).copied(); if grandparent.is_none() { unreachable!( "Expected {:?} to be an ancestor of {:?}.", nodes[i], grandparent ) } parent = grandparent; } if parent != Some(CssTree::ROOT) { return CssTree::ROOT; } nodes[innermost_contiguous.unwrap()] } fn visit_at_root_rule(&mut self, mut at_root_rule: AstAtRootRule) -> SassResult> { let query = match at_root_rule.query.clone() { Some(query) => { let resolved = self.perform_interpolation(query.node, true)?; let span = query.span; let query_toks = Lexer::new_from_string(&resolved, span); AtRootQueryParser::new(query_toks).parse()? } None => AtRootQuery::default(), }; let mut current_parent_idx = self.parent; let mut included = Vec::new(); while let Some(parent_idx) = current_parent_idx { let parent = self.css_tree.get(parent_idx); let grandparent_idx = match &*parent { Some(parent) => { if !query.excludes(parent) { included.push(parent_idx); } self.css_tree.child_to_parent.get(&parent_idx).copied() } None => break, }; current_parent_idx = grandparent_idx; } let root = self.trim_included(&included); // If we didn't exclude any rules, we don't need to use the copies we might // have created. if Some(root) == self.parent { self.with_scope::, _>(false, true, |visitor| { for stmt in at_root_rule.body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; return Ok(None); } let inner_copy = if !included.is_empty() { let inner_copy = self .css_tree .get(*included.first().unwrap()) .as_ref() .map(CssStmt::copy_without_children); let mut outer_copy = self.css_tree.add_stmt(inner_copy.unwrap(), None); for node in &included[1..] { let copy = self .css_tree .get(*node) .as_ref() .map(CssStmt::copy_without_children) .unwrap(); let copy_idx = self.css_tree.add_stmt(copy, None); self.css_tree.link_child_to_parent(outer_copy, copy_idx); outer_copy = copy_idx; } Some(outer_copy) } else { let inner_copy = self .css_tree .get(root) .as_ref() .map(CssStmt::copy_without_children); inner_copy.map(|p| self.css_tree.add_stmt(p, None)) }; let body = mem::take(&mut at_root_rule.body); self.with_scope_for_at_root::, _>(inner_copy, &query, |visitor| { for stmt in body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; Ok(None) } fn with_scope_for_at_root T>( &mut self, new_parent_idx: Option, query: &AtRootQuery, callback: F, ) -> T { let old_parent = self.parent; self.parent = new_parent_idx; let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); if query.excludes_style_rules() { self.flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, true); } let old_media_query_info = if self.media_queries.is_some() && query.excludes_name("media") { Some((self.media_queries.take(), self.media_query_sources.take())) } else { None }; let was_in_keyframes = if self.flags.in_keyframes() && query.excludes_name("keyframes") { let was = self.flags.in_keyframes(); self.flags.set(ContextFlags::IN_KEYFRAMES, false); was } else { self.flags.in_keyframes() }; // todo: // if self.flags.in_unknown_at_rule() && !included.iter().any(|parent| parent is CssAtRule) let res = self.with_scope(false, true, callback); self.parent = old_parent; self.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); if let Some((old_media_queries, old_media_query_sources)) = old_media_query_info { self.media_queries = old_media_queries; self.media_query_sources = old_media_query_sources; } self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); res } fn visit_function_decl(&mut self, fn_decl: AstFunctionDecl) { let name = fn_decl.name.node; // todo: independency let func = SassFunction::UserDefined(UserDefinedFunction { function: Arc::new(fn_decl), name, env: self.env.new_closure(), }); self.env.insert_fn(func); } pub(crate) fn parse_selector_from_string( &mut self, selector_text: &str, allows_parent: bool, allows_placeholder: bool, span: Span, ) -> SassResult { let sel_toks = Lexer::new_from_string(selector_text, span); SelectorParser::new(sel_toks, allows_parent, allows_placeholder, span).parse() } fn visit_extend_rule(&mut self, extend_rule: AstExtendRule) -> SassResult> { if !self.style_rule_exists() || self.declaration_name.is_some() { return Err(( "@extend may only be used within style rules.", extend_rule.span, ) .into()); } let super_selector = self.style_rule_ignoring_at_root.clone().unwrap(); let target_text = self.interpolation_to_value(extend_rule.value, false, true)?; let list = self.parse_selector_from_string(&target_text, false, true, extend_rule.span)?; for complex in list.components { if complex.components.len() != 1 || !complex.components.first().unwrap().is_compound() { // If the selector was a compound selector but not a simple // selector, emit a more explicit error. return Err(("complex selectors may not be extended.", extend_rule.span).into()); } let compound = match complex.components.first() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!("checked by above condition"), }; if compound.components.len() != 1 { return Err(( format!( "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", compound.components.iter().map(ToString::to_string).collect::>().join(", ") ) , extend_rule.span).into()); } self.extender.add_extension( super_selector.clone().into_selector().0, compound.components.first().unwrap(), &ExtendRule { is_optional: extend_rule.is_optional, }, &self.media_queries, extend_rule.span, ); } Ok(None) } fn visit_error_rule(&mut self, error_rule: AstErrorRule) -> SassResult> { let value = self .visit_expr(error_rule.value)? .inspect(error_rule.span)?; Ok((value, error_rule.span).into()) } fn merge_media_queries( queries1: &[MediaQuery], queries2: &[MediaQuery], ) -> Option> { let mut queries = Vec::new(); for query1 in queries1 { for query2 in queries2 { match query1.merge(query2) { MediaQueryMergeResult::Empty => continue, MediaQueryMergeResult::Unrepresentable => return None, MediaQueryMergeResult::Success(result) => queries.push(result), } } } Some(queries) } fn visit_media_queries( &mut self, queries: Interpolation, span: Span, ) -> SassResult> { let resolved = self.perform_interpolation(queries, true)?; CssMediaQuery::parse_list(&resolved, span) } fn visit_media_rule(&mut self, media_rule: AstMedia) -> SassResult> { if self.declaration_name.is_some() { return Err(( "Media rules may not be used within nested declarations.", media_rule.span, ) .into()); } let queries1 = self.visit_media_queries(media_rule.query, media_rule.query_span)?; // todo: superfluous clone? let queries2 = self.media_queries.clone(); let merged_queries = queries2 .as_ref() .and_then(|queries2| Self::merge_media_queries(queries2, &queries1)); let merged_sources = match &merged_queries { Some(merged_queries) if merged_queries.is_empty() => return Ok(None), Some(..) => { let mut set = IndexSet::new(); set.extend(self.media_query_sources.clone().unwrap()); set.extend(self.media_queries.clone().unwrap()); set.extend(queries1.clone()); set } None => IndexSet::new(), }; let children = media_rule.body; let query = merged_queries.clone().unwrap_or_else(|| queries1.clone()); let media_rule = CssStmt::Media( MediaRule { query, body: Vec::new(), }, false, ); self.with_parent( media_rule, true, |visitor| { visitor.with_media_queries( Some(merged_queries.unwrap_or(queries1)), Some(merged_sources.clone()), |visitor| { if !visitor.style_rule_exists() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the media query so that // declarations immediately inside @media have somewhere to go. // // For example, "a {@media screen {b: c}}" should produce // "@media screen {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let ruleset = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( ruleset, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, ) }, |stmt| match stmt { CssStmt::RuleSet { .. } => true, // todo: node.queries.every(mergedSources.contains)) CssStmt::Media(media_rule, ..) => { !merged_sources.is_empty() && media_rule .query .iter() .all(|query| merged_sources.contains(query)) } _ => false, }, )?; Ok(None) } fn visit_unknown_at_rule( &mut self, unknown_at_rule: AstUnknownAtRule, ) -> SassResult> { if self.declaration_name.is_some() { return Err(( "At-rules may not be used within nested declarations.", unknown_at_rule.span, ) .into()); } let name = self.interpolation_to_value(unknown_at_rule.name, false, false)?; let value = unknown_at_rule .value .map(|v| self.interpolation_to_value(v, true, true)) .transpose()?; if unknown_at_rule.body.is_none() { let stmt = CssStmt::UnknownAtRule( UnknownAtRule { name, params: value.unwrap_or_default(), body: Vec::new(), has_body: false, }, false, ); self.css_tree.add_stmt(stmt, self.parent); return Ok(None); } let was_in_keyframes = self.flags.in_keyframes(); let was_in_unknown_at_rule = self.flags.in_unknown_at_rule(); if unvendor(&name) == "keyframes" { self.flags.set(ContextFlags::IN_KEYFRAMES, true); } else { self.flags.set(ContextFlags::IN_UNKNOWN_AT_RULE, true); } let children = unknown_at_rule.body.unwrap(); let stmt = CssStmt::UnknownAtRule( UnknownAtRule { name, params: value.unwrap_or_default(), body: Vec::new(), has_body: true, }, false, ); self.with_parent( stmt, true, |visitor| { if !visitor.style_rule_exists() || visitor.flags.in_keyframes() { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } } else { // If we're in a style rule, copy it into the at-rule so that // declarations immediately inside it have somewhere to go. // // For example, "a {@foo {b: c}}" should produce "@foo {a {b: c}}". let selector = visitor.style_rule_ignoring_at_root.clone().unwrap(); let style_rule = CssStmt::RuleSet { selector, body: Vec::new(), is_group_end: false, }; visitor.with_parent( style_rule, false, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, |_| false, )?; } Ok(()) }, CssStmt::is_style_rule, )?; self.flags.set(ContextFlags::IN_KEYFRAMES, was_in_keyframes); self.flags .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); Ok(None) } pub(crate) fn emit_warning(&mut self, message: &str, span: Span) { if self.options.quiet { return; } let loc = self.map.look_up_span(span); self.options.logger.warn(loc, message); } fn visit_warn_rule(&mut self, warn_rule: AstWarn) -> SassResult<()> { if self.warnings_emitted.insert(warn_rule.span) { let value = self.visit_expr(warn_rule.value)?; let message = value.to_css_string(warn_rule.span, self.options.is_compressed())?; self.emit_warning(&message, warn_rule.span); } Ok(()) } fn with_media_queries( &mut self, queries: Option>, sources: Option>, callback: impl FnOnce(&mut Self) -> T, ) -> T { let old_media_queries = self.media_queries.take(); let old_media_query_sources = self.media_query_sources.take(); self.media_queries = queries; self.media_query_sources = sources; let result = callback(self); self.media_queries = old_media_queries; self.media_query_sources = old_media_query_sources; result } fn with_environment T>( &mut self, env: Environment, callback: F, ) -> T { let mut old_env = env; mem::swap(&mut self.env, &mut old_env); let val = callback(self); mem::swap(&mut self.env, &mut old_env); val } fn add_child bool>( &mut self, node: CssStmt, through: Option, ) -> CssTreeIdx { if self.parent.is_none() || self.parent == Some(CssTree::ROOT) { return self.css_tree.add_stmt(node, self.parent); } let mut parent = self.parent.unwrap(); if let Some(through) = through { while parent != CssTree::ROOT && through(self.css_tree.get(parent).as_ref().unwrap()) { let grandparent = self.css_tree.child_to_parent.get(&parent).copied(); debug_assert!( grandparent.is_some(), "through() must return false for at least one parent of $node." ); parent = grandparent.unwrap(); } // If the parent has a (visible) following sibling, we shouldn't add to // the parent. Instead, we should create a copy and add it after the // interstitial sibling. if self.css_tree.has_following_sibling(parent) { let grandparent = self.css_tree.child_to_parent.get(&parent).copied().unwrap(); let parent_node = self .css_tree .get(parent) .as_ref() .map(CssStmt::copy_without_children) .unwrap(); parent = self.css_tree.add_child(parent_node, grandparent); } } self.css_tree.add_child(node, parent) } fn with_parent SassResult<()>, FT: Fn(&CssStmt) -> bool>( &mut self, parent: CssStmt, // default=true scope_when: bool, callback: F, // todo: optional through: FT, ) -> SassResult<()> { let parent_idx = self.add_child(parent, Some(through)); let old_parent = self.parent; self.parent = Some(parent_idx); let result = self.with_scope(false, scope_when, callback); self.parent = old_parent; result } fn with_scope T>( &mut self, // default=false semi_global: bool, // default=true when: bool, callback: F, ) -> T { let semi_global = semi_global && self.flags.in_semi_global_scope(); let was_in_semi_global_scope = self.flags.in_semi_global_scope(); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, semi_global); if !when { let v = callback(self); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); return v; } self.env.scopes_mut().enter_new_scope(); let v = callback(self); self.flags .set(ContextFlags::IN_SEMI_GLOBAL_SCOPE, was_in_semi_global_scope); self.env.scopes_mut().exit_scope(); v } fn with_content( &mut self, content: Option>, callback: impl FnOnce(&mut Self) -> T, ) -> T { let old_content = self.env.content.take(); self.env.content = content; let v = callback(self); self.env.content = old_content; v } fn visit_include_stmt(&mut self, include_stmt: AstInclude) -> SassResult> { let mixin = self .env .get_mixin(include_stmt.name, include_stmt.namespace)?; match mixin { Mixin::Builtin(mixin) => { if include_stmt.content.is_some() { return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); } let args = self.eval_args(include_stmt.args, include_stmt.name.span)?; mixin(args, self)?; Ok(None) } Mixin::UserDefined(mixin, env) => { if include_stmt.content.is_some() && !mixin.has_content { return Err(("Mixin doesn't accept a content block.", include_stmt.span).into()); } let AstInclude { args, content, .. } = include_stmt; let old_in_mixin = self.flags.in_mixin(); self.flags.set(ContextFlags::IN_MIXIN, true); let callable_content = content.map(|c| { Arc::new(CallableContentBlock { content: c, env: self.env.new_closure(), }) }); self.run_user_defined_callable::<_, (), _>( MaybeEvaledArguments::Invocation(args), mixin, &env, include_stmt.name.span, |mixin, visitor| { visitor.with_content(callable_content, |visitor| { for stmt in mixin.body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }) }, )?; self.flags.set(ContextFlags::IN_MIXIN, old_in_mixin); Ok(None) } } } fn visit_mixin_decl(&mut self, mixin: AstMixin) { self.env.insert_mixin( mixin.name, Mixin::UserDefined(mixin, self.env.new_closure()), ); } fn visit_each_stmt(&mut self, each_stmt: AstEach) -> SassResult> { let list = self.visit_expr(each_stmt.list)?.as_list(); // todo: not setting semi_global: true maybe means we can't assign to global scope when declared as global self.env.scopes_mut().enter_new_scope(); let mut result = None; 'outer: for val in list { if each_stmt.variables.len() == 1 { let val = self.without_slash(val); self.env .scopes_mut() .insert_var_last(each_stmt.variables[0], val); } else { for (&var, val) in each_stmt.variables.iter().zip( val.as_list() .into_iter() .chain(std::iter::once(Value::Null).cycle()), ) { let val = self.without_slash(val); self.env.scopes_mut().insert_var_last(var, val); } } for stmt in each_stmt.body.clone() { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_for_stmt(&mut self, for_stmt: AstFor) -> SassResult> { let from_span = for_stmt.from.span; let to_span = for_stmt.to.span; let from_number = self .visit_expr(for_stmt.from.node)? .assert_number(from_span)?; let to_number = self.visit_expr(for_stmt.to.node)?.assert_number(to_span)?; if !to_number.unit().comparable(from_number.unit()) { // todo: better error message here return Err(( "to and from values have incompatible units", from_span.merge(to_span), ) .into()); } let from = from_number.num.assert_int(from_span)?; let mut to = to_number .num .convert(to_number.unit(), from_number.unit()) .assert_int(to_span)?; let direction = if from > to { -1 } else { 1 }; if to == i64::MAX || to == i64::MIN { return Err(( "@for loop upper bound exceeds valid integer representation (i64::MAX)", to_span, ) .into()); } if !for_stmt.is_exclusive { to += direction; } if from == to { return Ok(None); } // todo: self.with_scopes self.env.scopes_mut().enter_new_scope(); let mut result = None; let mut i = from; 'outer: while i != to { self.env.scopes_mut().insert_var_last( for_stmt.variable.node, Value::Dimension(SassNumber { num: Number::from(i), unit: from_number.unit().clone(), as_slash: None, }), ); for stmt in for_stmt.body.clone() { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } i += direction; } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_while_stmt(&mut self, while_stmt: &AstWhile) -> SassResult> { self.with_scope(true, true, |visitor| { let mut result = None; 'outer: while visitor .visit_expr(while_stmt.condition.clone())? .is_truthy() { for stmt in while_stmt.body.clone() { let val = visitor.visit_stmt(stmt)?; if val.is_some() { result = val; break 'outer; } } } Ok(result) }) } fn visit_if_stmt(&mut self, if_stmt: AstIf) -> SassResult> { let mut clause: Option> = if_stmt.else_clause; for clause_to_check in if_stmt.if_clauses { if self.visit_expr(clause_to_check.condition)?.is_truthy() { clause = Some(clause_to_check.body); break; } } // todo: self.with_scope self.env.scopes_mut().enter_new_scope(); let mut result = None; if let Some(stmts) = clause { for stmt in stmts { let val = self.visit_stmt(stmt)?; if val.is_some() { result = val; break; } } } self.env.scopes_mut().exit_scope(); Ok(result) } fn visit_loud_comment(&mut self, comment: AstLoudComment) -> SassResult> { if self.flags.in_function() { return Ok(None); } // todo: Comments are allowed to appear between CSS imports // if (_parent == _root && _endOfImports == _root.children.length) { // _endOfImports++; // } let comment = CssStmt::Comment( self.perform_interpolation(comment.text, false)?, comment.span, ); self.css_tree.add_stmt(comment, self.parent); Ok(None) } fn visit_variable_decl(&mut self, decl: AstVariableDecl) -> SassResult> { let name = Spanned { node: decl.name, span: decl.span, }; if decl.is_guarded { if decl.namespace.is_none() && self.env.at_root() { let var_override = (*self.configuration).borrow_mut().remove(decl.name); if !matches!( var_override, Some(ConfiguredValue { value: Value::Null, .. }) | None ) { self.env.insert_var( name, None, var_override.unwrap().value, true, self.flags.in_semi_global_scope(), )?; return Ok(None); } } if self.env.var_exists(decl.name, decl.namespace)? { let value = self.env.get_var(name, decl.namespace).unwrap(); if value != Value::Null { return Ok(None); } } } let value = self.visit_expr(decl.value)?; let value = self.without_slash(value); self.env.insert_var( name, decl.namespace, value, decl.is_global, self.flags.in_semi_global_scope(), )?; Ok(None) } fn interpolation_to_value( &mut self, interpolation: Interpolation, // default=false trim: bool, // default=false warn_for_color: bool, ) -> SassResult { let result = self.perform_interpolation(interpolation, warn_for_color)?; Ok(if trim { trim_ascii(&result, true).to_owned() } else { result }) } fn perform_interpolation( &mut self, mut interpolation: Interpolation, // todo check to emit warning if this is true _warn_for_color: bool, ) -> SassResult { let result = match interpolation.contents.len() { 0 => String::new(), 1 => match interpolation.contents.pop() { Some(InterpolationPart::String(s)) => s, Some(InterpolationPart::Expr(e)) => { let span = e.span; let result = self.visit_expr(e.node)?; // todo: span for specific expr self.serialize(result, QuoteKind::None, span)? } None => unreachable!(), }, _ => interpolation .contents .into_iter() .map(|part| match part { InterpolationPart::String(s) => Ok(s), InterpolationPart::Expr(e) => { let span = e.span; let result = self.visit_expr(e.node)?; // todo: span for specific expr self.serialize(result, QuoteKind::None, span) } }) .collect::>()?, }; Ok(result) } fn evaluate_to_css( &mut self, expr: AstExpr, quote: QuoteKind, span: Span, ) -> SassResult { let result = self.visit_expr(expr)?; self.serialize(result, quote, span) } #[allow(clippy::unused_self)] fn without_slash(&mut self, v: Value) -> Value { match v { Value::Dimension(SassNumber { .. }) if v.as_slash().is_some() => { // todo: emit warning. we don't currently because it can be quite loud // self.emit_warning( // Cow::Borrowed("Using / for division is deprecated and will be removed at some point in the future"), // self.empty_span, // ); } _ => {} } v.without_slash() } fn eval_maybe_args( &mut self, args: MaybeEvaledArguments, span: Span, ) -> SassResult { match args { MaybeEvaledArguments::Invocation(args) => self.eval_args(args, span), MaybeEvaledArguments::Evaled(args) => Ok(args), } } fn eval_args( &mut self, arguments: ArgumentInvocation, span: Span, ) -> SassResult { let mut positional = Vec::with_capacity(arguments.positional.len()); for expr in arguments.positional { let val = self.visit_expr(expr)?; positional.push(self.without_slash(val)); } let mut named = BTreeMap::new(); for (key, expr) in arguments.named { let val = self.visit_expr(expr)?; named.insert(key, self.without_slash(val)); } if arguments.rest.is_none() { return Ok(ArgumentResult { positional, named, separator: ListSeparator::Undecided, span, touched: BTreeSet::new(), }); } let rest = self.visit_expr(arguments.rest.unwrap())?; let mut separator = ListSeparator::Undecided; match rest { Value::Map(rest) => self.add_rest_map(&mut named, rest)?, Value::List(elems, list_separator, _) => { let mut list = elems .into_iter() .map(|e| self.without_slash(e)) .collect::>(); positional.append(&mut list); separator = list_separator; } Value::ArgList(arglist) => { // todo: superfluous clone for (&key, value) in arglist.keywords() { named.insert(key, self.without_slash(value.clone())); } let mut list = arglist .elems .into_iter() .map(|e| self.without_slash(e)) .collect::>(); positional.append(&mut list); separator = arglist.separator; } _ => { positional.push(self.without_slash(rest)); } } if arguments.keyword_rest.is_none() { return Ok(ArgumentResult { positional, named, separator: ListSeparator::Undecided, span: arguments.span, touched: BTreeSet::new(), }); } match self.visit_expr(arguments.keyword_rest.unwrap())? { Value::Map(keyword_rest) => { self.add_rest_map(&mut named, keyword_rest)?; Ok(ArgumentResult { positional, named, separator, span: arguments.span, touched: BTreeSet::new(), }) } v => Err(( format!( "Variable keyword arguments must be a map (was {}).", v.inspect(arguments.span)? ), arguments.span, ) .into()), } } fn add_rest_map( &mut self, named: &mut BTreeMap, rest: SassMap, ) -> SassResult<()> { for (key, val) in rest { match key.node { Value::String(text, ..) => { let val = self.without_slash(val); named.insert(Identifier::from(text), val); } _ => { return Err(( // todo: we have to render the map for this error message "Variable keyword argument map must have string keys.", key.span, ) .into()); } } } Ok(()) } fn run_user_defined_callable< F: UserDefinedCallable, V: fmt::Debug, R: FnOnce(F, &mut Self) -> SassResult, >( &mut self, arguments: MaybeEvaledArguments, func: F, env: &Environment, span: Span, run: R, ) -> SassResult { let mut evaluated = self.eval_maybe_args(arguments, span)?; let mut name = func.name().to_string(); if name != "@content" { name.push_str("()"); } self.with_environment(env.new_closure(), |visitor| { visitor.with_scope(false, true, move |visitor| { func.arguments().verify( evaluated.positional.len(), &evaluated.named, evaluated.span, )?; let declared_arguments = &func.arguments().args; let min_len = evaluated.positional.len().min(declared_arguments.len()); let positional_len = evaluated.positional.len(); #[allow(clippy::needless_range_loop)] for i in (0..min_len).rev() { visitor.env.scopes_mut().insert_var_last( declared_arguments[i].name, evaluated.positional.remove(i), ); } // todo: better name for var let additional_declared_args = if declared_arguments.len() > positional_len { &declared_arguments[positional_len..declared_arguments.len()] } else { &[] }; for argument in additional_declared_args { let name = argument.name; let value = evaluated.named.remove(&argument.name).map_or_else( || { // todo: superfluous clone let v = visitor.visit_expr(argument.default.clone().unwrap())?; Ok(visitor.without_slash(v)) }, SassResult::Ok, )?; visitor.env.scopes_mut().insert_var_last(name, value); } let were_keywords_accessed = Rc::new(Cell::new(false)); let num_named_args = evaluated.named.len(); let has_arg_list = if let Some(rest_arg) = func.arguments().rest { let rest = if !evaluated.positional.is_empty() { evaluated.positional } else { Vec::new() }; let arg_list = Value::ArgList(ArgList::new( rest, Rc::clone(&were_keywords_accessed), // todo: superfluous clone evaluated.named.clone(), if evaluated.separator == ListSeparator::Undecided { ListSeparator::Comma } else { ListSeparator::Space }, )); visitor.env.scopes_mut().insert_var_last(rest_arg, arg_list); true } else { false }; let val = run(func, visitor)?; if !has_arg_list || num_named_args == 0 { return Ok(val); } if (*were_keywords_accessed).get() { return Ok(val); } let argument_word = if num_named_args == 1 { "argument" } else { "arguments" }; let argument_names = to_sentence( evaluated .named .keys() .map(|key| format!("${key}", key = key)) .collect(), "or", ); Err(( format!( "No {argument_word} named {argument_names}.", argument_word = argument_word, argument_names = argument_names ), span, ) .into()) }) }) } pub(crate) fn run_function_callable( &mut self, func: SassFunction, arguments: ArgumentInvocation, span: Span, ) -> SassResult { self.run_function_callable_with_maybe_evaled( func, MaybeEvaledArguments::Invocation(arguments), span, ) } pub(crate) fn run_function_callable_with_maybe_evaled( &mut self, func: SassFunction, arguments: MaybeEvaledArguments, span: Span, ) -> SassResult { match func { SassFunction::Builtin(func, _name) => { let evaluated = self.eval_maybe_args(arguments, span)?; let val = func.0(evaluated, self)?; Ok(self.without_slash(val)) } SassFunction::UserDefined(UserDefinedFunction { function, env, .. }) => self .run_user_defined_callable(arguments, function, &env, span, |function, visitor| { for stmt in function.body.clone() { let result = visitor.visit_stmt(stmt)?; if let Some(val) = result { return Ok(val); } } Err(("Function finished without @return.", span).into()) }), SassFunction::Plain { name } => { let has_named; let mut rest = None; // todo: somewhat hacky solution to support plain css fns passed // as strings to `call(..)` let arguments = match arguments { MaybeEvaledArguments::Invocation(args) => { has_named = !args.named.is_empty() || args.keyword_rest.is_some(); rest = args.rest; args.positional .into_iter() .map(|arg| self.evaluate_to_css(arg, QuoteKind::Quoted, span)) .collect::>>()? } MaybeEvaledArguments::Evaled(args) => { has_named = !args.named.is_empty(); args.positional .into_iter() .map(|arg| arg.to_css_string(span, self.options.is_compressed())) .collect::>>()? } }; if has_named { return Err( ("Plain CSS functions don't support keyword arguments.", span).into(), ); } let mut buffer = format!("{}(", name.as_str()); let mut first = true; for argument in arguments { if first { first = false; } else { buffer.push_str(", "); } buffer.push_str(&argument); } if let Some(rest_arg) = rest { let rest = self.visit_expr(rest_arg)?; if !first { buffer.push_str(", "); } buffer.push_str(&self.serialize(rest, QuoteKind::Quoted, span)?); } buffer.push(')'); Ok(Value::String(buffer, QuoteKind::None)) } } } fn visit_list_expr(&mut self, list: ListExpr) -> SassResult { let elems = list .elems .into_iter() .map(|e| { let value = self.visit_expr(e.node)?; Ok(value) }) .collect::>>()?; Ok(Value::List(elems, list.separator, list.brackets)) } fn visit_function_call_expr(&mut self, func_call: FunctionCallExpr) -> SassResult { let name = func_call.name; let func = match self.env.get_fn(name, func_call.namespace)? { Some(func) => func, None => { if let Some(f) = self.options.custom_fns.get(name.as_str()) { SassFunction::Builtin(f.clone(), name) } else if let Some(f) = GLOBAL_FUNCTIONS.get(name.as_str()) { SassFunction::Builtin(f.clone(), name) } else { if func_call.namespace.is_some() { return Err(("Undefined function.", func_call.span).into()); } SassFunction::Plain { name } } } }; let old_in_function = self.flags.in_function(); self.flags.set(ContextFlags::IN_FUNCTION, true); let value = self.run_function_callable(func, (*func_call.arguments).clone(), func_call.span)?; self.flags.set(ContextFlags::IN_FUNCTION, old_in_function); Ok(value) } fn visit_interpolated_func_expr(&mut self, func: InterpolatedFunction) -> SassResult { let InterpolatedFunction { name, arguments: args, span, } = func; let fn_name = self.perform_interpolation(name, false)?; if !args.named.is_empty() || args.keyword_rest.is_some() { return Err(("Plain CSS functions don't support keyword arguments.", span).into()); } let mut buffer = format!("{}(", fn_name); let mut first = true; for arg in args.positional.clone() { if first { first = false; } else { buffer.push_str(", "); } let evaluated = self.evaluate_to_css(arg, QuoteKind::Quoted, span)?; buffer.push_str(&evaluated); } if let Some(rest_arg) = args.rest { let rest = self.visit_expr(rest_arg)?; if !first { buffer.push_str(", "); } buffer.push_str(&self.serialize(rest, QuoteKind::None, span)?); } buffer.push(')'); Ok(Value::String(buffer, QuoteKind::None)) } fn visit_parent_selector(&self) -> Value { match &self.style_rule_ignoring_at_root { Some(selector) => selector.as_selector_list().clone().to_sass_list(), None => Value::Null, } } fn visit_expr(&mut self, expr: AstExpr) -> SassResult { Ok(match expr { AstExpr::Color(color) => Value::Color(color), AstExpr::Number { n, unit } => Value::Dimension(SassNumber { num: n, unit, as_slash: None, }), AstExpr::List(list) => self.visit_list_expr(list)?, AstExpr::String(StringExpr(text, quote), ..) => self.visit_string(text, quote)?, AstExpr::BinaryOp(binop) => self.visit_bin_op( binop.lhs.clone(), binop.op, binop.rhs.clone(), binop.allows_slash, binop.span, )?, AstExpr::True => Value::True, AstExpr::False => Value::False, AstExpr::Calculation { name, args } => { self.visit_calculation_expr(name, args, self.empty_span)? } AstExpr::FunctionCall(func_call) => self.visit_function_call_expr(func_call)?, AstExpr::If(if_expr) => self.visit_ternary((*if_expr).clone())?, AstExpr::InterpolatedFunction(func) => { self.visit_interpolated_func_expr((*func).clone())? } AstExpr::Map(map) => self.visit_map(map)?, AstExpr::Null => Value::Null, AstExpr::Paren(expr) => self.visit_expr((*expr).clone())?, AstExpr::ParentSelector => self.visit_parent_selector(), AstExpr::UnaryOp(op, expr, span) => self.visit_unary_op(op, (*expr).clone(), span)?, AstExpr::Variable { name, namespace } => self.env.get_var(name, namespace)?, AstExpr::Supports(condition) => Value::String( self.visit_supports_condition((*condition).clone())?, QuoteKind::None, ), }) } fn visit_calculation_value( &mut self, expr: AstExpr, in_min_or_max: bool, span: Span, ) -> SassResult { Ok(match expr { AstExpr::Paren(inner) => match &*inner { AstExpr::FunctionCall(FunctionCallExpr { ref name, .. }) if name.as_str().to_ascii_lowercase() == "var" => { let result = self.visit_calculation_value((*inner).clone(), in_min_or_max, span)?; if let CalculationArg::String(text) = result { CalculationArg::String(format!("({})", text)) } else { result } } _ => self.visit_calculation_value((*inner).clone(), in_min_or_max, span)?, }, AstExpr::String(string_expr, _span) => { debug_assert!(string_expr.1 == QuoteKind::None); CalculationArg::Interpolation(self.perform_interpolation(string_expr.0, false)?) } AstExpr::BinaryOp(binop) => SassCalculation::operate_internal( binop.op, self.visit_calculation_value(binop.lhs.clone(), in_min_or_max, span)?, self.visit_calculation_value(binop.rhs.clone(), in_min_or_max, span)?, in_min_or_max, !self.flags.in_supports_declaration(), self.options, span, )?, AstExpr::Number { .. } | AstExpr::Calculation { .. } | AstExpr::Variable { .. } | AstExpr::FunctionCall { .. } | AstExpr::If(..) => { let result = self.visit_expr(expr)?; match result { Value::Dimension(SassNumber { num, unit, as_slash, }) => CalculationArg::Number(SassNumber { num, unit, as_slash, }), Value::Calculation(calc) => CalculationArg::Calculation(calc), Value::String(s, QuoteKind::None) => CalculationArg::String(s), value => { return Err(( format!( "Value {} can't be used in a calculation.", value.inspect(span)? ), span, ) .into()) } } } v => unreachable!("{:?}", v), }) } fn visit_calculation_expr( &mut self, name: CalculationName, args: Vec, span: Span, ) -> SassResult { let mut args = args .into_iter() .map(|arg| self.visit_calculation_value(arg, name.in_min_or_max(), span)) .collect::>>()?; if self.flags.in_supports_declaration() { return Ok(Value::Calculation(SassCalculation::unsimplified( name, args, ))); } match name { CalculationName::Calc => { debug_assert_eq!(args.len(), 1); Ok(SassCalculation::calc(args.remove(0))) } CalculationName::Min => SassCalculation::min(args, self.options, span), CalculationName::Max => SassCalculation::max(args, self.options, span), CalculationName::Clamp => { let min = args.remove(0); let value = if args.is_empty() { None } else { Some(args.remove(0)) }; let max = if args.is_empty() { None } else { Some(args.remove(0)) }; SassCalculation::clamp(min, value, max, self.options, span) } } } fn visit_unary_op(&mut self, op: UnaryOp, expr: AstExpr, span: Span) -> SassResult { let operand = self.visit_expr(expr)?; match op { UnaryOp::Plus => operand.unary_plus(self, span), UnaryOp::Neg => operand.unary_neg(self, span), UnaryOp::Div => operand.unary_div(self, span), UnaryOp::Not => Ok(operand.unary_not()), } } fn visit_ternary(&mut self, if_expr: Ternary) -> SassResult { if_arguments().verify(if_expr.0.positional.len(), &if_expr.0.named, if_expr.0.span)?; let mut positional = if_expr.0.positional; let mut named = if_expr.0.named; let condition = if positional.is_empty() { named.remove(&Identifier::from("condition")).unwrap() } else { positional.remove(0) }; let if_true = if positional.is_empty() { named.remove(&Identifier::from("if_true")).unwrap() } else { positional.remove(0) }; let if_false = if positional.is_empty() { named.remove(&Identifier::from("if_false")).unwrap() } else { positional.remove(0) }; let value = if self.visit_expr(condition)?.is_truthy() { self.visit_expr(if_true)? } else { self.visit_expr(if_false)? }; Ok(self.without_slash(value)) } fn visit_string(&mut self, mut text: Interpolation, quote: QuoteKind) -> SassResult { // Don't use [performInterpolation] here because we need to get the raw text // from strings, rather than the semantic value. let old_in_supports_declaration = self.flags.in_supports_declaration(); self.flags.set(ContextFlags::IN_SUPPORTS_DECLARATION, false); let result = match text.contents.len() { 0 => String::new(), 1 => match text.contents.pop() { Some(InterpolationPart::String(s)) => s, Some(InterpolationPart::Expr(Spanned { node, span })) => { match self.visit_expr(node)? { Value::String(s, ..) => s, e => self.serialize(e, QuoteKind::None, span)?, } } None => unreachable!(), }, _ => text .contents .into_iter() .map(|part| match part { InterpolationPart::String(s) => Ok(s), InterpolationPart::Expr(Spanned { node, span }) => { match self.visit_expr(node)? { Value::String(s, ..) => Ok(s), e => self.serialize(e, QuoteKind::None, span), } } }) .collect::>()?, }; self.flags.set( ContextFlags::IN_SUPPORTS_DECLARATION, old_in_supports_declaration, ); Ok(Value::String(result, quote)) } fn visit_map(&mut self, map: AstSassMap) -> SassResult { let mut sass_map = SassMap::new(); for pair in map.0 { let key_span = pair.0.span; let key = self.visit_expr(pair.0.node)?; let value = self.visit_expr(pair.1)?; if sass_map.get_ref(&key).is_some() { return Err(("Duplicate key.", key_span).into()); } sass_map.insert( Spanned { node: key, span: key_span, }, value, ); } Ok(Value::Map(sass_map)) } fn visit_bin_op( &mut self, lhs: AstExpr, op: BinaryOp, rhs: AstExpr, allows_slash: bool, span: Span, ) -> SassResult { let left = self.visit_expr(lhs)?; Ok(match op { BinaryOp::SingleEq => { let right = self.visit_expr(rhs)?; single_eq(&left, &right, self.options, span)? } BinaryOp::Or => { if left.is_truthy() { left } else { self.visit_expr(rhs)? } } BinaryOp::And => { if left.is_truthy() { self.visit_expr(rhs)? } else { left } } BinaryOp::Equal => { let right = self.visit_expr(rhs)?; Value::bool(left == right) } BinaryOp::NotEqual => { let right = self.visit_expr(rhs)?; Value::bool(left != right) } BinaryOp::GreaterThan | BinaryOp::GreaterThanEqual | BinaryOp::LessThan | BinaryOp::LessThanEqual => { let right = self.visit_expr(rhs)?; cmp(&left, &right, self.options, span, op)? } BinaryOp::Plus => { let right = self.visit_expr(rhs)?; add(left, right, self.options, span)? } BinaryOp::Minus => { let right = self.visit_expr(rhs)?; sub(left, right, self.options, span)? } BinaryOp::Mul => { let right = self.visit_expr(rhs)?; mul(left, right, self.options, span)? } BinaryOp::Div => { let right = self.visit_expr(rhs)?; let left_is_number = matches!(left, Value::Dimension { .. }); let right_is_number = matches!(right, Value::Dimension { .. }); if left_is_number && right_is_number && allows_slash { let result = div(left.clone(), right.clone(), self.options, span)?; return result.with_slash( left.assert_number(span)?, right.assert_number(span)?, span, ); } else if left_is_number && right_is_number { // todo: emit warning here. it prints too frequently, so we do not currently // self.emit_warning( // Cow::Borrowed(format!( // "Using / for division outside of calc() is deprecated" // )), // span, // ); } div(left, right, self.options, span)? } BinaryOp::Rem => { let right = self.visit_expr(rhs)?; rem(left, right, self.options, span)? } }) } // todo: superfluous taking `expr` by value fn serialize(&mut self, mut expr: Value, quote: QuoteKind, span: Span) -> SassResult { if quote == QuoteKind::None { expr = expr.unquote(); } expr.to_css_string(span, self.options.is_compressed()) } pub(crate) fn visit_ruleset(&mut self, ruleset: AstRuleSet) -> SassResult> { if self.declaration_name.is_some() { return Err(( "Style rules may not be used within nested declarations.", ruleset.span, ) .into()); } let AstRuleSet { selector: ruleset_selector, body: ruleset_body, .. } = ruleset; let selector_text = self.interpolation_to_value(ruleset_selector, true, true)?; if self.flags.in_keyframes() { let span = ruleset.selector_span; let sel_toks = Lexer::new_from_string(&selector_text, span); let parsed_selector = KeyframesSelectorParser::new(sel_toks).parse_keyframes_selector()?; let keyframes_ruleset = CssStmt::KeyframesRuleSet(KeyframesRuleSet { selector: parsed_selector, body: Vec::new(), }); self.with_parent( keyframes_ruleset, true, |visitor| { for stmt in ruleset_body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, CssStmt::is_style_rule, )?; return Ok(None); } let mut parsed_selector = self.parse_selector_from_string( &selector_text, !self.is_plain_css, !self.is_plain_css, ruleset.selector_span, )?; parsed_selector = parsed_selector.resolve_parent_selectors( self.style_rule_ignoring_at_root .as_ref() // todo: this clone should be superfluous(?) .map(|x| x.as_selector_list().clone()), !self.flags.at_root_excluding_style_rule(), )?; // todo: _mediaQueries let selector = self .extender .add_selector(parsed_selector, &self.media_queries); let rule = CssStmt::RuleSet { selector: selector.clone(), body: Vec::new(), is_group_end: false, }; let old_at_root_excluding_style_rule = self.flags.at_root_excluding_style_rule(); self.flags .set(ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, false); let old_style_rule_ignoring_at_root = self.style_rule_ignoring_at_root.take(); self.style_rule_ignoring_at_root = Some(selector); self.with_parent( rule, true, |visitor| { for stmt in ruleset_body { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) }, CssStmt::is_style_rule, )?; self.style_rule_ignoring_at_root = old_style_rule_ignoring_at_root; self.flags.set( ContextFlags::AT_ROOT_EXCLUDING_STYLE_RULE, old_at_root_excluding_style_rule, ); self.set_group_end(); Ok(None) } fn set_group_end(&mut self) -> Option<()> { if !self.style_rule_exists() { let children = self .css_tree .parent_to_child .get(&self.parent.unwrap_or(CssTree::ROOT))?; let child = *children.last()?; self.css_tree .get_mut(child) .as_mut() .map(CssStmt::set_group_end)?; } Some(()) } fn style_rule_exists(&self) -> bool { !self.flags.at_root_excluding_style_rule() && self.style_rule_ignoring_at_root.is_some() } pub(crate) fn visit_style(&mut self, style: AstStyle) -> SassResult> { if !self.style_rule_exists() && !self.flags.in_unknown_at_rule() && !self.flags.in_keyframes() { return Err(( "Declarations may only be used within style rules.", style.span, ) .into()); } let is_custom_property = style.is_custom_property(); let mut name = self.interpolation_to_value(style.name, false, true)?; if let Some(declaration_name) = &self.declaration_name { name = format!("{}-{}", declaration_name, name); } if let Some(value) = style .value .map(|s| { SassResult::Ok(Spanned { node: self.visit_expr(s.node)?, span: s.span, }) }) .transpose()? { // If the value is an empty list, preserve it, because converting it to CSS // will throw an error that we want the user to see. if !value.is_blank() || value.is_empty_list() { // todo: superfluous clones? self.css_tree.add_stmt( CssStmt::Style(Style { property: InternedString::get_or_intern(&name), value: Box::new(value), declared_as_custom_property: is_custom_property, }), self.parent, ); } else if name.starts_with("--") { return Err(("Custom property values may not be empty.", style.span).into()); } } let children = style.body; if !children.is_empty() { let old_declaration_name = self.declaration_name.take(); self.declaration_name = Some(name); self.with_scope::, _>(false, true, |visitor| { for stmt in children { let result = visitor.visit_stmt(stmt)?; debug_assert!(result.is_none()); } Ok(()) })?; self.declaration_name = old_declaration_name; } Ok(None) } } grass_compiler-0.13.4/src/fs.rs000064400000000000000000000050721046102023000144730ustar 00000000000000use std::{ io::{self, Error, ErrorKind}, path::{Path, PathBuf}, }; /// A trait to allow replacing the file system lookup mechanisms. /// /// As it stands, this is imperfect: it’s still using the types and some operations from /// `std::path`, which constrain it to the target platform’s norms. This could be ameliorated by /// the use of associated types for `Path` and `PathBuf`, and putting all remaining methods on this /// trait (`is_absolute`, `parent`, `join`, *&c.*); but that would infect too many other APIs to be /// desirable, so we live with it as it is—which is also acceptable, because the motivating example /// use case is mostly using this as an optimisation over the real platform underneath. pub trait Fs: std::fmt::Debug { /// Returns `true` if the path exists on disk and is pointing at a directory. fn is_dir(&self, path: &Path) -> bool; /// Returns `true` if the path exists on disk and is pointing at a regular file. fn is_file(&self, path: &Path) -> bool; /// Read the entire contents of a file into a bytes vector. fn read(&self, path: &Path) -> io::Result>; /// Canonicalize a file path fn canonicalize(&self, path: &Path) -> io::Result { Ok(path.to_path_buf()) } } /// Use [`std::fs`] to read any files from disk. /// /// This is the default file system implementation. #[derive(Debug)] pub struct StdFs; impl Fs for StdFs { #[inline] fn is_file(&self, path: &Path) -> bool { path.is_file() } #[inline] fn is_dir(&self, path: &Path) -> bool { path.is_dir() } #[inline] fn read(&self, path: &Path) -> io::Result> { std::fs::read(path) } #[inline] fn canonicalize(&self, path: &Path) -> io::Result { std::fs::canonicalize(path) } } /// A file system implementation that acts like it’s completely empty. /// /// This may be useful for security as it denies all access to the file system (so `@import` is /// prevented from leaking anything); you’ll need to use [`from_string`][crate::from_string] for /// this to make any sense (since [`from_path`][crate::from_path] would fail to find a file). #[derive(Debug)] pub struct NullFs; impl Fs for NullFs { #[inline] fn is_file(&self, _path: &Path) -> bool { false } #[inline] fn is_dir(&self, _path: &Path) -> bool { false } #[inline] fn read(&self, _path: &Path) -> io::Result> { Err(Error::new( ErrorKind::NotFound, "NullFs, there is no file system", )) } } grass_compiler-0.13.4/src/interner.rs000064400000000000000000000020451046102023000157060ustar 00000000000000use lasso::{Rodeo, Spur}; use std::cell::RefCell; use std::fmt::{self, Display}; thread_local!(static STRINGS: RefCell> = RefCell::new(Rodeo::default())); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)] pub struct InternedString(Spur); impl InternedString { pub fn get_or_intern>(s: T) -> Self { Self(STRINGS.with(|interner| interner.borrow_mut().get_or_intern(s))) } #[allow(dead_code)] pub fn resolve(self) -> String { STRINGS.with(|interner| interner.borrow().resolve(&self.0).to_owned()) } #[allow(dead_code)] pub fn is_empty(self) -> bool { self.resolve_ref() == "" } // todo: no need for unsafe here pub fn resolve_ref<'a>(self) -> &'a str { unsafe { STRINGS.with(|interner| interner.as_ptr().as_ref().unwrap().resolve(&self.0)) } } } impl Display for InternedString { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { STRINGS.with(|interner| write!(f, "{}", interner.borrow().resolve(&self.0))) } } grass_compiler-0.13.4/src/lexer.rs000064400000000000000000000110561046102023000152010ustar 00000000000000use std::{iter::Peekable, str::Chars, sync::Arc}; use codemap::{File, Span}; const FORM_FEED: char = '\x0C'; #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub(crate) struct Token { pub kind: char, pos: u32, } #[derive(Debug, Clone)] pub(crate) struct Lexer { buf: Vec, entire_span: Span, cursor: usize, /// If the input this lexer is spanned over is larger than the original span. /// This is possible due to interpolation. is_expanded: bool, } impl Lexer { pub fn raw_text(&self, start: usize) -> String { self.buf[start..self.cursor] .iter() .map(|t| t.kind) .collect() } pub fn next_char_is(&self, c: char) -> bool { matches!(self.peek(), Some(Token { kind, .. }) if kind == c) } /// Gets the span of the character at the given index. If the index is out of /// bounds, it returns the span of the last character. If the input is empty, /// it returns an empty span fn span_at_index(&self, idx: usize) -> Span { if self.is_expanded { return self.entire_span; } let (start, len) = match self.buf.get(idx) { Some(tok) => (tok.pos, tok.kind.len_utf8()), None => match self.buf.last() { Some(tok) => (tok.pos, tok.kind.len_utf8()), None => (0, 0), }, }; self.entire_span .subspan(start as u64, start as u64 + len as u64) } pub fn span_from(&self, start: usize) -> Span { let start = self.span_at_index(start); let end = self.prev_span(); start.merge(end) } pub fn prev_span(&self) -> Span { self.span_at_index(self.cursor.saturating_sub(1)) } pub fn current_span(&self) -> Span { self.span_at_index(self.cursor) } pub fn peek(&self) -> Option { self.buf.get(self.cursor).copied() } /// Peeks the previous token without modifying the peek cursor pub fn peek_previous(&mut self) -> Option { self.buf.get(self.cursor.checked_sub(1)?).copied() } /// Peeks `n` from current peeked position without modifying cursor pub fn peek_n(&self, n: usize) -> Option { self.buf.get(self.cursor + n).copied() } /// Peeks `n` behind current peeked position without modifying cursor pub fn peek_n_backwards(&self, n: usize) -> Option { self.buf.get(self.cursor.checked_sub(n)?).copied() } /// Set cursor to position and reset peek pub fn set_cursor(&mut self, cursor: usize) { self.cursor = cursor; } pub fn cursor(&self) -> usize { self.cursor } } impl Iterator for Lexer { type Item = Token; fn next(&mut self) -> Option { self.buf.get(self.cursor).copied().map(|tok| { self.cursor += 1; tok }) } fn size_hint(&self) -> (usize, Option) { let remaining = self.buf.len() - self.cursor; (remaining, Some(remaining)) } } /// Lex a string into a series of tokens pub(crate) struct TokenLexer<'a> { buf: Peekable>, cursor: u32, } // todo: maybe char indices? impl<'a> TokenLexer<'a> { pub fn new(buf: Peekable>) -> TokenLexer<'a> { Self { buf, cursor: 0 } } } impl<'a> Iterator for TokenLexer<'a> { type Item = Token; fn next(&mut self) -> Option { let kind = match self.buf.next()? { FORM_FEED => '\n', '\r' => { if self.buf.peek() == Some(&'\n') { self.cursor += 1; self.buf.next(); } '\n' } c => c, }; let len = kind.len_utf8() as u32; let pos = self.cursor; self.cursor += len; Some(Token { pos, kind }) } fn size_hint(&self) -> (usize, Option) { self.buf.size_hint() } } impl Lexer { pub fn new_from_file(file: &Arc) -> Self { let buf = TokenLexer::new(file.source().chars().peekable()).collect(); Self::new(buf, file.span, false) } pub fn new_from_string(s: &str, entire_span: Span) -> Self { let is_expanded = s.len() as u64 > entire_span.len(); let buf = TokenLexer::new(s.chars().peekable()).collect(); Self::new(buf, entire_span, is_expanded) } fn new(buf: Vec, entire_span: Span, is_expanded: bool) -> Self { Lexer { buf, cursor: 0, entire_span, is_expanded, } } } grass_compiler-0.13.4/src/lib.rs000064400000000000000000000170501046102023000146300ustar 00000000000000/*! This crate provides functionality for compiling [Sass](https://sass-lang.com/) to CSS. This crate targets compatibility with the reference implementation in Dart. If upgrading from the [now deprecated](https://sass-lang.com/blog/libsass-is-deprecated) `libsass`, one may have to modify their stylesheets. These changes will not differ from those necessary to upgrade to `dart-sass`, and in general such changes should be quite rare. This crate is capable of compiling Bootstrap 4 and 5, bulma and bulma-scss, Bourbon, as well as most other large Sass libraries with complete accuracy. For the vast majority of use cases there should be no perceptible differences from the reference implementation. ## Use as library ``` # use grass_compiler as grass; fn main() -> Result<(), Box> { let css = grass::from_string( "a { b { color: &; } }".to_owned(), &grass::Options::default().style(grass::OutputStyle::Compressed) )?; assert_eq!(css, "a b{color:a b}"); Ok(()) } ``` ## Use as binary ```bash cargo install grass grass input.scss ``` */ #![cfg_attr(doc_cfg, feature(doc_cfg))] #![warn(clippy::all, clippy::cargo, clippy::dbg_macro)] #![deny(missing_debug_implementations)] #![allow( clippy::use_self, // filter isn't fallible clippy::manual_filter_map, renamed_and_removed_lints, clippy::unknown_clippy_lints, clippy::single_match, clippy::new_without_default, clippy::single_match_else, clippy::multiple_crate_versions, clippy::wrong_self_convention, clippy::comparison_chain, clippy::unwrap_or_default, clippy::manual_unwrap_or_default, // todo: these should be enabled clippy::arc_with_non_send_sync, // todo: unignore once we bump MSRV clippy::assigning_clones, unknown_lints, )] use std::path::Path; use parse::{CssParser, SassParser, StylesheetParser}; use sass_ast::StyleSheet; use serializer::Serializer; #[cfg(feature = "wasm-exports")] use wasm_bindgen::prelude::*; use codemap::CodeMap; pub use crate::error::{ PublicSassErrorKind as ErrorKind, SassError as Error, SassResult as Result, }; pub use crate::fs::{Fs, NullFs, StdFs}; pub use crate::logger::{Logger, NullLogger, StdLogger}; pub use crate::options::{InputSyntax, Options, OutputStyle}; pub use crate::{builtin::Builtin, evaluate::Visitor}; pub(crate) use crate::{context_flags::ContextFlags, lexer::Token}; use crate::{lexer::Lexer, parse::ScssParser}; pub mod sass_value { pub use crate::{ ast::ArgumentResult, color::Color, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, unit::{ComplexUnit, Unit}, value::{ ArgList, CalculationArg, CalculationName, Number, SassCalculation, SassFunction, SassMap, SassNumber, Value, }, }; } pub mod sass_ast { pub use crate::ast::*; } pub use codemap; mod ast; mod builtin; mod color; mod common; mod context_flags; mod error; mod evaluate; mod fs; mod interner; mod lexer; mod logger; mod options; mod parse; mod selector; mod serializer; mod unit; mod utils; mod value; fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box { let (message, span) = err.raw(); Box::new(Error::from_loc(message, map.look_up_span(span), unicode)) } pub fn parse_stylesheet>( input: String, file_name: P, options: &Options, ) -> Result { // todo: much of this logic is duplicated in `from_string_with_file_name` let mut map = CodeMap::new(); let path = file_name.as_ref(); let file = map.add_file(path.to_string_lossy().into_owned(), input); let empty_span = file.span.subspan(0, 0); let lexer = Lexer::new_from_file(&file); let input_syntax = options .input_syntax .unwrap_or_else(|| InputSyntax::for_path(path)); let stylesheet = match input_syntax { InputSyntax::Scss => { ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Sass => { SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Css => { CssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } }; let stylesheet = match stylesheet { Ok(v) => v, Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)), }; Ok(stylesheet) } fn from_string_with_file_name>( input: String, file_name: P, options: &Options, ) -> Result { let mut map = CodeMap::new(); let path = file_name.as_ref(); let file = map.add_file(path.to_string_lossy().into_owned(), input); let empty_span = file.span.subspan(0, 0); let lexer = Lexer::new_from_file(&file); let input_syntax = options .input_syntax .unwrap_or_else(|| InputSyntax::for_path(path)); let stylesheet = match input_syntax { InputSyntax::Scss => { ScssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Sass => { SassParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } InputSyntax::Css => { CssParser::new(lexer, options, empty_span, file_name.as_ref()).__parse() } }; let stylesheet = match stylesheet { Ok(v) => v, Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)), }; let mut visitor = Visitor::new(path, options, &mut map, empty_span); match visitor.visit_stylesheet(stylesheet) { Ok(_) => {} Err(e) => return Err(raw_to_parse_error(&map, *e, options.unicode_error_messages)), } let stmts = visitor.finish(); let mut serializer = Serializer::new(options, &map, false, empty_span); let mut prev_was_group_end = false; let mut prev_requires_semicolon = false; for stmt in stmts { if stmt.is_invisible() { continue; } let is_group_end = stmt.is_group_end(); let requires_semicolon = Serializer::requires_semicolon(&stmt); serializer .visit_group(stmt, prev_was_group_end, prev_requires_semicolon) .map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?; prev_was_group_end = is_group_end; prev_requires_semicolon = requires_semicolon; } Ok(serializer.finish(prev_requires_semicolon)) } /// Compile CSS from a path /// /// n.b. `grass` does not currently support files or paths that are not valid UTF-8 /// /// ``` /// # use grass_compiler as grass; /// fn main() -> Result<(), Box> { /// let css = grass::from_path("input.scss", &grass::Options::default())?; /// Ok(()) /// } /// ``` #[inline] pub fn from_path>(p: P, options: &Options) -> Result { from_string_with_file_name(String::from_utf8(options.fs.read(p.as_ref())?)?, p, options) } /// Compile CSS from a string /// /// ``` /// # use grass_compiler as grass; /// fn main() -> Result<(), Box> { /// let css = grass::from_string("a { b { color: &; } }".to_string(), &grass::Options::default())?; /// assert_eq!(css, "a b {\n color: a b;\n}\n"); /// Ok(()) /// } /// ``` #[inline] pub fn from_string>(input: S, options: &Options) -> Result { from_string_with_file_name(input.into(), "stdin", options) } #[cfg(feature = "wasm-exports")] #[wasm_bindgen(js_name = from_string)] pub fn from_string_js(input: String) -> std::result::Result { from_string(input, &Options::default()).map_err(|e| e.to_string()) } grass_compiler-0.13.4/src/logger.rs000064400000000000000000000024561046102023000153450ustar 00000000000000use codemap::SpanLoc; use std::fmt::Debug; /// A trait to allow replacing logging mechanisms pub trait Logger: Debug { /// Logs message from a [`@debug`](https://sass-lang.com/documentation/at-rules/debug/) /// statement fn debug(&self, location: SpanLoc, message: &str); /// Logs message from a [`@warn`](https://sass-lang.com/documentation/at-rules/warn/) /// statement fn warn(&self, location: SpanLoc, message: &str); } /// Logs events to standard error, through [`eprintln!`] #[derive(Debug)] pub struct StdLogger; impl Logger for StdLogger { #[inline] fn debug(&self, location: SpanLoc, message: &str) { eprintln!( "{}:{} DEBUG: {}", location.file.name(), location.begin.line + 1, message ); } #[inline] fn warn(&self, location: SpanLoc, message: &str) { eprintln!( "Warning: {}\n ./{}:{}:{}", message, location.file.name(), location.begin.line + 1, location.begin.column + 1 ); } } /// Discards all logs #[derive(Debug)] pub struct NullLogger; impl Logger for NullLogger { #[inline] fn debug(&self, _location: SpanLoc, _message: &str) {} #[inline] fn warn(&self, _location: SpanLoc, _message: &str) {} } grass_compiler-0.13.4/src/options.rs000064400000000000000000000163001046102023000155520ustar 00000000000000use std::{ collections::HashMap, path::{Path, PathBuf}, }; use crate::{builtin::Builtin, Fs, Logger, StdFs, StdLogger}; /// Configuration for Sass compilation /// /// The simplest usage is `grass::Options::default()`; however, a builder pattern /// is also exposed to offer more control. #[derive(Debug)] pub struct Options<'a> { pub(crate) fs: &'a dyn Fs, pub(crate) logger: &'a dyn Logger, pub(crate) style: OutputStyle, pub(crate) load_paths: Vec, pub(crate) allows_charset: bool, pub(crate) unicode_error_messages: bool, pub(crate) quiet: bool, pub(crate) input_syntax: Option, pub(crate) custom_fns: HashMap, } impl Default for Options<'_> { #[inline] fn default() -> Self { Self { fs: &StdFs, logger: &StdLogger, style: OutputStyle::Expanded, load_paths: Vec::new(), allows_charset: true, unicode_error_messages: true, quiet: false, input_syntax: None, custom_fns: HashMap::new(), } } } impl<'a> Options<'a> { /// This option allows you to control the file system that Sass will see. /// /// By default, it uses [`StdFs`], which is backed by [`std::fs`], /// allowing direct, unfettered access to the local file system. #[must_use] #[inline] pub fn fs(mut self, fs: &'a dyn Fs) -> Self { self.fs = fs; self } /// This option allows you to define how log events should be handled /// /// Be default, [`StdLogger`] is used, which writes all events to standard output. #[must_use] #[inline] pub fn logger(mut self, logger: &'a dyn Logger) -> Self { self.logger = logger; self } /// `grass` currently offers 2 different output styles /// /// - [`OutputStyle::Expanded`] writes each selector and declaration on its own line. /// - [`OutputStyle::Compressed`] removes as many extra characters as possible /// and writes the entire stylesheet on a single line. /// /// By default, output is expanded. #[must_use] #[inline] pub const fn style(mut self, style: OutputStyle) -> Self { self.style = style; self } /// This flag tells Sass not to emit any warnings when compiling. By default, /// Sass emits warnings when deprecated features are used or when the `@warn` /// rule is encountered. It also silences the `@debug` rule. /// /// Setting this option to `true` will stop all logs from reaching the [`crate::Logger`]. /// /// By default, this value is `false` and warnings are emitted. #[must_use] #[inline] pub const fn quiet(mut self, quiet: bool) -> Self { self.quiet = quiet; self } /// All Sass implementations allow users to provide load paths: paths on the /// filesystem that Sass will look in when locating modules. For example, if /// you pass `node_modules/susy/sass` as a load path, you can use /// `@import "susy"` to load `node_modules/susy/sass/susy.scss`. /// /// Imports will always be resolved relative to the current file first, though. /// Load paths will only be used if no relative file exists that matches the /// module's URL. This ensures that you can't accidentally mess up your relative /// imports when you add a new library. /// /// This method will append a single path to the list. #[must_use] #[inline] pub fn load_path>(mut self, path: P) -> Self { self.load_paths.push(path.as_ref().to_owned()); self } /// Append multiple loads paths /// /// Note that this method does *not* remove existing load paths /// /// See [`Options::load_path`](Options::load_path) for more information about /// load paths #[must_use] #[inline] pub fn load_paths>(mut self, paths: &[P]) -> Self { for path in paths { self.load_paths.push(path.as_ref().to_owned()); } self } /// This flag tells Sass whether to emit a `@charset` /// declaration or a UTF-8 byte-order mark. /// /// By default, Sass will insert either a `@charset` /// declaration (in expanded output mode) or a byte-order /// mark (in compressed output mode) if the stylesheet /// contains any non-ASCII characters. #[must_use] #[inline] pub const fn allows_charset(mut self, allows_charset: bool) -> Self { self.allows_charset = allows_charset; self } /// This flag tells Sass only to emit ASCII characters as /// part of error messages. /// /// By default Sass will emit non-ASCII characters for /// these messages. /// /// This flag does not affect the CSS output. #[must_use] #[inline] pub const fn unicode_error_messages(mut self, unicode_error_messages: bool) -> Self { self.unicode_error_messages = unicode_error_messages; self } /// This option forces Sass to parse input using the given syntax. /// /// By default, Sass will attempt to read the file extension to determine /// the syntax. If this is not possible, it will default to [`InputSyntax::Scss`]. /// /// This flag only affects the first file loaded. Files that are loaded using /// `@import`, `@use`, or `@forward` will always have their syntax inferred. #[must_use] #[inline] pub const fn input_syntax(mut self, syntax: InputSyntax) -> Self { self.input_syntax = Some(syntax); self } /// Add a custom function accessible from within Sass /// /// See the [`Builtin`] documentation for additional information #[must_use] #[inline] #[cfg(any(feature = "custom-builtin-fns", doc))] #[cfg_attr(doc_cfg, doc(cfg(feature = "custom-builtin-fns")))] pub fn add_custom_fn>(mut self, name: S, func: Builtin) -> Self { self.custom_fns.insert(name.into(), func); self } pub(crate) fn is_compressed(&self) -> bool { matches!(self.style, OutputStyle::Compressed) } } /// Useful when parsing Sass from sources other than the file system /// /// See [`Options::input_syntax`] for additional information #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum InputSyntax { /// The CSS-superset SCSS syntax. Scss, /// The whitespace-sensitive indented syntax. Sass, /// The plain CSS syntax, which disallows special Sass features. Css, } impl InputSyntax { pub(crate) fn for_path(path: &Path) -> Self { match path .extension() .and_then(|ext| ext.to_str()) .map(str::to_ascii_lowercase) .as_deref() { Some("css") => Self::Css, Some("sass") => Self::Sass, _ => Self::Scss, } } } #[non_exhaustive] #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum OutputStyle { /// This mode writes each selector and declaration on its own line. /// /// This is the default output. Expanded, /// Ideal for release builds, this mode removes as many extra characters as /// possible and writes the entire stylesheet on a single line. Compressed, } grass_compiler-0.13.4/src/parse/at_root_query.rs000064400000000000000000000022701046102023000200660ustar 00000000000000use std::collections::HashSet; use crate::{ast::AtRootQuery, error::SassResult, lexer::Lexer}; use super::BaseParser; pub(crate) struct AtRootQueryParser { toks: Lexer, } impl BaseParser for AtRootQueryParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl AtRootQueryParser { pub fn new(toks: Lexer) -> AtRootQueryParser { AtRootQueryParser { toks } } pub fn parse(&mut self) -> SassResult { self.expect_char('(')?; self.whitespace()?; let include = self.scan_identifier("with", false)?; if !include { self.expect_identifier("without", false)?; } self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let mut names = HashSet::new(); loop { names.insert(self.parse_identifier(false, false)?.to_ascii_lowercase()); self.whitespace()?; if !self.looking_at_identifier() { break; } } self.expect_char(')')?; self.expect_done()?; Ok(AtRootQuery::new(include, names)) } } grass_compiler-0.13.4/src/parse/base.rs000064400000000000000000000511271046102023000161110ustar 00000000000000use crate::{ error::SassResult, lexer::Lexer, utils::{as_hex, hex_char_for, is_name, is_name_start, opposite_bracket}, Token, }; pub(crate) trait BaseParser { fn toks(&self) -> &Lexer; fn toks_mut(&mut self) -> &mut Lexer; fn whitespace_without_comments(&mut self) { while matches!( self.toks().peek(), Some(Token { kind: ' ' | '\t' | '\n', .. }) ) { self.toks_mut().next(); } } fn whitespace(&mut self) -> SassResult<()> { loop { self.whitespace_without_comments(); if !self.scan_comment()? { break; } } Ok(()) } fn scan_comment(&mut self) -> SassResult { if !matches!(self.toks().peek(), Some(Token { kind: '/', .. })) { return Ok(false); } Ok(match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { self.skip_silent_comment()?; true } Some(Token { kind: '*', .. }) => { self.skip_loud_comment()?; true } _ => false, }) } fn skip_silent_comment(&mut self) -> SassResult<()> { debug_assert!(self.next_matches("//")); self.toks_mut().next(); self.toks_mut().next(); while self.toks().peek().is_some() && !self.toks().next_char_is('\n') { self.toks_mut().next(); } Ok(()) } fn next_matches(&mut self, s: &str) -> bool { for (idx, c) in s.chars().enumerate() { match self.toks().peek_n(idx) { Some(Token { kind, .. }) if kind == c => {} _ => return false, } } true } fn skip_loud_comment(&mut self) -> SassResult<()> { debug_assert!(self.next_matches("/*")); self.toks_mut().next(); self.toks_mut().next(); while let Some(next) = self.toks_mut().next() { if next.kind != '*' { continue; } while self.scan_char('*') {} if self.scan_char('/') { return Ok(()); } } Err(("expected more input.", self.toks().current_span()).into()) } fn scan_char(&mut self, c: char) -> bool { if let Some(Token { kind, .. }) = self.toks().peek() { if kind == c { self.toks_mut().next(); return true; } } false } fn scan(&mut self, s: &str) -> bool { let start = self.toks().cursor(); for c in s.chars() { if !self.scan_char(c) { self.toks_mut().set_cursor(start); return false; } } true } fn expect_whitespace(&mut self) -> SassResult<()> { if !matches!( self.toks().peek(), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) ) && !self.scan_comment()? { return Err(("Expected whitespace.", self.toks().current_span()).into()); } self.whitespace()?; Ok(()) } fn parse_identifier( &mut self, // default=false normalize: bool, // default=false unit: bool, ) -> SassResult { let mut text = String::new(); if self.scan_char('-') { text.push('-'); if self.scan_char('-') { text.push('-'); self.parse_identifier_body(&mut text, normalize, unit)?; return Ok(text); } } match self.toks().peek() { Some(Token { kind: '_', .. }) if normalize => { self.toks_mut().next(); text.push('-'); } Some(Token { kind, .. }) if is_name_start(kind) => { self.toks_mut().next(); text.push(kind); } Some(Token { kind: '\\', .. }) => { text.push_str(&self.parse_escape(true)?); } Some(..) | None => { return Err(("Expected identifier.", self.toks().current_span()).into()) } } self.parse_identifier_body(&mut text, normalize, unit)?; Ok(text) } fn parse_identifier_body( &mut self, buffer: &mut String, normalize: bool, unit: bool, ) -> SassResult<()> { while let Some(tok) = self.toks().peek() { if unit && tok.kind == '-' { // Disallow `-` followed by a dot or a digit digit in units. let second = match self.toks().peek_n(1) { Some(v) => v, None => break, }; if second.kind == '.' || second.kind.is_ascii_digit() { break; } self.toks_mut().next(); buffer.push('-'); } else if normalize && tok.kind == '_' { buffer.push('-'); self.toks_mut().next(); } else if is_name(tok.kind) { self.toks_mut().next(); buffer.push(tok.kind); } else if tok.kind == '\\' { buffer.push_str(&self.parse_escape(false)?); } else { break; } } Ok(()) } fn parse_escape(&mut self, identifier_start: bool) -> SassResult { let start = self.toks().cursor(); self.expect_char('\\')?; let mut value = 0; let first = match self.toks().peek() { Some(t) => t, None => return Err(("Expected expression.", self.toks().current_span()).into()), }; if first.kind == '\n' { return Err(("Expected escape sequence.", self.toks().current_span()).into()); } else if first.kind.is_ascii_hexdigit() { for _ in 0..6 { let next = match self.toks().peek() { Some(t) => t, None => break, }; if !next.kind.is_ascii_hexdigit() { break; } value *= 16; value += as_hex(next.kind); self.toks_mut().next(); } if matches!( self.toks().peek(), Some(Token { kind: ' ', .. }) | Some(Token { kind: '\n', .. }) | Some(Token { kind: '\t', .. }) ) { self.toks_mut().next(); } } else { value = first.kind as u32; self.toks_mut().next(); } let c = std::char::from_u32(value) .ok_or_else(|| ("Invalid Unicode code point.", self.toks().span_from(start)))?; if (identifier_start && is_name_start(c) && !c.is_ascii_digit()) || (!identifier_start && is_name(c)) { Ok(c.to_string()) } else if value <= 0x1F || value == 0x7F || (identifier_start && c.is_ascii_digit()) { let mut buf = String::with_capacity(4); buf.push('\\'); if value > 0xF { buf.push(hex_char_for(value >> 4)); } buf.push(hex_char_for(value & 0xF)); buf.push(' '); Ok(buf) } else { Ok(format!("\\{}", c)) } } fn expect_char(&mut self, c: char) -> SassResult<()> { match self.toks().peek() { Some(tok) if tok.kind == c => { self.toks_mut().next(); Ok(()) } Some(..) | None => { Err((format!("expected \"{}\".", c), self.toks().current_span()).into()) } } } fn expect_char_with_message(&mut self, c: char, msg: &'static str) -> SassResult<()> { match self.toks().peek() { Some(tok) if tok.kind == c => { self.toks_mut().next(); Ok(()) } Some(..) | None => Err((format!("expected {}.", msg), self.toks().prev_span()).into()), } } fn parse_string(&mut self) -> SassResult { let quote = match self.toks_mut().next() { Some(Token { kind: q @ ('\'' | '"'), .. }) => q, Some(..) | None => return Err(("Expected string.", self.toks().current_span()).into()), }; let mut buffer = String::new(); let mut found_matching_quote = false; while let Some(next) = self.toks().peek() { if next.kind == quote { self.toks_mut().next(); found_matching_quote = true; break; } else if next.kind == '\n' || next.kind == '\r' { break; } else if next.kind == '\\' { if matches!( self.toks().peek_n(1), Some(Token { kind: '\n' | '\r', .. }) ) { self.toks_mut().next(); self.toks_mut().next(); } else { buffer.push(self.consume_escaped_char()?); } } else { self.toks_mut().next(); buffer.push(next.kind); } } if !found_matching_quote { return Err(( format!("Expected {quote}.", quote = quote), self.toks().current_span(), ) .into()); } Ok(buffer) } fn consume_escaped_char(&mut self) -> SassResult { self.expect_char('\\')?; match self.toks().peek() { None => Ok('\u{FFFD}'), Some(Token { kind: '\n' | '\r', .. }) => Err(("Expected escape sequence.", self.toks().current_span()).into()), Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => { let mut value = 0; for _ in 0..6 { let next = match self.toks().peek() { Some(c) => c, None => break, }; if !next.kind.is_ascii_hexdigit() { break; } self.toks_mut().next(); value = (value << 4) + as_hex(next.kind); } if self.toks().peek().is_some() && self.toks().peek().unwrap().kind.is_ascii_whitespace() { self.toks_mut().next(); } if value == 0 || (0xD800..=0xDFFF).contains(&value) || value >= 0x0010_FFFF { Ok('\u{FFFD}') } else { Ok(char::from_u32(value).unwrap()) } } Some(Token { kind, .. }) => { self.toks_mut().next(); Ok(kind) } } } fn declaration_value(&mut self, allow_empty: bool) -> SassResult { let mut buffer = String::new(); let mut brackets = Vec::new(); let mut wrote_newline = false; while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { buffer.push_str(&self.parse_escape(true)?); wrote_newline = false; } '"' | '\'' => { buffer.push_str(&self.fallible_raw_text(Self::parse_string)?); wrote_newline = false; } '/' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) { buffer.push_str(&self.fallible_raw_text(Self::skip_loud_comment)?); } else { buffer.push('/'); self.toks_mut().next(); } wrote_newline = false; } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { let s = self.parse_identifier(false, false)?; buffer.push_str(&s); } else { buffer.push('#'); self.toks_mut().next(); } wrote_newline = false; } c @ (' ' | '\t') => { if wrote_newline || !self .toks() .peek_n(1) .map_or(false, |tok| tok.kind.is_ascii_whitespace()) { buffer.push(c); } self.toks_mut().next(); } '\n' | '\r' => { if !wrote_newline { buffer.push('\n'); } wrote_newline = true; self.toks_mut().next(); } '[' | '(' | '{' => { buffer.push(tok.kind); self.toks_mut().next(); brackets.push(opposite_bracket(tok.kind)); wrote_newline = false; } ']' | ')' | '}' => { if let Some(end) = brackets.pop() { buffer.push(tok.kind); self.expect_char(end)?; } else { break; } wrote_newline = false; } ';' => { if brackets.is_empty() { break; } self.toks_mut().next(); buffer.push(';'); wrote_newline = false; } 'u' | 'U' => { if let Some(url) = self.try_parse_url()? { buffer.push_str(&url); } else { buffer.push(tok.kind); self.toks_mut().next(); } wrote_newline = false; } c => { if self.looking_at_identifier() { buffer.push_str(&self.parse_identifier(false, false)?); } else { self.toks_mut().next(); buffer.push(c); } wrote_newline = false; } } } if let Some(last) = brackets.pop() { self.expect_char(last)?; } if !allow_empty && buffer.is_empty() { return Err(("Expected token.", self.toks().current_span()).into()); } Ok(buffer) } /// Returns whether the scanner is immediately before a plain CSS identifier. /// /// This is based on [the CSS algorithm][], but it assumes all backslashes /// start escapes. /// /// [the CSS algorithm]: https://drafts.csswg.org/css-syntax-3/#would-start-an-identifier fn looking_at_identifier(&self) -> bool { match self.toks().peek() { Some(Token { kind, .. }) if is_name_start(kind) || kind == '\\' => return true, Some(Token { kind: '-', .. }) => {} Some(..) | None => return false, } match self.toks().peek_n(1) { Some(Token { kind, .. }) if is_name_start(kind) || kind == '-' || kind == '\\' => true, Some(..) | None => false, } } fn try_parse_url(&mut self) -> SassResult> { let start = self.toks().cursor(); if !self.scan_identifier("url", false)? { return Ok(None); } if !self.scan_char('(') { self.toks_mut().set_cursor(start); return Ok(None); } self.whitespace()?; // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. let mut buffer = "url(".to_owned(); while let Some(next) = self.toks().peek() { match next.kind { '\\' => { buffer.push_str(&self.parse_escape(false)?); } '!' | '#' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { self.toks_mut().next(); buffer.push(next.kind); } ')' => { self.toks_mut().next(); buffer.push(next.kind); return Ok(Some(buffer)); } ' ' | '\t' | '\n' | '\r' => { self.whitespace_without_comments(); if !self.toks().next_char_is(')') { break; } } _ => break, } } self.toks_mut().set_cursor(start); Ok(None) } fn raw_text(&mut self, func: impl Fn(&mut Self) -> T) -> String { let start = self.toks().cursor(); func(self); self.toks().raw_text(start) } fn fallible_raw_text( &mut self, func: impl Fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks().cursor(); func(self)?; Ok(self.toks().raw_text(start)) } /// Peeks to see if the `ident` is at the current position. If it is, /// consume the identifier fn scan_identifier( &mut self, ident: &'static str, // default=false case_sensitive: bool, ) -> SassResult { if !self.looking_at_identifier() { return Ok(false); } let start = self.toks().cursor(); if self.consume_identifier(ident, case_sensitive)? && !self.looking_at_identifier_body() { Ok(true) } else { self.toks_mut().set_cursor(start); Ok(false) } } fn consume_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult { for c in ident.chars() { if !self.scan_ident_char(c, case_sensitive)? { return Ok(false); } } Ok(true) } fn scan_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult { let matches = |actual: char| { if case_sensitive { actual == c } else { actual.to_ascii_lowercase() == c.to_ascii_lowercase() } }; Ok(match self.toks().peek() { Some(Token { kind, .. }) if matches(kind) => { self.toks_mut().next(); true } Some(Token { kind: '\\', .. }) => { let start = self.toks().cursor(); if matches(self.consume_escaped_char()?) { return Ok(true); } self.toks_mut().set_cursor(start); false } Some(..) | None => false, }) } fn expect_ident_char(&mut self, c: char, case_sensitive: bool) -> SassResult<()> { if self.scan_ident_char(c, case_sensitive)? { return Ok(()); } Err((format!("Expected \"{}\".", c), self.toks().current_span()).into()) } fn looking_at_identifier_body(&mut self) -> bool { matches!(self.toks().peek(), Some(t) if is_name(t.kind) || t.kind == '\\') } fn parse_variable_name(&mut self) -> SassResult { self.expect_char('$')?; self.parse_identifier(true, false) } fn expect_identifier(&mut self, ident: &str, case_sensitive: bool) -> SassResult<()> { let start = self.toks().cursor(); for c in ident.chars() { if !self.scan_ident_char(c, case_sensitive)? { return Err(( format!("Expected \"{}\".", ident), self.toks_mut().span_from(start), ) .into()); } } if !self.looking_at_identifier_body() { return Ok(()); } Err(( format!("Expected \"{}\".", ident), self.toks_mut().span_from(start), ) .into()) } // todo: not real impl fn expect_done(&mut self) -> SassResult<()> { debug_assert!(self.toks().peek().is_none()); Ok(()) } fn spaces(&mut self) { while self.toks().next_char_is(' ') || self.toks().next_char_is('\t') { self.toks_mut().next(); } } } grass_compiler-0.13.4/src/parse/css.rs000064400000000000000000000142021046102023000157600ustar 00000000000000use std::{collections::BTreeMap, path::Path, sync::Arc}; use codemap::{Span, Spanned}; use crate::{ ast::*, builtin::DISALLOWED_PLAIN_CSS_FUNCTION_NAMES, common::QuoteKind, error::SassResult, lexer::Lexer, ContextFlags, Options, }; use super::{value::ValueParser, BaseParser, StylesheetParser}; pub(crate) struct CssParser<'a> { pub toks: Lexer, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, } impl<'a> BaseParser for CssParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } fn skip_silent_comment(&mut self) -> SassResult<()> { Err(( "Silent comments aren't allowed in plain CSS.", self.toks.current_span(), ) .into()) } } impl<'a> StylesheetParser<'a> for CssParser<'a> { fn is_plain_css(&self) -> bool { true } fn is_indented(&self) -> bool { false } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn current_indentation(&self) -> usize { 0 } fn empty_span(&self) -> Span { self.empty_span } const IDENTIFIER_LIKE: Option SassResult>> = Some(Self::parse_identifier_like); fn parse_at_rule( &mut self, _child: fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks.cursor(); self.expect_char('@')?; let name = self.parse_interpolated_identifier()?; self.whitespace()?; match name.as_plain() { Some("at-root") | Some("content") | Some("debug") | Some("each") | Some("error") | Some("extend") | Some("for") | Some("function") | Some("if") | Some("include") | Some("mixin") | Some("return") | Some("warn") | Some("while") => { self.almost_any_value(false)?; Err(( "This at-rule isn't allowed in plain CSS.", self.toks.span_from(start), ) .into()) } Some("import") => self.parse_css_import_rule(start), Some("media") => self.parse_media_rule(start), Some("-moz-document") => self._parse_moz_document_rule(name), Some("supports") => self.parse_supports_rule(), _ => self.unknown_at_rule(name, start), } } } impl<'a> CssParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { CssParser { toks, path: file_name, empty_span, flags: ContextFlags::empty(), options, } } fn parse_css_import_rule(&mut self, _start: usize) -> SassResult { let url_start = self.toks.cursor(); let url = if self.toks.next_char_is('u') || self.toks.next_char_is('U') { self.parse_dynamic_url()? .span(self.toks.span_from(url_start)) } else { let string = self.parse_interpolated_string()?; AstExpr::String( StringExpr(string.node.as_interpolation(true), QuoteKind::None), string.span, ) .span(string.span) }; self.whitespace()?; let modifiers = self.try_import_modifiers()?; self.expect_statement_separator(Some("@import rule"))?; Ok(AstStmt::ImportRule(AstImportRule { imports: vec![AstImport::Plain(AstPlainCssImport { url: Interpolation::new_with_expr(url), modifiers, span: self.toks.span_from(url_start), })], })) } fn parse_identifier_like(&mut self) -> SassResult> { let start = self.toks.cursor(); let identifier = self.parse_interpolated_identifier()?; let plain = identifier.as_plain().unwrap(); let lower = plain.to_ascii_lowercase(); if let Some(special_fn) = ValueParser::try_parse_special_function(self, &lower, start)? { return Ok(special_fn); } let before_args = self.toks.cursor(); if !self.scan_char('(') { let span = self.toks.span_from(start); return Ok(AstExpr::String(StringExpr(identifier, QuoteKind::None), span).span(span)); } let allow_empty_second_arg = lower == "var"; let mut arguments = Vec::new(); if !self.scan_char(')') { loop { self.whitespace()?; let arg_start = self.toks.cursor(); if allow_empty_second_arg && arguments.len() == 1 && self.toks.next_char_is(')') { arguments.push(AstExpr::String( StringExpr(Interpolation::new_plain(String::new()), QuoteKind::None), self.toks.span_from(arg_start), )); break; } arguments.push(self.parse_expression_until_comma(true)?.node); self.whitespace()?; if !self.scan_char(',') { break; } } self.expect_char(')')?; } let span = self.toks.span_from(start); if DISALLOWED_PLAIN_CSS_FUNCTION_NAMES.contains(plain) { return Err(("This function isn't allowed in plain CSS.", span).into()); } Ok( AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: identifier, arguments: ArgumentInvocation { positional: arguments, named: BTreeMap::new(), rest: None, keyword_rest: None, span: self.toks.span_from(before_args), }, span, })) .span(span), ) } } grass_compiler-0.13.4/src/parse/keyframes.rs000064400000000000000000000071371046102023000171670ustar 00000000000000use std::fmt; use crate::{ast::KeyframesSelector, error::SassResult, lexer::Lexer, Token}; use super::BaseParser; impl fmt::Display for KeyframesSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { KeyframesSelector::To => f.write_str("to"), KeyframesSelector::From => f.write_str("from"), KeyframesSelector::Percent(p) => write!(f, "{}%", p), } } } pub(crate) struct KeyframesSelectorParser { toks: Lexer, } impl BaseParser for KeyframesSelectorParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl KeyframesSelectorParser { pub fn new(toks: Lexer) -> KeyframesSelectorParser { KeyframesSelectorParser { toks } } pub fn parse_keyframes_selector(&mut self) -> SassResult> { let mut selectors = Vec::new(); loop { self.whitespace()?; if self.looking_at_identifier() { if self.scan_identifier("to", false)? { selectors.push(KeyframesSelector::To); } else if self.scan_identifier("from", false)? { selectors.push(KeyframesSelector::From); } else { return Err(("Expected \"to\" or \"from\".", self.toks.current_span()).into()); } } else { selectors.push(self.parse_percentage_selector()?); } self.whitespace()?; if !self.scan_char(',') { break; } } Ok(selectors) } fn parse_percentage_selector(&mut self) -> SassResult { let mut buffer = String::new(); if self.scan_char('+') { buffer.push('+'); } if !matches!( self.toks.peek(), Some(Token { kind: '0'..='9' | '.', .. }) ) { return Err(("Expected number.", self.toks.current_span()).into()); } while matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } if self.scan_char('.') { buffer.push('.'); while matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } } if self.scan_ident_char('e', false)? { buffer.push('e'); if matches!( self.toks.peek(), Some(Token { kind: '+' | '-', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } if !matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { return Err(("Expected digit.", self.toks.current_span()).into()); } while matches!( self.toks.peek(), Some(Token { kind: '0'..='9', .. }) ) { buffer.push(self.toks.next().unwrap().kind); } } self.expect_char('%')?; Ok(KeyframesSelector::Percent(buffer.into_boxed_str())) } } grass_compiler-0.13.4/src/parse/media_query.rs000064400000000000000000000102001046102023000174660ustar 00000000000000use crate::{ast::MediaQuery, error::SassResult, lexer::Lexer}; use super::BaseParser; pub(crate) struct MediaQueryParser { pub toks: Lexer, } impl BaseParser for MediaQueryParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl MediaQueryParser { pub fn new(toks: Lexer) -> MediaQueryParser { MediaQueryParser { toks } } pub fn parse(&mut self) -> SassResult> { let mut queries = Vec::new(); loop { self.whitespace()?; queries.push(self.parse_media_query()?); self.whitespace()?; if !self.scan_char(',') { break; } } if self.toks.next().is_some() { return Err(("expected no more input.", self.toks.current_span()).into()); } Ok(queries) } fn parse_media_query(&mut self) -> SassResult { if self.toks.next_char_is('(') { let mut conditions = vec![self.parse_media_in_parens()?]; self.whitespace()?; let mut conjunction = true; if self.scan_identifier("and", false)? { self.expect_whitespace()?; conditions.append(&mut self.parse_media_logic_sequence("and")?); } else if self.scan_identifier("or", false)? { self.expect_whitespace()?; conjunction = false; conditions.append(&mut self.parse_media_logic_sequence("or")?); } return Ok(MediaQuery::condition(conditions, conjunction)); } let mut modifier: Option = None; let media_type: Option; let identifier1 = self.parse_identifier(false, false)?; if identifier1.to_ascii_lowercase() == "not" { self.expect_whitespace()?; if !self.looking_at_identifier() { return Ok(MediaQuery::condition( vec![format!("(not {})", self.parse_media_in_parens()?)], true, )); } } self.whitespace()?; if !self.looking_at_identifier() { return Ok(MediaQuery::media_type(Some(identifier1), None, None)); } let identifier2 = self.parse_identifier(false, false)?; if identifier2.to_ascii_lowercase() == "and" { self.expect_whitespace()?; media_type = Some(identifier1); } else { self.whitespace()?; modifier = Some(identifier1); media_type = Some(identifier2); if self.scan_identifier("and", false)? { // For example, "@media only screen and ..." self.expect_whitespace()?; } else { // For example, "@media only screen {" return Ok(MediaQuery::media_type(media_type, modifier, None)); } } // We've consumed either `IDENTIFIER "and"` or // `IDENTIFIER IDENTIFIER "and"`. if self.scan_identifier("not", false)? { // For example, "@media screen and not (...) {" self.expect_whitespace()?; return Ok(MediaQuery::media_type( media_type, modifier, Some(vec![format!("(not {})", self.parse_media_in_parens()?)]), )); } Ok(MediaQuery::media_type( media_type, modifier, Some(self.parse_media_logic_sequence("and")?), )) } fn parse_media_in_parens(&mut self) -> SassResult { self.expect_char('(')?; let result = format!("({})", self.declaration_value(false)?); self.expect_char(')')?; Ok(result) } fn parse_media_logic_sequence(&mut self, operator: &'static str) -> SassResult> { let mut result = Vec::new(); loop { result.push(self.parse_media_in_parens()?); self.whitespace()?; if !self.scan_identifier(operator, false)? { return Ok(result); } self.expect_whitespace()?; } } } grass_compiler-0.13.4/src/parse/mod.rs000064400000000000000000000016741046102023000157600ustar 00000000000000use crate::ast::*; pub(crate) use at_root_query::AtRootQueryParser; pub(crate) use base::BaseParser; pub(crate) use css::CssParser; pub(crate) use keyframes::KeyframesSelectorParser; pub(crate) use media_query::MediaQueryParser; pub(crate) use sass::SassParser; pub(crate) use scss::ScssParser; pub(crate) use stylesheet::StylesheetParser; mod at_root_query; mod base; mod css; mod keyframes; mod media_query; mod sass; mod scss; mod stylesheet; mod value; #[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] pub(crate) enum DeclarationOrBuffer { Stmt(AstStmt), Buffer(Interpolation), } /// Names that functions are not allowed to have pub(super) const RESERVED_IDENTIFIERS: [&str; 8] = [ "calc", "element", "expression", "url", "and", "or", "not", "clamp", ]; #[derive(Debug, Clone)] pub(crate) enum VariableDeclOrInterpolation { VariableDecl(AstVariableDecl), Interpolation(Interpolation), } grass_compiler-0.13.4/src/parse/sass.rs000064400000000000000000000403741046102023000161520ustar 00000000000000use std::path::Path; use codemap::Span; use crate::{ast::*, error::SassResult, lexer::Lexer, ContextFlags, Options, Token}; use super::{BaseParser, StylesheetParser}; pub(crate) struct SassParser<'a> { pub toks: Lexer, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, pub current_indentation: usize, pub next_indentation: Option, pub spaces: Option, pub next_indentation_end: Option, } impl<'a> BaseParser for SassParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } fn whitespace_without_comments(&mut self) { while let Some(next) = self.toks.peek() { if next.kind != '\t' && next.kind != ' ' { break; } self.toks.next(); } } fn skip_loud_comment(&mut self) -> SassResult<()> { self.expect_char('/')?; self.expect_char('*')?; loop { let mut next = self.toks.next(); match next { Some(Token { kind: '\n', .. }) => { return Err(("expected */.", self.toks.prev_span()).into()) } Some(Token { kind: '*', .. }) => {} _ => continue, } loop { next = self.toks.next(); if !matches!(next, Some(Token { kind: '*', .. })) { break; } } if matches!(next, Some(Token { kind: '/', .. })) { break; } } Ok(()) } } impl<'a> StylesheetParser<'a> for SassParser<'a> { fn is_plain_css(&self) -> bool { false } fn is_indented(&self) -> bool { true } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn current_indentation(&self) -> usize { self.current_indentation } fn empty_span(&self) -> Span { self.empty_span } fn parse_style_rule_selector(&mut self) -> SassResult { let mut buffer = Interpolation::new(); loop { buffer.add_interpolation(self.almost_any_value(true)?); buffer.add_char('\n'); if !(buffer.trailing_string().trim_end().ends_with(',') && self.scan_char('\n')) { break; } } Ok(buffer) } fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> { if !self.at_end_of_statement() { self.expect_newline()?; } if self.peek_indentation()? <= self.current_indentation { return Ok(()); } // todo: position: _nextIndentationEnd!.position // todo: error message, "Nothing may be indented ${name == null ? 'here' : 'beneath a $name'}." Err(("Nothing may be indented here", self.toks.current_span()).into()) } fn at_end_of_statement(&self) -> bool { matches!(self.toks.peek(), Some(Token { kind: '\n', .. }) | None) } fn looking_at_children(&mut self) -> SassResult { Ok(self.at_end_of_statement() && self.peek_indentation()? > self.current_indentation) } fn scan_else(&mut self, if_indentation: usize) -> SassResult { if self.peek_indentation()? != if_indentation { return Ok(false); } let start = self.toks.cursor(); let start_indentation = self.current_indentation; let start_next_indentation = self.next_indentation; let start_next_indentation_end = self.next_indentation_end; self.read_indentation()?; if self.scan_char('@') && self.scan_identifier("else", false)? { return Ok(true); } self.toks.set_cursor(start); self.current_indentation = start_indentation; self.next_indentation = start_next_indentation; self.next_indentation_end = start_next_indentation_end; Ok(false) } fn parse_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult> { let mut children = Vec::new(); self.while_indented_lower(|parser| { if let Some(parsed_child) = parser.parse_child(|parser| Ok(Some(child(parser)?)))? { children.push(parsed_child); } Ok(()) })?; Ok(children) } fn parse_statements( &mut self, statement: fn(&mut Self) -> SassResult>, ) -> SassResult> { if self.toks.next_char_is(' ') || self.toks.next_char_is('\t') { return Err(( "Indenting at the beginning of the document is illegal.", self.toks.current_span(), ) .into()); } let mut statements = Vec::new(); while self.toks.peek().is_some() { if let Some(child) = self.parse_child(statement)? { statements.push(child); } let indentation = self.read_indentation()?; assert_eq!(indentation, 0); } Ok(statements) } fn parse_silent_comment(&mut self) -> SassResult { let start = self.toks.cursor(); self.expect_char('/')?; self.expect_char('/')?; let mut buffer = String::new(); let parent_indentation = self.current_indentation; 'outer: loop { let comment_prefix = if self.scan_char('/') { "///" } else { "//" }; loop { buffer.push_str(comment_prefix); // buffer.write(commentPrefix); // Skip the initial characters because we're already writing the // slashes. for _ in comment_prefix.len()..(self.current_indentation - parent_indentation) { buffer.push(' '); } while self.toks.peek().is_some() && !self.toks.next_char_is('\n') { buffer.push(self.toks.next().unwrap().kind); } buffer.push('\n'); if self.peek_indentation()? < parent_indentation { break 'outer; } if self.peek_indentation()? == parent_indentation { // Look ahead to the next line to see if it starts another comment. if matches!( self.toks.peek_n(1 + parent_indentation), Some(Token { kind: '/', .. }) ) && matches!( self.toks.peek_n(2 + parent_indentation), Some(Token { kind: '/', .. }) ) { self.read_indentation()?; } break; } self.read_indentation()?; } if !self.scan("//") { break; } } Ok(AstStmt::SilentComment(AstSilentComment { text: buffer, span: self.toks.span_from(start), })) } fn parse_loud_comment(&mut self) -> SassResult { let start = self.toks.cursor(); self.expect_char('/')?; self.expect_char('*')?; let mut first = true; let mut buffer = Interpolation::new_plain("/*".to_owned()); let parent_indentation = self.current_indentation; loop { if first { let beginning_of_comment = self.toks.cursor(); self.spaces(); if self.toks.next_char_is('\n') { self.read_indentation()?; buffer.add_char(' '); } else { buffer.add_string(self.toks.raw_text(beginning_of_comment)); } } else { buffer.add_string("\n * ".to_owned()); } first = false; for _ in 3..(self.current_indentation - parent_indentation) { buffer.add_char(' '); } while self.toks.peek().is_some() { match self.toks.peek() { Some(Token { kind: '\n' | '\r', .. }) => break, Some(Token { kind: '#', .. }) => { if matches!(self.toks.peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { buffer.add_char('#'); self.toks.next(); } } Some(Token { kind, .. }) => { buffer.add_char(kind); self.toks.next(); } None => todo!(), } } if self.peek_indentation()? <= parent_indentation { break; } // Preserve empty lines. while self.looking_at_double_newline() { self.expect_newline()?; buffer.add_char('\n'); buffer.add_char(' '); buffer.add_char('*'); } self.read_indentation()?; } if !buffer.trailing_string().trim_end().ends_with("*/") { buffer.add_string(" */".to_owned()); } Ok(AstLoudComment { text: buffer, span: self.toks.span_from(start), }) } } impl<'a> SassParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IS_USE_ALLOWED, true); SassParser { toks, path: file_name, empty_span, flags, options, current_indentation: 0, next_indentation: None, next_indentation_end: None, spaces: None, } } fn peek_indentation(&mut self) -> SassResult { if let Some(next) = self.next_indentation { return Ok(next); } if self.toks.peek().is_none() { self.next_indentation = Some(0); self.next_indentation_end = Some(self.toks.cursor()); return Ok(0); } let start = self.toks.cursor(); if !self.scan_char('\n') { return Err(("Expected newline.", self.toks.current_span()).into()); } let mut contains_tab; let mut contains_space; let mut next_indentation; loop { contains_tab = false; contains_space = false; next_indentation = 0; while let Some(next) = self.toks.peek() { match next.kind { ' ' => contains_space = true, '\t' => contains_tab = true, _ => break, } next_indentation += 1; self.toks.next(); } if self.toks.peek().is_none() { self.next_indentation = Some(0); self.next_indentation_end = Some(self.toks.cursor()); self.toks.set_cursor(start); return Ok(0); } if !self.scan_char('\n') { break; } } self.check_indentation_consistency(contains_tab, contains_space, start)?; self.next_indentation = Some(next_indentation); if next_indentation > 0 { self.spaces.get_or_insert(contains_space); } self.next_indentation_end = Some(self.toks.cursor()); self.toks.set_cursor(start); Ok(next_indentation) } fn check_indentation_consistency( &mut self, contains_tab: bool, contains_space: bool, start: usize, ) -> SassResult<()> { // NOTE: error message spans here start from the beginning of the line if contains_tab { if contains_space { return Err(( "Tabs and spaces may not be mixed.", self.toks.span_from(start), ) .into()); } else if self.spaces == Some(true) { return Err(("Expected spaces, was tabs.", self.toks.span_from(start)).into()); } } else if contains_space && self.spaces == Some(false) { return Err(("Expected tabs, was spaces.", self.toks.span_from(start)).into()); } Ok(()) } fn expect_newline(&mut self) -> SassResult<()> { match self.toks.peek() { Some(Token { kind: ';', .. }) => Err(( "semicolons aren't allowed in the indented syntax.", self.toks.current_span(), ) .into()), Some(Token { kind: '\r', .. }) => { self.toks.next(); self.scan_char('\n'); Ok(()) } Some(Token { kind: '\n', .. }) => { self.toks.next(); Ok(()) } _ => Err(("expected newline.", self.toks.current_span()).into()), } } fn read_indentation(&mut self) -> SassResult { self.current_indentation = match self.next_indentation { Some(indent) => indent, None => { let indent = self.peek_indentation()?; self.next_indentation = Some(indent); indent } }; self.toks.set_cursor(self.next_indentation_end.unwrap()); self.next_indentation = None; self.next_indentation_end = None; Ok(self.current_indentation) } fn while_indented_lower( &mut self, mut body: impl FnMut(&mut Self) -> SassResult<()>, ) -> SassResult<()> { let parent_indentation = self.current_indentation; let mut child_indentation = None; while self.peek_indentation()? > parent_indentation { let indentation = self.read_indentation()?; let child_indent = *child_indentation.get_or_insert(indentation); if child_indent != indentation { return Err(( format!( "Inconsistent indentation, expected {child_indent} spaces.", child_indent = child_indent ), self.toks.current_span(), ) .into()); } body(self)?; } Ok(()) } fn parse_child( &mut self, child: impl FnOnce(&mut Self) -> SassResult>, ) -> SassResult> { Ok(Some(match self.toks.peek() { Some(Token { kind: '\n' | '\r', .. }) => return Ok(None), Some(Token { kind: '$', .. }) => AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, ), Some(Token { kind: '/', .. }) => match self.toks.peek_n(1) { Some(Token { kind: '/', .. }) => self.parse_silent_comment()?, Some(Token { kind: '*', .. }) => AstStmt::LoudComment(self.parse_loud_comment()?), _ => return child(self), }, _ => return child(self), })) } fn looking_at_double_newline(&mut self) -> bool { match self.toks.peek() { // todo: is this branch reachable Some(Token { kind: '\r', .. }) => match self.toks.peek_n(1) { Some(Token { kind: '\n', .. }) => { matches!(self.toks.peek_n(2), Some(Token { kind: '\n', .. })) } Some(Token { kind: '\r', .. }) => true, _ => false, }, Some(Token { kind: '\n', .. }) => matches!( self.toks.peek_n(1), Some(Token { kind: '\n' | '\r', .. }) ), _ => false, } } } grass_compiler-0.13.4/src/parse/scss.rs000064400000000000000000000026761046102023000161570ustar 00000000000000use std::path::Path; use codemap::Span; use crate::{lexer::Lexer, ContextFlags, Options}; use super::{BaseParser, StylesheetParser}; pub(crate) struct ScssParser<'a> { pub toks: Lexer, pub path: &'a Path, pub empty_span: Span, pub flags: ContextFlags, pub options: &'a Options<'a>, } impl<'a> ScssParser<'a> { pub fn new( toks: Lexer, options: &'a Options<'a>, empty_span: Span, file_name: &'a Path, ) -> Self { let mut flags = ContextFlags::empty(); flags.set(ContextFlags::IS_USE_ALLOWED, true); ScssParser { toks, path: file_name, empty_span, flags, options, } } } impl<'a> BaseParser for ScssParser<'a> { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl<'a> StylesheetParser<'a> for ScssParser<'a> { fn is_plain_css(&self) -> bool { false } fn is_indented(&self) -> bool { false } fn path(&self) -> &'a Path { self.path } fn options(&self) -> &Options { self.options } fn current_indentation(&self) -> usize { 0 } fn flags(&self) -> &ContextFlags { &self.flags } fn flags_mut(&mut self) -> &mut ContextFlags { &mut self.flags } fn empty_span(&self) -> Span { self.empty_span } } grass_compiler-0.13.4/src/parse/stylesheet.rs000064400000000000000000003200221046102023000173610ustar 00000000000000use std::{ cell::Cell, collections::{BTreeMap, HashSet}, ffi::OsString, mem, path::{Path, PathBuf}, sync::Arc, }; use codemap::{Span, Spanned}; use crate::{ ast::*, common::{unvendor, Identifier, QuoteKind}, error::SassResult, lexer::Lexer, utils::{is_name, is_name_start, is_plain_css_import, opposite_bracket}, ContextFlags, Options, Token, }; use super::{ value::{Predicate, ValueParser}, BaseParser, DeclarationOrBuffer, ScssParser, VariableDeclOrInterpolation, RESERVED_IDENTIFIERS, }; /// Default implementations are oriented towards the SCSS syntax, as both CSS and /// SCSS share the behavior pub(crate) trait StylesheetParser<'a>: BaseParser + Sized { // todo: make constant? fn is_plain_css(&self) -> bool; // todo: make constant? fn is_indented(&self) -> bool; fn options(&self) -> &Options; fn path(&self) -> &Path; fn empty_span(&self) -> Span; fn current_indentation(&self) -> usize; fn flags(&self) -> &ContextFlags; fn flags_mut(&mut self) -> &mut ContextFlags; #[allow(clippy::type_complexity)] const IDENTIFIER_LIKE: Option SassResult>> = None; fn parse_style_rule_selector(&mut self) -> SassResult { self.almost_any_value(false) } fn expect_statement_separator(&mut self, _name: Option<&str>) -> SassResult<()> { self.whitespace_without_comments(); match self.toks().peek() { Some(Token { kind: ';' | '}', .. }) | None => Ok(()), _ => { self.expect_char(';')?; unreachable!(); } } } fn at_end_of_statement(&self) -> bool { matches!( self.toks().peek(), Some(Token { kind: ';' | '}' | '{', .. }) | None ) } fn looking_at_children(&mut self) -> SassResult { Ok(matches!(self.toks().peek(), Some(Token { kind: '{', .. }))) } fn scan_else(&mut self, _if_indentation: usize) -> SassResult { let start = self.toks().cursor(); self.whitespace()?; if self.scan_char('@') { if self.scan_identifier("else", true)? { return Ok(true); } if self.scan_identifier("elseif", true)? { // todo: deprecation warning here let new_cursor = self.toks().cursor() - 2; self.toks_mut().set_cursor(new_cursor); return Ok(true); } } self.toks_mut().set_cursor(start); Ok(false) } fn parse_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult> { self.expect_char('{')?; self.whitespace_without_comments(); let mut children = Vec::new(); let mut found_matching_brace = false; while let Some(tok) = self.toks().peek() { match tok.kind { '$' => children.push(AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, )), '/' => match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { children.push(self.parse_silent_comment()?); self.whitespace_without_comments(); } Some(Token { kind: '*', .. }) => { children.push(AstStmt::LoudComment(self.parse_loud_comment()?)); self.whitespace_without_comments(); } _ => children.push(child(self)?), }, ';' => { self.toks_mut().next(); self.whitespace_without_comments(); } '}' => { self.expect_char('}')?; found_matching_brace = true; break; } _ => children.push(child(self)?), } } if !found_matching_brace { return Err(("expected \"}\".", self.toks().current_span()).into()); } Ok(children) } fn parse_statements( &mut self, statement: fn(&mut Self) -> SassResult>, ) -> SassResult> { let mut stmts = Vec::new(); self.whitespace_without_comments(); while let Some(tok) = self.toks().peek() { match tok.kind { '$' => stmts.push(AstStmt::VariableDecl( self.parse_variable_declaration_without_namespace(None, None)?, )), '/' => match self.toks().peek_n(1) { Some(Token { kind: '/', .. }) => { stmts.push(self.parse_silent_comment()?); self.whitespace_without_comments(); } Some(Token { kind: '*', .. }) => { stmts.push(AstStmt::LoudComment(self.parse_loud_comment()?)); self.whitespace_without_comments(); } _ => { if let Some(stmt) = statement(self)? { stmts.push(stmt); } } }, ';' => { self.toks_mut().next(); self.whitespace_without_comments(); } _ => { if let Some(stmt) = statement(self)? { stmts.push(stmt); } } } } Ok(stmts) } // todo: rename fn __parse(&mut self) -> SassResult { let mut style_sheet = StyleSheet::new( self.is_plain_css(), self.options() .fs .canonicalize(self.path()) .unwrap_or_else(|_| self.path().to_path_buf()), ); // Allow a byte-order mark at the beginning of the document. self.scan_char('\u{feff}'); style_sheet.body = self.parse_statements(|parser| { if parser.next_matches("@charset") { parser.expect_char('@')?; parser.expect_identifier("charset", false)?; parser.whitespace()?; parser.parse_string()?; return Ok(None); } Ok(Some(parser.parse_statement()?)) })?; for (idx, child) in style_sheet.body.iter().enumerate() { match child { AstStmt::VariableDecl(_) | AstStmt::LoudComment(_) | AstStmt::SilentComment(_) => { continue } AstStmt::Use(..) => style_sheet.uses.push(idx), AstStmt::Forward(..) => style_sheet.forwards.push(idx), _ => break, } } Ok(style_sheet) } fn looking_at_expression(&mut self) -> bool { let character = if let Some(c) = self.toks().peek() { c } else { return false; }; match character.kind { '.' => !matches!(self.toks().peek_n(1), Some(Token { kind: '.', .. })), '!' => match self.toks().peek_n(1) { Some(Token { kind: 'i' | 'I', .. }) | None => true, Some(Token { kind, .. }) => kind.is_ascii_whitespace(), }, '(' | '/' | '[' | '\'' | '"' | '#' | '+' | '-' | '\\' | '$' | '&' => true, c => is_name_start(c) || c.is_ascii_digit(), } } fn parse_argument_declaration(&mut self) -> SassResult { self.expect_char('(')?; self.whitespace()?; let mut arguments = Vec::new(); let mut named = HashSet::new(); let mut rest_argument: Option = None; while self.toks_mut().next_char_is('$') { let name_start = self.toks().cursor(); let name = Identifier::from(self.parse_variable_name()?); let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let mut default_value: Option = None; if self.scan_char(':') { self.whitespace()?; default_value = Some(self.parse_expression_until_comma(false)?.node); } else if self.scan_char('.') { self.expect_char('.')?; self.expect_char('.')?; self.whitespace()?; rest_argument = Some(name); break; } arguments.push(Argument { name, default: default_value, }); if !named.insert(name) { return Err(("Duplicate argument.", name_span).into()); } if !self.scan_char(',') { break; } self.whitespace()?; } self.expect_char(')')?; Ok(ArgumentDeclaration { args: arguments, rest: rest_argument, }) } fn plain_at_rule_name(&mut self) -> SassResult { self.expect_char('@')?; let name = self.parse_identifier(false, false)?; self.whitespace()?; Ok(name) } fn with_children( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult>> { let start = self.toks().cursor(); let children = self.parse_children(child)?; let span = self.toks_mut().span_from(start); self.whitespace_without_comments(); Ok(Spanned { node: children, span, }) } fn parse_at_root_query(&mut self) -> SassResult { let mut buffer = Interpolation::new(); self.expect_char('(')?; buffer.add_char('('); self.whitespace()?; buffer.add_expr(self.parse_expression(None, None, None)?); if self.scan_char(':') { self.whitespace()?; buffer.add_char(':'); buffer.add_char(' '); buffer.add_expr(self.parse_expression(None, None, None)?); } self.expect_char(')')?; self.whitespace()?; buffer.add_char(')'); Ok(buffer) } fn parse_at_root_rule(&mut self, start: usize) -> SassResult { Ok(AstStmt::AtRootRule(if self.toks_mut().next_char_is('(') { let query_start = self.toks().cursor(); let query = self.parse_at_root_query()?; let query_span = self.toks_mut().span_from(query_start); self.whitespace()?; let children = self.with_children(Self::parse_statement)?.node; AstAtRootRule { query: Some(Spanned { node: query, span: query_span, }), body: children, span: self.toks_mut().span_from(start), } } else if self.looking_at_children()? { let children = self.with_children(Self::parse_statement)?.node; AstAtRootRule { query: None, body: children, span: self.toks_mut().span_from(start), } } else { let child = self.parse_style_rule(None, None)?; AstAtRootRule { query: None, body: vec![child], span: self.toks_mut().span_from(start), } })) } fn parse_content_rule(&mut self, start: usize) -> SassResult { if !self.flags().in_mixin() { return Err(( "@content is only allowed within mixin declarations.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_invocation(true, false)? } else { ArgumentInvocation::empty(self.toks().current_span()) }; self.expect_statement_separator(Some("@content rule"))?; self.flags_mut().set(ContextFlags::FOUND_CONTENT_RULE, true); Ok(AstStmt::ContentRule(AstContentRule { args })) } fn parse_debug_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@debug rule"))?; Ok(AstStmt::Debug(AstDebugRule { value: value.node, span: value.span, })) } fn parse_each_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let mut variables = vec![Identifier::from(self.parse_variable_name()?)]; self.whitespace()?; while self.scan_char(',') { self.whitespace()?; variables.push(Identifier::from(self.parse_variable_name()?)); self.whitespace()?; } self.expect_identifier("in", false)?; self.whitespace()?; let list = self.parse_expression(None, None, None)?.node; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::Each(AstEach { variables, list, body, })) } fn parse_disallowed_at_rule(&mut self, start: usize) -> SassResult { self.almost_any_value(false)?; Err(( "This at-rule is not allowed here.", self.toks_mut().span_from(start), ) .into()) } fn parse_error_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@error rule"))?; Ok(AstStmt::ErrorRule(AstErrorRule { value: value.node, span: value.span, })) } fn parse_extend_rule(&mut self, start: usize) -> SassResult { if !self.flags().in_style_rule() && !self.flags().in_mixin() && !self.flags().in_content_block() { return Err(( "@extend may only be used within style rules.", self.toks_mut().span_from(start), ) .into()); } let value = self.almost_any_value(false)?; let is_optional = self.scan_char('!'); if is_optional { self.expect_identifier("optional", false)?; } self.expect_statement_separator(Some("@extend rule"))?; Ok(AstStmt::Extend(AstExtendRule { value, is_optional, span: self.toks_mut().span_from(start), })) } fn parse_for_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let var_start = self.toks().cursor(); let variable = Spanned { node: Identifier::from(self.parse_variable_name()?), span: self.toks_mut().span_from(var_start), }; self.whitespace()?; self.expect_identifier("from", false)?; self.whitespace()?; let exclusive: Cell> = Cell::new(None); let from = self.parse_expression( Some(&|parser| { if !parser.looking_at_identifier() { return Ok(false); } Ok(if parser.scan_identifier("to", false)? { exclusive.set(Some(true)); true } else if parser.scan_identifier("through", false)? { exclusive.set(Some(false)); true } else { false }) }), None, None, )?; let is_exclusive = match exclusive.get() { Some(b) => b, None => { return Err(( "Expected \"to\" or \"through\".", self.toks().current_span(), ) .into()) } }; self.whitespace()?; let to = self.parse_expression(None, None, None)?; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::For(AstFor { variable, from, to, is_exclusive, body, })) } fn parse_function_rule(&mut self, start: usize) -> SassResult { let name_start = self.toks().cursor(); let name = self.parse_identifier(true, false)?; let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let arguments = self.parse_argument_declaration()?; if self.flags().in_mixin() || self.flags().in_content_block() { return Err(( "Mixins may not contain function declarations.", self.toks_mut().span_from(start), ) .into()); } else if self.flags().in_control_flow() { return Err(( "Functions may not be declared in control directives.", self.toks_mut().span_from(start), ) .into()); } if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) { return Err(("Invalid function name.", self.toks_mut().span_from(start)).into()); } self.whitespace()?; let children = self.with_children(Self::function_child)?.node; Ok(AstStmt::FunctionDecl(AstFunctionDecl { name: Spanned { node: Identifier::from(name), span: name_span, }, arguments, body: children, })) } fn parse_variable_declaration_with_namespace(&mut self) -> SassResult { let start = self.toks().cursor(); let namespace = self.parse_identifier(false, false)?; let namespace_span = self.toks_mut().span_from(start); self.expect_char('.')?; self.parse_variable_declaration_without_namespace( Some(Spanned { node: Identifier::from(namespace), span: namespace_span, }), Some(start), ) } fn function_child(&mut self) -> SassResult { let start = self.toks().cursor(); if !self.toks_mut().next_char_is('@') { match self.parse_variable_declaration_with_namespace() { Ok(decl) => return Ok(AstStmt::VariableDecl(decl)), Err(e) => { self.toks_mut().set_cursor(start); let stmt = match self.parse_declaration_or_style_rule() { Ok(stmt) => stmt, Err(..) => return Err(e), }; let (is_style_rule, span) = match stmt { AstStmt::RuleSet(ruleset) => (true, ruleset.span), AstStmt::Style(style) => (false, style.span), _ => unreachable!(), }; return Err(( format!( "@function rules may not contain {}.", if is_style_rule { "style rules" } else { "declarations" } ), span, ) .into()); } } } return match self.plain_at_rule_name()?.as_str() { "debug" => self.parse_debug_rule(), "each" => self.parse_each_rule(Self::function_child), "else" => self.parse_disallowed_at_rule(start), "error" => self.parse_error_rule(), "for" => self.parse_for_rule(Self::function_child), "if" => self.parse_if_rule(Self::function_child), "return" => self.parse_return_rule(), "warn" => self.parse_warn_rule(), "while" => self.parse_while_rule(Self::function_child), _ => self.parse_disallowed_at_rule(start), }; } fn parse_if_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let if_indentation = self.current_indentation(); let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let condition = self.parse_expression(None, None, None)?.node; let body = self.parse_children(child)?; self.whitespace_without_comments(); let mut clauses = vec![AstIfClause { condition, body }]; let mut last_clause: Option> = None; while self.scan_else(if_indentation)? { self.whitespace()?; if self.scan_identifier("if", false)? { self.whitespace()?; let condition = self.parse_expression(None, None, None)?.node; let body = self.parse_children(child)?; clauses.push(AstIfClause { condition, body }); } else { last_clause = Some(self.parse_children(child)?); break; } } self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); self.whitespace_without_comments(); Ok(AstStmt::If(AstIf { if_clauses: clauses, else_clause: last_clause, })) } fn try_parse_import_supports_function(&mut self) -> SassResult> { if !self.looking_at_interpolated_identifier() { return Ok(None); } let start = self.toks().cursor(); let name = self.parse_interpolated_identifier()?; debug_assert!(name.as_plain() != Some("not")); if !self.scan_char('(') { self.toks_mut().set_cursor(start); return Ok(None); } let value = self.parse_interpolated_declaration_value(true, true, true)?; self.expect_char(')')?; Ok(Some(AstSupportsCondition::Function { name, args: value })) } fn parse_import_supports_query(&mut self) -> SassResult { Ok(if self.scan_identifier("not", false)? { self.whitespace()?; AstSupportsCondition::Negation(Box::new(self.supports_condition_in_parens()?)) } else if self.toks_mut().next_char_is('(') { self.parse_supports_condition()? } else { match self.try_parse_import_supports_function()? { Some(function) => function, None => { let start = self.toks().cursor(); let name = self.parse_expression(None, None, None)?; self.expect_char(':')?; self.supports_declaration_value(name.node, start)? } } }) } fn try_import_modifiers(&mut self) -> SassResult> { // Exit before allocating anything if we're not looking at any modifiers, as // is the most common case. if !self.looking_at_interpolated_identifier() && !self.toks_mut().next_char_is('(') { return Ok(None); } let mut buffer = Interpolation::new(); loop { if self.looking_at_interpolated_identifier() { if !buffer.is_empty() { buffer.add_char(' '); } let identifier = self.parse_interpolated_identifier()?; let name = identifier.as_plain().map(str::to_ascii_lowercase); buffer.add_interpolation(identifier); if name.as_deref() != Some("and") && self.scan_char('(') { if name.as_deref() == Some("supports") { let query = self.parse_import_supports_query()?; let is_declaration = matches!(query, AstSupportsCondition::Declaration { .. }); if !is_declaration { buffer.add_char('('); } buffer.add_expr(AstExpr::Supports(Arc::new(query)).span(self.empty_span())); if !is_declaration { buffer.add_char(')'); } } else { buffer.add_char('('); buffer.add_interpolation( self.parse_interpolated_declaration_value(true, true, true)?, ); buffer.add_char(')'); } self.expect_char(')')?; self.whitespace()?; } else { self.whitespace()?; if self.scan_char(',') { buffer.add_char(','); buffer.add_char(' '); buffer.add_interpolation(self.parse_media_query_list()?); return Ok(Some(buffer)); } } } else if self.toks_mut().next_char_is('(') { if !buffer.is_empty() { buffer.add_char(' '); } buffer.add_interpolation(self.parse_media_query_list()?); return Ok(Some(buffer)); } else { return Ok(Some(buffer)); } } } fn try_url_contents(&mut self, name: Option<&str>) -> SassResult> { let start = self.toks().cursor(); if !self.scan_char('(') { return Ok(None); } self.whitespace_without_comments(); // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not // backtrack and re-parse as a function expression. let mut buffer = Interpolation::new(); buffer.add_string(name.unwrap_or("url").to_owned()); buffer.add_char('('); while let Some(next) = self.toks().peek() { match next.kind { '\\' => buffer.add_string(self.parse_escape(false)?), '!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { self.toks_mut().next(); buffer.add_char(next.kind); } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { let interpolation = self.parse_single_interpolation()?; buffer.add_interpolation(interpolation); } else { self.toks_mut().next(); buffer.add_char(next.kind); } } ')' => { self.toks_mut().next(); buffer.add_char(next.kind); return Ok(Some(buffer)); } ' ' | '\t' | '\n' | '\r' => { self.whitespace_without_comments(); if !self.toks_mut().next_char_is(')') { break; } } _ => break, } } self.toks_mut().set_cursor(start); Ok(None) } fn parse_dynamic_url(&mut self) -> SassResult { let start = self.toks().cursor(); self.expect_identifier("url", false)?; Ok(match self.try_url_contents(None)? { Some(contents) => AstExpr::String( StringExpr(contents, QuoteKind::None), self.toks_mut().span_from(start), ), None => AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: Interpolation::new_plain("url".to_owned()), arguments: self.parse_argument_invocation(false, false)?, span: self.toks_mut().span_from(start), })), }) } fn parse_import_argument(&mut self, start: usize) -> SassResult { if self.toks_mut().next_char_is('u') || self.toks_mut().next_char_is('U') { let url = self.parse_dynamic_url()?; self.whitespace()?; let modifiers = self.try_import_modifiers()?; return Ok(AstImport::Plain(AstPlainCssImport { url: Interpolation::new_with_expr(url.span(self.toks_mut().span_from(start))), modifiers, span: self.toks_mut().span_from(start), })); } let start = self.toks().cursor(); let url = self.parse_string()?; let raw_url = self.toks().raw_text(start); self.whitespace()?; let modifiers = self.try_import_modifiers()?; let span = self.toks_mut().span_from(start); if is_plain_css_import(&url) || modifiers.is_some() { Ok(AstImport::Plain(AstPlainCssImport { url: Interpolation::new_plain(raw_url), modifiers, span, })) } else { // todo: try parseImportUrl Ok(AstImport::Sass(AstSassImport { url, span })) } } fn parse_import_rule(&mut self, start: usize) -> SassResult { let mut imports = Vec::new(); loop { self.whitespace()?; let argument = self.parse_import_argument(self.toks().cursor())?; // todo: _inControlDirective if (self.flags().in_control_flow() || self.flags().in_mixin()) && argument.is_dynamic() { self.parse_disallowed_at_rule(start)?; } imports.push(argument); self.whitespace()?; if !self.scan_char(',') { break; } } Ok(AstStmt::ImportRule(AstImportRule { imports })) } fn parse_public_identifier(&mut self) -> SassResult { let start = self.toks().cursor(); let ident = self.parse_identifier(true, false)?; Self::assert_public(&ident, self.toks_mut().span_from(start))?; Ok(ident) } fn parse_include_rule(&mut self) -> SassResult { let mut namespace: Option> = None; let name_start = self.toks().cursor(); let mut name = self.parse_identifier(false, false)?; if self.scan_char('.') { let namespace_span = self.toks_mut().span_from(name_start); namespace = Some(Spanned { node: Identifier::from(name), span: namespace_span, }); name = self.parse_public_identifier()?; } else { name = name.replace('_', "-"); } let name = Identifier::from(name); let name_span = self.toks_mut().span_from(name_start); self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_invocation(true, false)? } else { ArgumentInvocation::empty(self.toks().current_span()) }; self.whitespace()?; let content_args = if self.scan_identifier("using", false)? { self.whitespace()?; let args = self.parse_argument_declaration()?; self.whitespace()?; Some(args) } else { None }; let mut content_block: Option = None; if content_args.is_some() || self.looking_at_children()? { let content_args = content_args.unwrap_or_else(ArgumentDeclaration::empty); let was_in_content_block = self.flags().in_content_block(); self.flags_mut().set(ContextFlags::IN_CONTENT_BLOCK, true); let body = self.with_children(Self::parse_statement)?.node; content_block = Some(AstContentBlock { args: content_args, body, }); self.flags_mut() .set(ContextFlags::IN_CONTENT_BLOCK, was_in_content_block); } else { self.expect_statement_separator(None)?; } Ok(AstStmt::Include(AstInclude { namespace, name: Spanned { node: name, span: name_span, }, args, content: content_block, span: name_span, })) } fn parse_media_rule(&mut self, start: usize) -> SassResult { let query_start = self.toks().cursor(); let query = self.parse_media_query_list()?; let query_span = self.toks_mut().span_from(query_start); let body = self.with_children(Self::parse_statement)?.node; Ok(AstStmt::Media(AstMedia { query, query_span, body, span: self.toks_mut().span_from(start), })) } fn parse_interpolated_string(&mut self) -> SassResult> { let start = self.toks().cursor(); let quote = match self.toks_mut().next() { Some(Token { kind: kind @ ('"' | '\''), .. }) => kind, Some(..) | None => unreachable!("Expected string."), }; let mut buffer = Interpolation::new(); let mut found_match = false; while let Some(next) = self.toks().peek() { match next.kind { c if c == quote => { self.toks_mut().next(); found_match = true; break; } '\n' => break, '\\' => { match self.toks().peek_n(1) { // todo: if (second == $cr) scanner.scanChar($lf); // we basically need to stop normalizing to gain parity Some(Token { kind: '\n', .. }) => { self.toks_mut().next(); self.toks_mut().next(); } _ => buffer.add_char(self.consume_escaped_char()?), } } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { self.toks_mut().next(); buffer.add_char(next.kind); } } _ => { buffer.add_char(next.kind); self.toks_mut().next(); } } } if !found_match { return Err(( format!("Expected {quote}.", quote = quote), self.toks().current_span(), ) .into()); } Ok(Spanned { node: StringExpr(buffer, QuoteKind::Quoted), span: self.toks_mut().span_from(start), }) } fn parse_return_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(None)?; Ok(AstStmt::Return(AstReturn { val: value.node, span: value.span, })) } fn parse_mixin_rule(&mut self, start: usize) -> SassResult { let name = Identifier::from(self.parse_identifier(true, false)?); self.whitespace()?; let args = if self.toks_mut().next_char_is('(') { self.parse_argument_declaration()? } else { ArgumentDeclaration::empty() }; if self.flags().in_mixin() || self.flags().in_content_block() { return Err(( "Mixins may not contain mixin declarations.", self.toks_mut().span_from(start), ) .into()); } else if self.flags().in_control_flow() { return Err(( "Mixins may not be declared in control directives.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; let old_found_content_rule = self.flags().found_content_rule(); self.flags_mut() .set(ContextFlags::FOUND_CONTENT_RULE, false); self.flags_mut().set(ContextFlags::IN_MIXIN, true); let body = self.with_children(Self::parse_statement)?.node; let has_content = self.flags_mut().found_content_rule(); self.flags_mut() .set(ContextFlags::FOUND_CONTENT_RULE, old_found_content_rule); self.flags_mut().set(ContextFlags::IN_MIXIN, false); Ok(AstStmt::Mixin(AstMixin { name, args, body, has_content, })) } fn _parse_moz_document_rule(&mut self, _name: Interpolation) -> SassResult { todo!("special cased @-moz-document not yet implemented") } fn unknown_at_rule(&mut self, name: Interpolation, start: usize) -> SassResult { let was_in_unknown_at_rule = self.flags().in_unknown_at_rule(); self.flags_mut().set(ContextFlags::IN_UNKNOWN_AT_RULE, true); let value: Option = if !self.toks_mut().next_char_is('!') && !self.at_end_of_statement() { Some(self.almost_any_value(false)?) } else { None }; let children = if self.looking_at_children()? { Some(self.with_children(Self::parse_statement)?.node) } else { self.expect_statement_separator(None)?; None }; self.flags_mut() .set(ContextFlags::IN_UNKNOWN_AT_RULE, was_in_unknown_at_rule); Ok(AstStmt::UnknownAtRule(AstUnknownAtRule { name, value, body: children, span: self.toks_mut().span_from(start), })) } fn try_supports_operation( &mut self, interpolation: &Interpolation, _start: usize, ) -> SassResult> { if interpolation.contents.len() != 1 { return Ok(None); } let expression = match interpolation.contents.first() { Some(InterpolationPart::Expr(e)) => e, Some(InterpolationPart::String(..)) => return Ok(None), None => unreachable!(), }; let before_whitespace = self.toks().cursor(); self.whitespace()?; let mut operation: Option = None; let mut operator: Option = None; while self.looking_at_identifier() { if let Some(operator) = &operator { self.expect_identifier(operator, false)?; } else if self.scan_identifier("and", false)? { operator = Some("and".to_owned()); } else if self.scan_identifier("or", false)? { operator = Some("or".to_owned()); } else { self.toks_mut().set_cursor(before_whitespace); return Ok(None); } self.whitespace()?; let right = self.supports_condition_in_parens()?; operation = Some(AstSupportsCondition::Operation { left: Box::new(operation.unwrap_or_else(|| { AstSupportsCondition::Interpolation(expression.clone().node) })), operator: operator.clone(), right: Box::new(right), }); self.whitespace()?; } Ok(operation) } fn supports_declaration_value( &mut self, name: AstExpr, start: usize, ) -> SassResult { let value = match &name { AstExpr::String(StringExpr(text, QuoteKind::None), ..) if text.initial_plain().starts_with("--") => { let text = self.parse_interpolated_declaration_value(false, false, true)?; AstExpr::String( StringExpr(text, QuoteKind::None), self.toks_mut().span_from(start), ) } _ => { self.whitespace()?; self.parse_expression(None, None, None)?.node } }; Ok(AstSupportsCondition::Declaration { name, value }) } fn supports_condition_in_parens(&mut self) -> SassResult { let start = self.toks().cursor(); if self.looking_at_interpolated_identifier() { let identifier = self.parse_interpolated_identifier()?; let ident_span = self.toks_mut().span_from(start); if identifier.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { return Err((r#""not" is not a valid identifier here."#, ident_span).into()); } if self.scan_char('(') { let arguments = self.parse_interpolated_declaration_value(true, true, true)?; self.expect_char(')')?; return Ok(AstSupportsCondition::Function { name: identifier, args: arguments, }); } else if identifier.contents.len() != 1 || !matches!( identifier.contents.first(), Some(InterpolationPart::Expr(..)) ) { return Err(("Expected @supports condition.", ident_span).into()); } else { match identifier.contents.first() { Some(InterpolationPart::Expr(e)) => { return Ok(AstSupportsCondition::Interpolation(e.clone().node)) } _ => unreachable!(), } } } self.expect_char('(')?; self.whitespace()?; if self.scan_identifier("not", false)? { self.whitespace()?; let condition = self.supports_condition_in_parens()?; self.expect_char(')')?; return Ok(AstSupportsCondition::Negation(Box::new(condition))); } else if self.toks_mut().next_char_is('(') { let condition = self.parse_supports_condition()?; self.expect_char(')')?; return Ok(condition); } // Unfortunately, we may have to backtrack here. The grammar is: // // Expression ":" Expression // | InterpolatedIdentifier InterpolatedAnyValue? // // These aren't ambiguous because this `InterpolatedAnyValue` is forbidden // from containing a top-level colon, but we still have to parse the full // expression to figure out if there's a colon after it. // // We could avoid the overhead of a full expression parse by looking ahead // for a colon (outside of balanced brackets), but in practice we expect the // vast majority of real uses to be `Expression ":" Expression`, so it makes // sense to parse that case faster in exchange for less code complexity and // a slower backtracking case. let name: AstExpr; let name_start = self.toks().cursor(); let was_in_parens = self.flags().in_parens(); let expr = self.parse_expression(None, None, None); let found_colon = self.expect_char(':'); match (expr, found_colon) { (Ok(val), Ok(..)) => { name = val.node; } (Ok(..), Err(e)) | (Err(e), Ok(..)) | (Err(e), Err(..)) => { self.toks_mut().set_cursor(name_start); self.flags_mut().set(ContextFlags::IN_PARENS, was_in_parens); let identifier = self.parse_interpolated_identifier()?; // todo: superfluous clone? if let Some(operation) = self.try_supports_operation(&identifier, name_start)? { self.expect_char(')')?; return Ok(operation); } // If parsing an expression fails, try to parse an // `InterpolatedAnyValue` instead. But if that value runs into a // top-level colon, then this is probably intended to be a declaration // after all, so we rethrow the declaration-parsing error. let mut contents = Interpolation::new(); contents.add_interpolation(identifier); contents.add_interpolation( self.parse_interpolated_declaration_value(true, true, false)?, ); if self.toks_mut().next_char_is(':') { return Err(e); } self.expect_char(')')?; return Ok(AstSupportsCondition::Anything { contents }); } } let declaration = self.supports_declaration_value(name, start)?; self.expect_char(')')?; Ok(declaration) } fn parse_supports_condition(&mut self) -> SassResult { if self.scan_identifier("not", false)? { self.whitespace()?; return Ok(AstSupportsCondition::Negation(Box::new( self.supports_condition_in_parens()?, ))); } let mut condition = self.supports_condition_in_parens()?; self.whitespace()?; let mut operator: Option = None; while self.looking_at_identifier() { if let Some(operator) = &operator { self.expect_identifier(operator, false)?; } else if self.scan_identifier("or", false)? { operator = Some("or".to_owned()); } else { self.expect_identifier("and", false)?; operator = Some("and".to_owned()); } self.whitespace()?; let right = self.supports_condition_in_parens()?; condition = AstSupportsCondition::Operation { left: Box::new(condition), operator: operator.clone(), right: Box::new(right), }; self.whitespace()?; } Ok(condition) } fn parse_supports_rule(&mut self) -> SassResult { let condition = self.parse_supports_condition()?; self.whitespace()?; let children = self.with_children(Self::parse_statement)?; Ok(AstStmt::Supports(AstSupportsRule { condition, body: children.node, span: children.span, })) } fn parse_warn_rule(&mut self) -> SassResult { let value = self.parse_expression(None, None, None)?; self.expect_statement_separator(Some("@warn rule"))?; Ok(AstStmt::Warn(AstWarn { value: value.node, span: value.span, })) } fn parse_while_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let was_in_control_directive = self.flags().in_control_flow(); self.flags_mut().set(ContextFlags::IN_CONTROL_FLOW, true); let condition = self.parse_expression(None, None, None)?.node; let body = self.with_children(child)?.node; self.flags_mut() .set(ContextFlags::IN_CONTROL_FLOW, was_in_control_directive); Ok(AstStmt::While(AstWhile { condition, body })) } fn parse_forward_rule(&mut self, start: usize) -> SassResult { let url = PathBuf::from(self.parse_url_string()?); self.whitespace()?; let prefix = if self.scan_identifier("as", false)? { self.whitespace()?; let prefix = self.parse_identifier(true, false)?; self.expect_char('*')?; self.whitespace()?; Some(prefix) } else { None }; let mut shown_mixins_and_functions: Option> = None; let mut shown_variables: Option> = None; let mut hidden_mixins_and_functions: Option> = None; let mut hidden_variables: Option> = None; if self.scan_identifier("show", false)? { let members = self.parse_member_list()?; shown_mixins_and_functions = Some(members.0); shown_variables = Some(members.1); } else if self.scan_identifier("hide", false)? { let members = self.parse_member_list()?; hidden_mixins_and_functions = Some(members.0); hidden_variables = Some(members.1); } let config = self.parse_configuration(true)?; self.expect_statement_separator(Some("@forward rule"))?; let span = self.toks_mut().span_from(start); if !self.flags().is_use_allowed() { return Err(( "@forward rules must be written before any other rules.", span, ) .into()); } Ok(AstStmt::Forward( if let (Some(shown_mixins_and_functions), Some(shown_variables)) = (shown_mixins_and_functions, shown_variables) { AstForwardRule::show( url, shown_mixins_and_functions, shown_variables, prefix, config, span, ) } else if let (Some(hidden_mixins_and_functions), Some(hidden_variables)) = (hidden_mixins_and_functions, hidden_variables) { AstForwardRule::hide( url, hidden_mixins_and_functions, hidden_variables, prefix, config, span, ) } else { AstForwardRule::new(url, prefix, config, span) }, )) } fn parse_member_list(&mut self) -> SassResult<(HashSet, HashSet)> { let mut identifiers = HashSet::new(); let mut variables = HashSet::new(); loop { self.whitespace()?; // todo: withErrorMessage("Expected variable, mixin, or function name" if self.toks_mut().next_char_is('$') { variables.insert(Identifier::from(self.parse_variable_name()?)); } else { identifiers.insert(Identifier::from(self.parse_identifier(true, false)?)); } self.whitespace()?; if !self.scan_char(',') { break; } } Ok((identifiers, variables)) } fn parse_url_string(&mut self) -> SassResult { // todo: real uri parsing self.parse_string() } fn use_namespace( &mut self, url: &Path, _start: usize, url_span: Span, ) -> SassResult> { if self.scan_identifier("as", false)? { self.whitespace()?; return Ok(if self.scan_char('*') { None } else { Some(self.parse_identifier(false, false)?) }); } let base_name = url .file_name() .map_or_else(OsString::new, ToOwned::to_owned); let base_name = base_name.to_string_lossy(); let dot = base_name.find('.'); let start = if base_name.starts_with('_') { 1 } else { 0 }; let end = dot.unwrap_or(base_name.len()); let namespace = if url.to_string_lossy().starts_with("sass:") { return Ok(Some(url.to_string_lossy().into_owned())); } else { &base_name[start..end] }; let mut toks = Lexer::new_from_string(namespace, url_span); // if namespace is empty, avoid attempting to parse an identifier from // an empty string, as there will be no span to emit let identifier = if namespace.is_empty() { Err(("", self.empty_span()).into()) } else { mem::swap(self.toks_mut(), &mut toks); let ident = self.parse_identifier(false, false); mem::swap(self.toks_mut(), &mut toks); ident }; match (identifier, toks.peek().is_none()) { (Ok(i), true) => Ok(Some(i)), _ => { Err(( format!( "The default namespace \"{namespace}\" is not a valid Sass identifier.\n\nRecommendation: add an \"as\" clause to define an explicit namespace.", namespace = namespace ), self.toks_mut().span_from(start) ).into()) } } } fn parse_configuration( &mut self, // default=false allow_guarded: bool, ) -> SassResult>> { if !self.scan_identifier("with", false)? { return Ok(None); } let mut variable_names = HashSet::new(); let mut configuration = Vec::new(); self.whitespace()?; self.expect_char('(')?; loop { self.whitespace()?; let var_start = self.toks().cursor(); let name = Identifier::from(self.parse_variable_name()?); let name_span = self.toks_mut().span_from(var_start); self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let expr = self.parse_expression_until_comma(false)?; let mut is_guarded = false; let flag_start = self.toks().cursor(); if allow_guarded && self.scan_char('!') { let flag = self.parse_identifier(false, false)?; if flag == "default" { is_guarded = true; self.whitespace()?; } else { return Err( ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), ); } } let span = self.toks_mut().span_from(var_start); if variable_names.contains(&name) { return Err(("The same variable may only be configured once.", span).into()); } variable_names.insert(name); configuration.push(ConfiguredVariable { name: Spanned { node: name, span: name_span, }, expr, is_guarded, }); if !self.scan_char(',') { break; } self.whitespace()?; if !self.looking_at_expression() { break; } } self.expect_char(')')?; Ok(Some(configuration)) } fn parse_use_rule(&mut self, start: usize) -> SassResult { let url_start = self.toks().cursor(); let url = self.parse_url_string()?; let url_span = self.toks().span_from(url_start); self.whitespace()?; let path = PathBuf::from(url); let namespace = self.use_namespace(path.as_ref(), start, url_span)?; self.whitespace()?; let configuration = self.parse_configuration(false)?; self.expect_statement_separator(Some("@use rule"))?; let span = self.toks_mut().span_from(start); if !self.flags().is_use_allowed() { return Err(( "@use rules must be written before any other rules.", self.toks_mut().span_from(start), ) .into()); } self.expect_statement_separator(Some("@use rule"))?; Ok(AstStmt::Use(AstUseRule { url: path, namespace, configuration: configuration.unwrap_or_default(), span, })) } fn parse_at_rule( &mut self, child: fn(&mut Self) -> SassResult, ) -> SassResult { let start = self.toks().cursor(); self.expect_char('@')?; let name = self.parse_interpolated_identifier()?; self.whitespace()?; // We want to set [_isUseAllowed] to `false` *unless* we're parsing // `@charset`, `@forward`, or `@use`. To avoid double-comparing the rule // name, we always set it to `false` and then set it back to its previous // value if we're parsing an allowed rule. let was_use_allowed = self.flags().is_use_allowed(); self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); match name.as_plain() { Some("at-root") => self.parse_at_root_rule(start), Some("content") => self.parse_content_rule(start), Some("debug") => self.parse_debug_rule(), Some("each") => self.parse_each_rule(child), Some("else") | Some("return") => self.parse_disallowed_at_rule(start), Some("error") => self.parse_error_rule(), Some("extend") => self.parse_extend_rule(start), Some("for") => self.parse_for_rule(child), Some("forward") => { self.flags_mut() .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); // if (!root) { // _disallowedAtRule(); // } self.parse_forward_rule(start) } Some("function") => self.parse_function_rule(start), Some("if") => self.parse_if_rule(child), Some("import") => self.parse_import_rule(start), Some("include") => self.parse_include_rule(), Some("media") => self.parse_media_rule(start), Some("mixin") => self.parse_mixin_rule(start), // todo: support -moz-document // Some("-moz-document") => self.parse_moz_document_rule(name), Some("supports") => self.parse_supports_rule(), Some("use") => { self.flags_mut() .set(ContextFlags::IS_USE_ALLOWED, was_use_allowed); // if (!root) { // _disallowedAtRule(); // } self.parse_use_rule(start) } Some("warn") => self.parse_warn_rule(), Some("while") => self.parse_while_rule(child), Some(..) | None => self.unknown_at_rule(name, start), } } fn parse_statement(&mut self) -> SassResult { match self.toks().peek() { Some(Token { kind: '@', .. }) => self.parse_at_rule(Self::parse_statement), Some(Token { kind: '+', .. }) => { if !self.is_indented() { return self.parse_style_rule(None, None); } let start = self.toks().cursor(); self.toks_mut().next(); if !self.looking_at_identifier() { self.toks_mut().set_cursor(start); return self.parse_style_rule(None, None); } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); self.parse_include_rule() } Some(Token { kind: '=', .. }) => { if !self.is_indented() { return self.parse_style_rule(None, None); } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); let start = self.toks().cursor(); self.toks_mut().next(); self.whitespace()?; self.parse_mixin_rule(start) } Some(Token { kind: '}', .. }) => { Err(("unmatched \"}\".", self.toks().current_span()).into()) } _ => { if self.flags().in_style_rule() || self.flags().in_unknown_at_rule() || self.flags().in_mixin() || self.flags().in_content_block() { self.parse_declaration_or_style_rule() } else { self.parse_variable_declaration_or_style_rule() } } } } fn parse_declaration_or_style_rule(&mut self) -> SassResult { let start = self.toks().cursor(); if self.is_plain_css() && self.flags().in_style_rule() && !self.flags().in_unknown_at_rule() { return self.parse_property_or_variable_declaration(true); } // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. if self.is_indented() && self.scan_char('\\') { return self.parse_style_rule(None, None); }; match self.parse_declaration_or_buffer()? { DeclarationOrBuffer::Stmt(s) => Ok(s), DeclarationOrBuffer::Buffer(existing_buffer) => { self.parse_style_rule(Some(existing_buffer), Some(start)) } } } fn parse_property_or_variable_declaration( &mut self, // default=true parse_custom_properties: bool, ) -> SassResult { let start = self.toks().cursor(); let name = if matches!( self.toks().peek(), Some(Token { kind: ':' | '*' | '.', .. }) ) || (matches!(self.toks().peek(), Some(Token { kind: '#', .. })) && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) { // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" // hacks. let mut name_buffer = Interpolation::new(); name_buffer.add_char(self.toks_mut().next().unwrap().kind); name_buffer.add_string(self.raw_text(Self::whitespace)); name_buffer.add_interpolation(self.parse_interpolated_identifier()?); name_buffer } else if !self.is_plain_css() { match self.parse_variable_declaration_or_interpolation()? { VariableDeclOrInterpolation::Interpolation(interpolation) => interpolation, VariableDeclOrInterpolation::VariableDecl(decl) => { return Ok(AstStmt::VariableDecl(decl)) } } } else { self.parse_interpolated_identifier()? }; self.whitespace()?; self.expect_char(':')?; if parse_custom_properties && name.initial_plain().starts_with("--") { let interpolation = self.parse_interpolated_declaration_value(false, false, true)?; let value_span = self.toks_mut().span_from(start); let value = AstExpr::String(StringExpr(interpolation, QuoteKind::None), value_span) .span(value_span); self.expect_statement_separator(Some("custom property"))?; return Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: Vec::new(), span: value_span, })); } self.whitespace()?; if self.looking_at_children()? { if self.is_plain_css() { return Err(( "Nested declarations aren't allowed in plain CSS.", self.toks().current_span(), ) .into()); } if name.initial_plain().starts_with("--") { return Err(( "Declarations whose names begin with \"--\" may not be nested", self.toks_mut().span_from(start), ) .into()); } let children = self.with_children(Self::parse_declaration_child)?.node; return Ok(AstStmt::Style(AstStyle { name, value: None, body: children, span: self.toks_mut().span_from(start), })); } let value = self.parse_expression(None, None, None)?; if self.looking_at_children()? { if self.is_plain_css() { return Err(( "Nested declarations aren't allowed in plain CSS.", self.toks().current_span(), ) .into()); } if name.initial_plain().starts_with("--") && !matches!(value.node, AstExpr::String(..)) { return Err(( "Declarations whose names begin with \"--\" may not be nested", self.toks_mut().span_from(start), ) .into()); } let children = self.with_children(Self::parse_declaration_child)?.node; Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: children, span: self.toks_mut().span_from(start), })) } else { self.expect_statement_separator(None)?; Ok(AstStmt::Style(AstStyle { name, value: Some(value), body: Vec::new(), span: self.toks_mut().span_from(start), })) } } fn parse_single_interpolation(&mut self) -> SassResult { self.expect_char('#')?; self.expect_char('{')?; self.whitespace()?; let contents = self.parse_expression(None, None, None)?; self.expect_char('}')?; if self.is_plain_css() { return Err(("Interpolation isn't allowed in plain CSS.", contents.span).into()); } let mut interpolation = Interpolation::new(); interpolation .contents .push(InterpolationPart::Expr(contents)); Ok(interpolation) } fn parse_interpolated_identifier_body(&mut self, buffer: &mut Interpolation) -> SassResult<()> { while let Some(next) = self.toks().peek() { match next.kind { 'a'..='z' | 'A'..='Z' | '0'..='9' | '_' | '-' | '\u{80}'..=std::char::MAX => { buffer.add_char(next.kind); self.toks_mut().next(); } '\\' => { buffer.add_string(self.parse_escape(false)?); } '#' if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { buffer.add_interpolation(self.parse_single_interpolation()?); } _ => break, } } Ok(()) } fn parse_interpolated_identifier(&mut self) -> SassResult { let mut buffer = Interpolation::new(); if self.scan_char('-') { buffer.add_char('-'); if self.scan_char('-') { buffer.add_char('-'); self.parse_interpolated_identifier_body(&mut buffer)?; return Ok(buffer); } } match self.toks().peek() { Some(tok) if is_name_start(tok.kind) => { buffer.add_char(tok.kind); self.toks_mut().next(); } Some(Token { kind: '\\', .. }) => { buffer.add_string(self.parse_escape(true)?); } Some(Token { kind: '#', .. }) if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { buffer.add_interpolation(self.parse_single_interpolation()?); } Some(..) | None => { return Err(("Expected identifier.", self.toks().current_span()).into()) } } self.parse_interpolated_identifier_body(&mut buffer)?; Ok(buffer) } fn looking_at_interpolated_identifier(&mut self) -> bool { let first = match self.toks().peek() { Some(Token { kind: '\\', .. }) => return true, Some(Token { kind: '#', .. }) => { return matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) } Some(Token { kind, .. }) if is_name_start(kind) => return true, Some(tok) => tok, None => return false, }; if first.kind != '-' { return false; } match self.toks().peek_n(1) { Some(Token { kind: '#', .. }) => { matches!(self.toks().peek_n(2), Some(Token { kind: '{', .. })) } Some(Token { kind: '\\' | '-', .. }) => true, Some(Token { kind, .. }) => is_name_start(kind), None => false, } } fn parse_loud_comment(&mut self) -> SassResult { let start = self.toks().cursor(); self.expect_char('/')?; self.expect_char('*')?; let mut buffer = Interpolation::new_plain("/*".to_owned()); while let Some(tok) = self.toks().peek() { match tok.kind { '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { buffer.add_interpolation(self.parse_single_interpolation()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } } '*' => { self.toks_mut().next(); buffer.add_char(tok.kind); if self.scan_char('/') { buffer.add_char('/'); return Ok(AstLoudComment { text: buffer, span: self.toks_mut().span_from(start), }); } } '\r' => { self.toks_mut().next(); // todo: does \r even exist at this point? (removed by lexer) if !self.toks_mut().next_char_is('\n') { buffer.add_char('\n'); } } _ => { buffer.add_char(tok.kind); self.toks_mut().next(); } } } Err(("expected more input.", self.toks().current_span()).into()) } fn parse_interpolated_declaration_value( &mut self, // default=false allow_semicolon: bool, // default=false allow_empty: bool, // default=true allow_colon: bool, ) -> SassResult { let mut buffer = Interpolation::new(); let mut brackets = Vec::new(); let mut wrote_newline = false; while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { buffer.add_string(self.parse_escape(true)?); wrote_newline = false; } '"' | '\'' => { buffer.add_interpolation( self.parse_interpolated_string()? .node .as_interpolation(false), ); wrote_newline = false; } '/' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '*', .. })) { let comment = self.fallible_raw_text(Self::skip_loud_comment)?; buffer.add_string(comment); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } wrote_newline = false; } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { // Add a full interpolated identifier to handle cases like // "#{...}--1", since "--1" isn't a valid identifier on its own. buffer.add_interpolation(self.parse_interpolated_identifier()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } wrote_newline = false; } ' ' | '\t' => { if wrote_newline || !matches!( self.toks().peek_n(1), Some(Token { kind: ' ' | '\r' | '\t' | '\n', .. }) ) { self.toks_mut().next(); buffer.add_char(tok.kind); } else { self.toks_mut().next(); } } '\n' | '\r' => { if self.is_indented() { break; } if !matches!( self.toks().peek_n_backwards(1), Some(Token { kind: '\r' | '\n', .. }) ) { buffer.add_char('\n'); } self.toks_mut().next(); wrote_newline = true; } '(' | '{' | '[' => { self.toks_mut().next(); buffer.add_char(tok.kind); brackets.push(opposite_bracket(tok.kind)); wrote_newline = false; } ')' | '}' | ']' => { if brackets.is_empty() { break; } buffer.add_char(tok.kind); self.expect_char(brackets.pop().unwrap())?; wrote_newline = false; } ';' => { if !allow_semicolon && brackets.is_empty() { break; } buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; } ':' => { if !allow_colon && brackets.is_empty() { break; } buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; } 'u' | 'U' => { let before_url = self.toks().cursor(); if !self.scan_identifier("url", false)? { buffer.add_char(tok.kind); self.toks_mut().next(); wrote_newline = false; continue; } match self.try_url_contents(None)? { Some(contents) => { buffer.add_interpolation(contents); } None => { self.toks_mut().set_cursor(before_url); buffer.add_char(tok.kind); self.toks_mut().next(); } } wrote_newline = false; } _ => { if self.looking_at_identifier() { buffer.add_string(self.parse_identifier(false, false)?); } else { buffer.add_char(tok.kind); self.toks_mut().next(); } wrote_newline = false; } } } if let Some(&last) = brackets.last() { self.expect_char(last)?; } if !allow_empty && buffer.contents.is_empty() { return Err(("Expected token.", self.toks().current_span()).into()); } Ok(buffer) } fn parse_expression_until_comma( &mut self, // default=false single_equals: bool, ) -> SassResult> { ValueParser::parse_expression( self, Some(&|parser| { Ok(matches!( parser.toks().peek(), Some(Token { kind: ',', .. }) )) }), false, single_equals, ) } fn parse_argument_invocation( &mut self, for_mixin: bool, allow_empty_second_arg: bool, ) -> SassResult { let start = self.toks().cursor(); self.expect_char('(')?; self.whitespace()?; let mut positional = Vec::new(); let mut named = BTreeMap::new(); let mut rest: Option = None; let mut keyword_rest: Option = None; while self.looking_at_expression() { let expression = self.parse_expression_until_comma(!for_mixin)?; self.whitespace()?; if expression.node.is_variable() && self.scan_char(':') { let name = match expression.node { AstExpr::Variable { name, .. } => name, _ => unreachable!(), }; self.whitespace()?; if named.contains_key(&name.node) { return Err(("Duplicate argument.", name.span).into()); } named.insert( name.node, self.parse_expression_until_comma(!for_mixin)?.node, ); } else if self.scan_char('.') { self.expect_char('.')?; self.expect_char('.')?; if rest.is_none() { rest = Some(expression.node); } else { keyword_rest = Some(expression.node); self.whitespace()?; break; } } else if !named.is_empty() { return Err(( "Positional arguments must come before keyword arguments.", expression.span, ) .into()); } else { positional.push(expression.node); } self.whitespace()?; if !self.scan_char(',') { break; } self.whitespace()?; if allow_empty_second_arg && positional.len() == 1 && named.is_empty() && rest.is_none() && matches!(self.toks().peek(), Some(Token { kind: ')', .. })) { positional.push(AstExpr::String( StringExpr(Interpolation::new(), QuoteKind::None), self.toks().current_span(), )); break; } } self.expect_char(')')?; Ok(ArgumentInvocation { positional, named, rest, keyword_rest, span: self.toks_mut().span_from(start), }) } fn parse_expression( &mut self, parse_until: Option>, inside_bracketed_list: Option, single_equals: Option, ) -> SassResult> { ValueParser::parse_expression( self, parse_until, inside_bracketed_list.unwrap_or(false), single_equals.unwrap_or(false), ) } fn parse_declaration_or_buffer(&mut self) -> SassResult { let start = self.toks().cursor(); let mut name_buffer = Interpolation::new(); // Allow the "*prop: val", ":prop: val", "#prop: val", and ".prop: val" // hacks. let first = self.toks().peek(); let mut starts_with_punctuation = false; if matches!( first, Some(Token { kind: ':' | '*' | '.', .. }) ) || (matches!(first, Some(Token { kind: '#', .. })) && !matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. }))) { starts_with_punctuation = true; name_buffer.add_char(self.toks_mut().next().unwrap().kind); name_buffer.add_string(self.raw_text(Self::whitespace)); } if !self.looking_at_interpolated_identifier() { return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } let variable_or_interpolation = if starts_with_punctuation { VariableDeclOrInterpolation::Interpolation(self.parse_interpolated_identifier()?) } else { self.parse_variable_declaration_or_interpolation()? }; match variable_or_interpolation { VariableDeclOrInterpolation::Interpolation(int) => name_buffer.add_interpolation(int), VariableDeclOrInterpolation::VariableDecl(v) => { return Ok(DeclarationOrBuffer::Stmt(AstStmt::VariableDecl(v))) } } self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); if self.next_matches("/*") { name_buffer.add_string(self.fallible_raw_text(Self::skip_loud_comment)?); } let mut mid_buffer = String::new(); mid_buffer.push_str(&self.raw_text(Self::whitespace)); if !self.scan_char(':') { if !mid_buffer.is_empty() { name_buffer.add_char(' '); } return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } mid_buffer.push(':'); // Parse custom properties as declarations no matter what. if name_buffer.initial_plain().starts_with("--") { let value_start = self.toks().cursor(); let value = self.parse_interpolated_declaration_value(false, false, true)?; let value_span = self.toks_mut().span_from(value_start); self.expect_statement_separator(Some("custom property"))?; return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some( AstExpr::String(StringExpr(value, QuoteKind::None), value_span) .span(value_span), ), span: self.toks_mut().span_from(start), body: Vec::new(), }))); } if self.scan_char(':') { name_buffer.add_string(mid_buffer); name_buffer.add_char(':'); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } else if self.is_indented() && self.looking_at_interpolated_identifier() { // In the indented syntax, `foo:bar` is always considered a selector // rather than a property. name_buffer.add_string(mid_buffer); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } let post_colon_whitespace = self.raw_text(Self::whitespace); if self.looking_at_children()? { let body = self.with_children(Self::parse_declaration_child)?.node; return Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: None, span: self.toks_mut().span_from(start), body, }))); } mid_buffer.push_str(&post_colon_whitespace); let could_be_selector = post_colon_whitespace.is_empty() && self.looking_at_interpolated_identifier(); let before_decl = self.toks().cursor(); let mut calculate_value = || { let value = self.parse_expression(None, None, None)?; if self.looking_at_children()? { if could_be_selector { self.expect_statement_separator(None)?; } } else if !self.at_end_of_statement() { self.expect_statement_separator(None)?; } Ok(value) }; let value = match calculate_value() { Ok(v) => v, Err(e) => { if !could_be_selector { return Err(e); } self.toks_mut().set_cursor(before_decl); let additional = self.almost_any_value(false)?; if !self.is_indented() && self.toks_mut().next_char_is(';') { return Err(e); } name_buffer.add_string(mid_buffer); name_buffer.add_interpolation(additional); return Ok(DeclarationOrBuffer::Buffer(name_buffer)); } }; if self.looking_at_children()? { let body = self.with_children(Self::parse_declaration_child)?.node; Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some(value), span: self.toks_mut().span_from(start), body, }))) } else { self.expect_statement_separator(None)?; Ok(DeclarationOrBuffer::Stmt(AstStmt::Style(AstStyle { name: name_buffer, value: Some(value), span: self.toks_mut().span_from(start), body: Vec::new(), }))) } } fn parse_declaration_child(&mut self) -> SassResult { let start = self.toks().cursor(); if self.toks_mut().next_char_is('@') { self.parse_declaration_at_rule(start) } else { self.parse_property_or_variable_declaration(false) } } fn parse_plain_at_rule_name(&mut self) -> SassResult { self.expect_char('@')?; let name = self.parse_identifier(false, false)?; self.whitespace()?; Ok(name) } fn parse_declaration_at_rule(&mut self, start: usize) -> SassResult { let name = self.parse_plain_at_rule_name()?; match name.as_str() { "content" => self.parse_content_rule(start), "debug" => self.parse_debug_rule(), "each" => self.parse_each_rule(Self::parse_declaration_child), "else" => self.parse_disallowed_at_rule(start), "error" => self.parse_error_rule(), "for" => self.parse_for_rule(Self::parse_declaration_child), "if" => self.parse_if_rule(Self::parse_declaration_child), "include" => self.parse_include_rule(), "warn" => self.parse_warn_rule(), "while" => self.parse_while_rule(Self::parse_declaration_child), _ => self.parse_disallowed_at_rule(start), } } fn parse_variable_declaration_or_style_rule(&mut self) -> SassResult { let start = self.toks().cursor(); if self.is_plain_css() { return self.parse_style_rule(None, None); } // The indented syntax allows a single backslash to distinguish a style rule // from old-style property syntax. We don't support old property syntax, but // we do support the backslash because it's easy to do. if self.is_indented() && self.scan_char('\\') { return self.parse_style_rule(None, None); }; if !self.looking_at_identifier() { return self.parse_style_rule(None, None); } match self.parse_variable_declaration_or_interpolation()? { VariableDeclOrInterpolation::VariableDecl(var) => Ok(AstStmt::VariableDecl(var)), VariableDeclOrInterpolation::Interpolation(int) => { self.parse_style_rule(Some(int), Some(start)) } } } fn parse_style_rule( &mut self, existing_buffer: Option, start: Option, ) -> SassResult { let start = start.unwrap_or_else(|| self.toks().cursor()); self.flags_mut().set(ContextFlags::IS_USE_ALLOWED, false); let mut interpolation = self.parse_style_rule_selector()?; if let Some(mut existing_buffer) = existing_buffer { existing_buffer.add_interpolation(interpolation); interpolation = existing_buffer; } if interpolation.contents.is_empty() { return Err(("expected \"}\".", self.toks().current_span()).into()); } let was_in_style_rule = self.flags().in_style_rule(); *self.flags_mut() |= ContextFlags::IN_STYLE_RULE; let selector_span = self.toks_mut().span_from(start); let children = self.with_children(Self::parse_statement)?; self.flags_mut() .set(ContextFlags::IN_STYLE_RULE, was_in_style_rule); let span = selector_span.merge(children.span); Ok(AstStmt::RuleSet(AstRuleSet { selector: interpolation, body: children.node, selector_span, span, })) } fn parse_silent_comment(&mut self) -> SassResult { let start = self.toks().cursor(); debug_assert!(self.next_matches("//")); self.toks_mut().next(); self.toks_mut().next(); let mut buffer = String::new(); while let Some(tok) = self.toks_mut().next() { if tok.kind == '\n' { self.whitespace_without_comments(); if self.next_matches("//") { self.toks_mut().next(); self.toks_mut().next(); buffer.clear(); continue; } break; } buffer.push(tok.kind); } if self.is_plain_css() { return Err(( "Silent comments aren't allowed in plain CSS.", self.toks_mut().span_from(start), ) .into()); } self.whitespace_without_comments(); Ok(AstStmt::SilentComment(AstSilentComment { text: buffer, span: self.toks_mut().span_from(start), })) } fn next_is_hex(&self) -> bool { match self.toks().peek() { Some(Token { kind, .. }) => kind.is_ascii_hexdigit(), None => false, } } fn assert_public(ident: &str, span: Span) -> SassResult<()> { if !ScssParser::is_private(ident) { return Ok(()); } Err(( "Private members can't be accessed from outside their modules.", span, ) .into()) } fn is_private(ident: &str) -> bool { ident.starts_with('-') || ident.starts_with('_') } fn parse_variable_declaration_without_namespace( &mut self, namespace: Option>, start: Option, ) -> SassResult { let start = start.unwrap_or_else(|| self.toks().cursor()); let name = self.parse_variable_name()?; if namespace.is_some() { Self::assert_public(&name, self.toks_mut().span_from(start))?; } if self.is_plain_css() { return Err(( "Sass variables aren't allowed in plain CSS.", self.toks_mut().span_from(start), ) .into()); } self.whitespace()?; self.expect_char(':')?; self.whitespace()?; let value = self.parse_expression(None, None, None)?.node; let mut is_guarded = false; let mut is_global = false; while self.scan_char('!') { let flag_start = self.toks().cursor(); let flag = self.parse_identifier(false, false)?; match flag.as_str() { "default" => is_guarded = true, "global" => { if namespace.is_some() { return Err(( "!global isn't allowed for variables in other modules.", self.toks_mut().span_from(flag_start), ) .into()); } is_global = true; } _ => { return Err( ("Invalid flag name.", self.toks_mut().span_from(flag_start)).into(), ) } } self.whitespace()?; } self.expect_statement_separator(Some("variable declaration"))?; let declaration = AstVariableDecl { namespace, name: Identifier::from(name), value, is_guarded, is_global, span: self.toks_mut().span_from(start), }; if is_global { // todo // _globalVariables.putIfAbsent(name, () => declaration) } Ok(declaration) } fn almost_any_value( &mut self, // default=false omit_comments: bool, ) -> SassResult { let mut buffer = Interpolation::new(); while let Some(tok) = self.toks().peek() { match tok.kind { '\\' => { // Write a literal backslash because this text will be re-parsed. buffer.add_char(tok.kind); self.toks_mut().next(); match self.toks_mut().next() { Some(tok) => buffer.add_char(tok.kind), None => { return Err(("expected more input.", self.toks().current_span()).into()) } } } '"' | '\'' => { buffer.add_interpolation( self.parse_interpolated_string()? .node .as_interpolation(false), ); } '/' => { let comment_start = self.toks().cursor(); if self.scan_comment()? { if !omit_comments { buffer.add_string(self.toks().raw_text(comment_start)); } } else { buffer.add_char(self.toks_mut().next().unwrap().kind); } } '#' => { if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) { // Add a full interpolated identifier to handle cases like // "#{...}--1", since "--1" isn't a valid identifier on its own. buffer.add_interpolation(self.parse_interpolated_identifier()?); } else { self.toks_mut().next(); buffer.add_char(tok.kind); } } '\r' | '\n' => { if self.is_indented() { break; } buffer.add_char(self.toks_mut().next().unwrap().kind); } '!' | ';' | '{' | '}' => break, 'u' | 'U' => { let before_url = self.toks().cursor(); if !self.scan_identifier("url", false)? { self.toks_mut().next(); buffer.add_char(tok.kind); continue; } match self.try_url_contents(None)? { Some(contents) => buffer.add_interpolation(contents), None => { self.toks_mut().set_cursor(before_url); self.toks_mut().next(); buffer.add_char(tok.kind); } } } _ => { if self.looking_at_identifier() { buffer.add_string(self.parse_identifier(false, false)?); } else { buffer.add_char(self.toks_mut().next().unwrap().kind); } } } } Ok(buffer) } fn parse_variable_declaration_or_interpolation( &mut self, ) -> SassResult { if !self.looking_at_identifier() { return Ok(VariableDeclOrInterpolation::Interpolation( self.parse_interpolated_identifier()?, )); } let start = self.toks().cursor(); let ident = self.parse_identifier(false, false)?; if self.next_matches(".$") { let namespace_span = self.toks_mut().span_from(start); self.expect_char('.')?; Ok(VariableDeclOrInterpolation::VariableDecl( self.parse_variable_declaration_without_namespace( Some(Spanned { node: Identifier::from(ident), span: namespace_span, }), Some(start), )?, )) } else { let mut buffer = Interpolation::new_plain(ident); if self.looking_at_interpolated_identifier_body() { buffer.add_interpolation(self.parse_interpolated_identifier()?); } Ok(VariableDeclOrInterpolation::Interpolation(buffer)) } } fn looking_at_interpolated_identifier_body(&mut self) -> bool { match self.toks().peek() { Some(Token { kind: '\\', .. }) => true, Some(Token { kind: '#', .. }) if matches!(self.toks().peek_n(1), Some(Token { kind: '{', .. })) => { true } Some(Token { kind, .. }) if is_name(kind) => true, Some(..) | None => false, } } fn expression_until_comparison(&mut self) -> SassResult> { let value = self.parse_expression( Some(&|parser| { Ok(match parser.toks().peek() { Some(Token { kind: '>', .. }) | Some(Token { kind: '<', .. }) => true, Some(Token { kind: '=', .. }) => { !matches!(parser.toks().peek_n(1), Some(Token { kind: '=', .. })) } _ => false, }) }), None, None, )?; Ok(value) } fn parse_media_query_list(&mut self) -> SassResult { let mut buf = Interpolation::new(); loop { self.whitespace()?; self.parse_media_query(&mut buf)?; self.whitespace()?; if !self.scan_char(',') { break; } buf.add_char(','); buf.add_char(' '); } Ok(buf) } fn parse_media_in_parens(&mut self, buf: &mut Interpolation) -> SassResult<()> { self.expect_char_with_message('(', "media condition in parentheses")?; buf.add_char('('); self.whitespace()?; if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { self.parse_media_in_parens(buf)?; self.whitespace()?; if self.scan_identifier("and", false)? { buf.add_string(" and ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "and")?; } else if self.scan_identifier("or", false)? { buf.add_string(" or ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "or")?; } } else if self.scan_identifier("not", false)? { buf.add_string("not ".to_owned()); self.expect_whitespace()?; self.parse_media_or_interpolation(buf)?; } else { buf.add_expr(self.expression_until_comparison()?); if self.scan_char(':') { self.whitespace()?; buf.add_char(':'); buf.add_char(' '); buf.add_expr(self.parse_expression(None, None, None)?); } else { let next = self.toks().peek(); if matches!( next, Some(Token { kind: '<' | '>' | '=', .. }) ) { let next = next.unwrap().kind; buf.add_char(' '); buf.add_char(self.toks_mut().next().unwrap().kind); if (next == '<' || next == '>') && self.scan_char('=') { buf.add_char('='); } buf.add_char(' '); self.whitespace()?; buf.add_expr(self.expression_until_comparison()?); if (next == '<' || next == '>') && self.scan_char(next) { buf.add_char(' '); buf.add_char(next); if self.scan_char('=') { buf.add_char('='); } buf.add_char(' '); self.whitespace()?; buf.add_expr(self.expression_until_comparison()?); } } } } self.expect_char(')')?; self.whitespace()?; buf.add_char(')'); Ok(()) } fn parse_media_logic_sequence( &mut self, buf: &mut Interpolation, operator: &'static str, ) -> SassResult<()> { loop { self.parse_media_or_interpolation(buf)?; self.whitespace()?; if !self.scan_identifier(operator, false)? { return Ok(()); } self.expect_whitespace()?; buf.add_char(' '); buf.add_string(operator.to_owned()); buf.add_char(' '); } } fn parse_media_or_interpolation(&mut self, buf: &mut Interpolation) -> SassResult<()> { if self.toks_mut().next_char_is('#') { buf.add_interpolation(self.parse_single_interpolation()?); } else { self.parse_media_in_parens(buf)?; } Ok(()) } fn parse_media_query(&mut self, buf: &mut Interpolation) -> SassResult<()> { if matches!(self.toks().peek(), Some(Token { kind: '(', .. })) { self.parse_media_in_parens(buf)?; self.whitespace()?; if self.scan_identifier("and", false)? { buf.add_string(" and ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "and")?; } else if self.scan_identifier("or", false)? { buf.add_string(" or ".to_owned()); self.expect_whitespace()?; self.parse_media_logic_sequence(buf, "or")?; } return Ok(()); } let ident1 = self.parse_interpolated_identifier()?; if ident1.as_plain().unwrap_or("").to_ascii_lowercase() == "not" { // For example, "@media not (...) {" self.expect_whitespace()?; if !self.looking_at_interpolated_identifier() { buf.add_string("not ".to_owned()); self.parse_media_or_interpolation(buf)?; return Ok(()); } } self.whitespace()?; buf.add_interpolation(ident1); if !self.looking_at_interpolated_identifier() { // For example, "@media screen {". return Ok(()); } buf.add_char(' '); let ident2 = self.parse_interpolated_identifier()?; if ident2.as_plain().unwrap_or("").to_ascii_lowercase() == "and" { self.expect_whitespace()?; // For example, "@media screen and ..." buf.add_string(" and ".to_owned()); } else { self.whitespace()?; buf.add_interpolation(ident2); if self.scan_identifier("and", false)? { // For example, "@media only screen and ..." self.expect_whitespace()?; buf.add_string(" and ".to_owned()); } else { // For example, "@media only screen {" return Ok(()); } } // We've consumed either `IDENTIFIER "and"` or // `IDENTIFIER IDENTIFIER "and"`. if self.scan_identifier("not", false)? { // For example, "@media screen and not (...) {" self.expect_whitespace()?; buf.add_string("not ".to_owned()); self.parse_media_or_interpolation(buf)?; return Ok(()); } self.parse_media_logic_sequence(buf, "and")?; Ok(()) } } grass_compiler-0.13.4/src/parse/value.rs000064400000000000000000001724711046102023000163210ustar 00000000000000use std::{iter::Iterator, marker::PhantomData, sync::Arc}; use codemap::Spanned; use crate::{ ast::*, color::{Color, ColorFormat, NAMED_COLORS}, common::{unvendor, BinaryOp, Brackets, Identifier, ListSeparator, QuoteKind, UnaryOp}, error::SassResult, unit::Unit, utils::{as_hex, opposite_bracket}, value::{CalculationName, Number}, ContextFlags, Token, }; use super::StylesheetParser; pub(crate) type Predicate<'c, P> = &'c dyn Fn(&mut P) -> SassResult; fn is_hex_color(interpolation: &Interpolation) -> bool { if let Some(plain) = interpolation.as_plain() { if ![3, 4, 6, 8].contains(&plain.len()) { return false; } return plain.chars().all(|c| c.is_ascii_hexdigit()); } false } pub(crate) struct ValueParser<'a, 'c, P: StylesheetParser<'a>> { comma_expressions: Option>>, space_expressions: Option>>, binary_operators: Option>, operands: Option>>, allow_slash: bool, single_expression: Option>, start: usize, inside_bracketed_list: bool, single_equals: bool, parse_until: Option>, _a: PhantomData<&'a ()>, } impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { pub fn parse_expression( parser: &mut P, parse_until: Option>, inside_bracketed_list: bool, single_equals: bool, ) -> SassResult> { let start = parser.toks().cursor(); let mut value_parser = Self::new(parser, parse_until, inside_bracketed_list, single_equals); if let Some(parse_until) = value_parser.parse_until { if parse_until(parser)? { return Err(("Expected expression.", parser.toks().current_span()).into()); } } if value_parser.inside_bracketed_list { let bracket_start = parser.toks().cursor(); parser.expect_char('[')?; parser.whitespace()?; if parser.scan_char(']') { return Ok(AstExpr::List(ListExpr { elems: Vec::new(), separator: ListSeparator::Undecided, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(bracket_start))); } }; value_parser.start = parser.toks().cursor(); value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?); let mut value = value_parser.parse_value(parser)?; value.span = parser.toks_mut().span_from(start); Ok(value) } pub fn new( parser: &mut P, parse_until: Option>, inside_bracketed_list: bool, single_equals: bool, ) -> Self { Self { comma_expressions: None, space_expressions: None, binary_operators: None, operands: None, allow_slash: true, start: parser.toks().cursor(), single_expression: None, parse_until, inside_bracketed_list, single_equals, _a: PhantomData, } } /// Parse a value from a stream of tokens /// /// This function will cease parsing if the predicate returns true. pub(crate) fn parse_value(&mut self, parser: &mut P) -> SassResult> { parser.whitespace()?; let start = parser.toks().cursor(); let was_in_parens = parser.flags().in_parens(); loop { parser.whitespace()?; if let Some(parse_until) = self.parse_until { if parse_until(parser)? { break; } } let first = parser.toks().peek(); match first { Some(Token { kind: '(', .. }) => { let expr = self.parse_paren_expr(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '[', .. }) => { let expr = parser.parse_expression(None, Some(true), None)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '$', .. }) => { let expr = Self::parse_variable(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '&', .. }) => { let expr = Self::parse_selector(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => { let expr = parser .parse_interpolated_string()? .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start))); self.add_single_expression(expr, parser)?; } Some(Token { kind: '#', .. }) => { let expr = self.parse_hash(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '=', .. }) => { parser.toks_mut().next(); if self.single_equals && !matches!(parser.toks().peek(), Some(Token { kind: '=', .. })) { self.add_operator( Spanned { node: BinaryOp::SingleEq, span: parser.toks_mut().span_from(start), }, parser, )?; } else { parser.expect_char('=')?; self.add_operator( Spanned { node: BinaryOp::Equal, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '!', .. }) => match parser.toks().peek_n(1) { Some(Token { kind: '=', .. }) => { parser.toks_mut().next(); parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::NotEqual, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind, .. }) if kind.is_ascii_whitespace() || kind == 'i' || kind == 'I' => { let expr = Self::parse_important_expr(parser)?; self.add_single_expression(expr, parser)?; } None => { let expr = Self::parse_important_expr(parser)?; self.add_single_expression(expr, parser)?; } Some(..) => break, }, Some(Token { kind: '<', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: if parser.scan_char('=') { BinaryOp::LessThanEqual } else { BinaryOp::LessThan }, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind: '>', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: if parser.scan_char('=') { BinaryOp::GreaterThanEqual } else { BinaryOp::GreaterThan }, span: parser.toks_mut().span_from(start), }, parser, )?; } Some(Token { kind: '*', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Mul, span: parser.toks().current_span(), }, parser, )?; } Some(Token { kind: '+', .. }) => { if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Plus, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '-', .. }) => { if matches!( parser.toks().peek_n(1), Some(Token { kind: '0'..='9' | '.', .. }) ) && (self.single_expression.is_none() || matches!( parser.toks_mut().peek_previous(), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) )) { let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } else if parser.looking_at_interpolated_identifier() { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } else if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Minus, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '/', .. }) => { if self.single_expression.is_none() { let expr = self.parse_unary_operation(parser)?; self.add_single_expression(expr, parser)?; } else { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Div, span: parser.toks_mut().span_from(start), }, parser, )?; } } Some(Token { kind: '%', .. }) => { parser.toks_mut().next(); self.add_operator( Spanned { node: BinaryOp::Rem, span: parser.toks().current_span(), }, parser, )?; } Some(Token { kind: '0'..='9', .. }) => { let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: '.', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { break; } let expr = ValueParser::parse_number(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: 'a', .. }) => { if !parser.is_plain_css() && parser.scan_identifier("and", false)? { self.add_operator( Spanned { node: BinaryOp::And, span: parser.toks_mut().span_from(start), }, parser, )?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'o', .. }) => { if !parser.is_plain_css() && parser.scan_identifier("or", false)? { self.add_operator( Spanned { node: BinaryOp::Or, span: parser.toks_mut().span_from(start), }, parser, )?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { let expr = Self::parse_unicode_range(parser)?; self.add_single_expression(expr, parser)?; } else { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } } Some(Token { kind: 'b'..='z', .. }) | Some(Token { kind: 'A'..='Z', .. }) | Some(Token { kind: '_', .. }) | Some(Token { kind: '\\', .. }) | Some(Token { kind: '\u{80}'..=std::char::MAX, .. }) => { let expr = self.parse_identifier_like(parser)?; self.add_single_expression(expr, parser)?; } Some(Token { kind: ',', .. }) => { // If we discover we're parsing a list whose first element is a // division operation, and we're in parentheses, reparse outside of a // paren context. This ensures that `(1/2, 1)` doesn't perform division // on its first element. if parser.flags().in_parens() { parser.flags_mut().set(ContextFlags::IN_PARENS, false); if self.allow_slash { self.reset_state(parser)?; continue; } // todo: does this branch ever get hit } if self.single_expression.is_none() { return Err(("Expected expression.", parser.toks().current_span()).into()); } self.resolve_space_expressions(parser)?; // [resolveSpaceExpressions] can modify [singleExpression_], but it // can't set it to null`. self.comma_expressions .get_or_insert_with(Default::default) .push(self.single_expression.take().unwrap()); parser.toks_mut().next(); self.allow_slash = true; } Some(..) | None => break, } } if self.inside_bracketed_list { parser.expect_char(']')?; } if self.comma_expressions.is_some() { self.resolve_space_expressions(parser)?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parens); if let Some(single_expression) = self.single_expression.take() { self.comma_expressions .as_mut() .unwrap() .push(single_expression); } Ok(AstExpr::List(ListExpr { elems: self.comma_expressions.take().unwrap(), separator: ListSeparator::Comma, brackets: if self.inside_bracketed_list { Brackets::Bracketed } else { Brackets::None }, }) .span(parser.toks_mut().span_from(start))) } else if self.inside_bracketed_list && self.space_expressions.is_some() { self.resolve_operations(parser)?; self.space_expressions .as_mut() .unwrap() .push(self.single_expression.take().unwrap()); Ok(AstExpr::List(ListExpr { elems: self.space_expressions.take().unwrap(), separator: ListSeparator::Space, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(start))) } else { self.resolve_space_expressions(parser)?; if self.inside_bracketed_list { return Ok(AstExpr::List(ListExpr { elems: vec![self.single_expression.take().unwrap()], separator: ListSeparator::Undecided, brackets: Brackets::Bracketed, }) .span(parser.toks_mut().span_from(start))); } Ok(self.single_expression.take().unwrap()) } } fn parse_single_expression(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); let first = parser.toks().peek(); match first { Some(Token { kind: '(', .. }) => self.parse_paren_expr(parser), Some(Token { kind: '/', .. }) => self.parse_unary_operation(parser), Some(Token { kind: '[', .. }) => Self::parse_expression(parser, None, true, false), Some(Token { kind: '$', .. }) => Self::parse_variable(parser), Some(Token { kind: '&', .. }) => Self::parse_selector(parser), Some(Token { kind: '"', .. }) | Some(Token { kind: '\'', .. }) => Ok(parser .parse_interpolated_string()? .map_node(|s| AstExpr::String(s, parser.toks_mut().span_from(start)))), Some(Token { kind: '#', .. }) => self.parse_hash(parser), Some(Token { kind: '+', .. }) => self.parse_plus_expr(parser), Some(Token { kind: '-', .. }) => self.parse_minus_expr(parser), Some(Token { kind: '!', .. }) => Self::parse_important_expr(parser), Some(Token { kind: 'u', .. }) | Some(Token { kind: 'U', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '+', .. })) { Self::parse_unicode_range(parser) } else { self.parse_identifier_like(parser) } } Some(Token { kind: '0'..='9', .. }) | Some(Token { kind: '.', .. }) => ValueParser::parse_number(parser), Some(Token { kind: 'a'..='z', .. }) | Some(Token { kind: 'A'..='Z', .. }) | Some(Token { kind: '_', .. }) | Some(Token { kind: '\\', .. }) | Some(Token { kind: '\u{80}'..=std::char::MAX, .. }) => self.parse_identifier_like(parser), Some(..) | None => Err(( "Expected expression.", parser.toks_mut().span_from(self.start), ) .into()), } } fn resolve_one_operation(&mut self, parser: &mut P) -> SassResult<()> { let operator = self.binary_operators.as_mut().unwrap().pop().unwrap(); let operands = self.operands.as_mut().unwrap(); let left = operands.pop().unwrap(); let right = match self.single_expression.take() { Some(val) => val, None => return Err(("Expected expression.", left.span).into()), }; let span = left.span.merge(right.span); if self.allow_slash && !parser.flags().in_parens() && operator == BinaryOp::Div && left.node.is_slash_operand() && right.node.is_slash_operand() { self.single_expression = Some(AstExpr::slash(left.node, right.node, span).span(span)); } else { self.single_expression = Some( AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: left.node, op: operator, rhs: right.node, allows_slash: false, span, })) .span(span), ); self.allow_slash = false; } Ok(()) } fn resolve_operations(&mut self, parser: &mut P) -> SassResult<()> { loop { let should_break = match self.binary_operators.as_ref() { Some(bin) => bin.is_empty(), None => true, }; if should_break { break; } self.resolve_one_operation(parser)?; } Ok(()) } fn add_single_expression( &mut self, expression: Spanned, parser: &mut P, ) -> SassResult<()> { if self.single_expression.is_some() { // If we discover we're parsing a list whose first element is a division // operation, and we're in parentheses, reparse outside of a paren // context. This ensures that `(1/2 1)` doesn't perform division on its // first element. if parser.flags().in_parens() { parser.flags_mut().set(ContextFlags::IN_PARENS, false); if self.allow_slash { self.reset_state(parser)?; return Ok(()); } } if self.space_expressions.is_none() { self.space_expressions = Some(Vec::new()); } self.resolve_operations(parser)?; self.space_expressions .as_mut() .unwrap() .push(self.single_expression.take().unwrap()); self.allow_slash = true; } self.single_expression = Some(expression); Ok(()) } fn add_operator(&mut self, op: Spanned, parser: &mut P) -> SassResult<()> { if parser.is_plain_css() && op.node != BinaryOp::Div && op.node != BinaryOp::SingleEq { return Err(("Operators aren't allowed in plain CSS.", op.span).into()); } self.allow_slash = self.allow_slash && op.node == BinaryOp::Div; if self.binary_operators.is_none() { self.binary_operators = Some(Vec::new()); } if self.operands.is_none() { self.operands = Some(Vec::new()); } while let Some(last_op) = self.binary_operators.as_ref().unwrap_or(&Vec::new()).last() { if last_op.precedence() < op.precedence() { break; } self.resolve_one_operation(parser)?; } self.binary_operators .get_or_insert_with(Default::default) .push(op.node); match self.single_expression.take() { Some(expr) => { self.operands.get_or_insert_with(Vec::new).push(expr); } None => return Err(("Expected expression.", op.span).into()), } parser.whitespace()?; self.single_expression = Some(self.parse_single_expression(parser)?); Ok(()) } fn resolve_space_expressions(&mut self, parser: &mut P) -> SassResult<()> { self.resolve_operations(parser)?; if let Some(mut space_expressions) = self.space_expressions.take() { let single_expression = match self.single_expression.take() { Some(val) => val, None => return Err(("Expected expression.", parser.toks().current_span()).into()), }; let span = single_expression.span; space_expressions.push(single_expression); self.single_expression = Some( AstExpr::List(ListExpr { elems: space_expressions, separator: ListSeparator::Space, brackets: Brackets::None, }) .span(span), ); } Ok(()) } fn parse_map( parser: &mut P, first: Spanned, start: usize, ) -> SassResult> { let mut pairs = vec![(first, parser.parse_expression_until_comma(false)?.node)]; while parser.scan_char(',') { parser.whitespace()?; if !parser.looking_at_expression() { break; } let key = parser.parse_expression_until_comma(false)?; parser.expect_char(':')?; parser.whitespace()?; let value = parser.parse_expression_until_comma(false)?; pairs.push((key, value.node)); } parser.expect_char(')')?; Ok(AstExpr::Map(AstSassMap(pairs)).span(parser.toks_mut().span_from(start))) } fn parse_paren_expr(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); if parser.is_plain_css() { return Err(( "Parentheses aren't allowed in plain CSS.", parser.toks().current_span(), ) .into()); } let was_in_parentheses = parser.flags().in_parens(); parser.flags_mut().set(ContextFlags::IN_PARENS, true); parser.expect_char('(')?; parser.whitespace()?; if !parser.looking_at_expression() { parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Ok(AstExpr::List(ListExpr { elems: Vec::new(), separator: ListSeparator::Undecided, brackets: Brackets::None, }) .span(parser.toks_mut().span_from(start))); } let first = parser.parse_expression_until_comma(false)?; if parser.scan_char(':') { parser.whitespace()?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Self::parse_map(parser, first, start); } if !parser.scan_char(',') { parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); return Ok(AstExpr::Paren(Arc::new(first.node)).span(first.span)); } parser.whitespace()?; let mut expressions = vec![first]; loop { if !parser.looking_at_expression() { break; } expressions.push(parser.parse_expression_until_comma(false)?); if !parser.scan_char(',') { break; } parser.whitespace()?; } parser.expect_char(')')?; parser .flags_mut() .set(ContextFlags::IN_PARENS, was_in_parentheses); Ok(AstExpr::List(ListExpr { elems: expressions, separator: ListSeparator::Comma, brackets: Brackets::None, }) .span(parser.toks_mut().span_from(start))) } fn parse_variable(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); let name = parser.parse_variable_name()?; if parser.is_plain_css() { return Err(( "Sass variables aren't allowed in plain CSS.", parser.toks_mut().span_from(start), ) .into()); } Ok(AstExpr::Variable { name: Spanned { node: Identifier::from(name), span: parser.toks_mut().span_from(start), }, namespace: None, } .span(parser.toks_mut().span_from(start))) } fn parse_selector(parser: &mut P) -> SassResult> { if parser.is_plain_css() { return Err(( "The parent selector isn't allowed in plain CSS.", parser.toks().current_span(), ) .into()); } let start = parser.toks().cursor(); parser.expect_char('&')?; if parser.toks().next_char_is('&') { // todo: emit a warning here // warn( // 'In Sass, "&&" means two copies of the parent selector. You ' // 'probably want to use "and" instead.', // scanner.spanFrom(start)); // scanner.position--; } Ok(AstExpr::ParentSelector.span(parser.toks_mut().span_from(start))) } fn parse_hash(&mut self, parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); debug_assert!(matches!( parser.toks().peek(), Some(Token { kind: '#', .. }) )); if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { return self.parse_identifier_like(parser); } parser.expect_char('#')?; if matches!( parser.toks().peek(), Some(Token { kind: '0'..='9', .. }) ) { let color = self.parse_hex_color_contents(parser)?; return Ok(AstExpr::Color(Arc::new(color)).span(parser.toks_mut().span_from(start))); } let after_hash = parser.toks().cursor(); let ident = parser.parse_interpolated_identifier()?; if is_hex_color(&ident) { parser.toks_mut().set_cursor(after_hash); let color = self.parse_hex_color_contents(parser)?; return Ok( AstExpr::Color(Arc::new(color)).span(parser.toks_mut().span_from(after_hash)) ); } let mut buffer = Interpolation::new(); buffer.add_char('#'); buffer.add_interpolation(ident); let span = parser.toks_mut().span_from(start); Ok(AstExpr::String(StringExpr(buffer, QuoteKind::None), span).span(span)) } fn parse_hex_digit(&mut self, parser: &mut P) -> SassResult { match parser.toks().peek() { Some(Token { kind, .. }) if kind.is_ascii_hexdigit() => { parser.toks_mut().next(); Ok(as_hex(kind)) } _ => Err(("Expected hex digit.", parser.toks().current_span()).into()), } } fn parse_hex_color_contents(&mut self, parser: &mut P) -> SassResult { let start = parser.toks().cursor(); let digit1 = self.parse_hex_digit(parser)?; let digit2 = self.parse_hex_digit(parser)?; let digit3 = self.parse_hex_digit(parser)?; let red: u32; let green: u32; let blue: u32; let mut alpha: f64 = 1.0; if parser.next_is_hex() { let digit4 = self.parse_hex_digit(parser)?; if parser.next_is_hex() { red = (digit1 << 4) + digit2; green = (digit3 << 4) + digit4; blue = (self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?; if parser.next_is_hex() { alpha = ((self.parse_hex_digit(parser)? << 4) + self.parse_hex_digit(parser)?) as f64 / 0xff as f64; } } else { // #abcd red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; blue = (digit3 << 4) + digit3; alpha = ((digit4 << 4) + digit4) as f64 / 0xff as f64; } } else { // #abc red = (digit1 << 4) + digit1; green = (digit2 << 4) + digit2; blue = (digit3 << 4) + digit3; } Ok(Color::new_rgba( Number::from(red), Number::from(green), Number::from(blue), Number(alpha), // todo: // // Don't emit four- or eight-digit hex colors as hex, since that's not // // yet well-supported in browsers. ColorFormat::Literal(parser.toks_mut().raw_text(start - 1)), )) } fn parse_unary_operation(&mut self, parser: &mut P) -> SassResult> { let op_span = parser.toks().current_span(); let operator = Self::expect_unary_operator(parser)?; if parser.is_plain_css() && operator != UnaryOp::Div { return Err(("Operators aren't allowed in plain CSS.", op_span).into()); } parser.whitespace()?; let operand = self.parse_single_expression(parser)?; let span = op_span.merge(parser.toks().current_span()); Ok(AstExpr::UnaryOp(operator, Arc::new(operand.node), span).span(span)) } fn expect_unary_operator(parser: &mut P) -> SassResult { let span = parser.toks().current_span(); Ok(match parser.toks_mut().next() { Some(Token { kind: '+', .. }) => UnaryOp::Plus, Some(Token { kind: '-', .. }) => UnaryOp::Neg, Some(Token { kind: '/', .. }) => UnaryOp::Div, Some(..) | None => return Err(("Expected unary operator.", span).into()), }) } fn consume_natural_number(parser: &mut P) -> SassResult<()> { if !matches!( parser.toks_mut().next(), Some(Token { kind: '0'..='9', .. }) ) { return Err(("Expected digit.", parser.toks().prev_span()).into()); } while matches!( parser.toks().peek(), Some(Token { kind: '0'..='9', .. }) ) { parser.toks_mut().next(); } Ok(()) } fn parse_number(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); if !parser.scan_char('+') { parser.scan_char('-'); } let after_sign = parser.toks().cursor(); if !parser.toks().next_char_is('.') { ValueParser::consume_natural_number(parser)?; } ValueParser::try_decimal(parser, parser.toks().cursor() != after_sign)?; ValueParser::try_exponent(parser)?; let number: f64 = parser.toks_mut().raw_text(start).parse().unwrap(); let unit = if parser.scan_char('%') { Unit::Percent } else if parser.looking_at_identifier() && (!matches!(parser.toks().peek(), Some(Token { kind: '-', .. })) || !matches!(parser.toks().peek_n(1), Some(Token { kind: '-', .. }))) { Unit::from(parser.parse_identifier(false, true)?) } else { Unit::None }; Ok(AstExpr::Number { n: Number::from(number), unit, } .span(parser.toks_mut().span_from(start))) } fn try_decimal(parser: &mut P, allow_trailing_dot: bool) -> SassResult> { if !matches!(parser.toks().peek(), Some(Token { kind: '.', .. })) { return Ok(None); } match parser.toks().peek_n(1) { Some(Token { kind, .. }) if !kind.is_ascii_digit() => { if allow_trailing_dot { return Ok(None); } return Err(("Expected digit.", parser.toks().current_span()).into()); } Some(..) => {} None => return Err(("Expected digit.", parser.toks().current_span()).into()), } let mut buffer = String::new(); parser.expect_char('.')?; buffer.push('.'); while let Some(Token { kind, .. }) = parser.toks().peek() { if !kind.is_ascii_digit() { break; } buffer.push(kind); parser.toks_mut().next(); } Ok(Some(buffer)) } fn try_exponent(parser: &mut P) -> SassResult> { let mut buffer = String::new(); match parser.toks().peek() { Some(Token { kind: 'e' | 'E', .. }) => buffer.push('e'), _ => return Ok(None), } let next = match parser.toks().peek_n(1) { Some(Token { kind: kind @ ('0'..='9' | '-' | '+'), .. }) => kind, _ => return Ok(None), }; parser.toks_mut().next(); if next == '+' || next == '-' { parser.toks_mut().next(); buffer.push(next); } match parser.toks().peek() { Some(Token { kind: '0'..='9', .. }) => {} _ => return Err(("Expected digit.", parser.toks().current_span()).into()), } while let Some(tok) = parser.toks().peek() { if !tok.kind.is_ascii_digit() { break; } buffer.push(tok.kind); parser.toks_mut().next(); } Ok(Some(buffer)) } fn parse_plus_expr(&mut self, parser: &mut P) -> SassResult> { debug_assert!(parser.toks().next_char_is('+')); match parser.toks().peek_n(1) { Some(Token { kind: '0'..='9' | '.', .. }) => ValueParser::parse_number(parser), _ => self.parse_unary_operation(parser), } } fn parse_minus_expr(&mut self, parser: &mut P) -> SassResult> { debug_assert!(parser.toks().next_char_is('-')); if matches!( parser.toks().peek_n(1), Some(Token { kind: '0'..='9' | '.', .. }) ) { return ValueParser::parse_number(parser); } if parser.looking_at_interpolated_identifier() { return self.parse_identifier_like(parser); } self.parse_unary_operation(parser) } fn parse_important_expr(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); parser.expect_char('!')?; parser.whitespace()?; parser.expect_identifier("important", false)?; let span = parser.toks_mut().span_from(start); Ok(AstExpr::String( StringExpr( Interpolation::new_plain("!important".to_owned()), QuoteKind::None, ), span, ) .span(span)) } fn parse_identifier_like(&mut self, parser: &mut P) -> SassResult> { if let Some(func) = P::IDENTIFIER_LIKE { return func(parser); } let start = parser.toks().cursor(); let identifier = parser.parse_interpolated_identifier()?; let ident_span = parser.toks_mut().span_from(start); let plain = identifier.as_plain(); let lower = plain.map(str::to_ascii_lowercase); if let Some(plain) = plain { if plain == "if" && parser.toks().next_char_is('(') { let call_args = parser.parse_argument_invocation(false, false)?; let span = call_args.span; return Ok(AstExpr::If(Arc::new(Ternary(call_args))).span(span)); } else if plain == "not" { parser.whitespace()?; let value = self.parse_single_expression(parser)?; let span = parser.toks_mut().span_from(start); return Ok(AstExpr::UnaryOp(UnaryOp::Not, Arc::new(value.node), span).span(span)); } let lower_ref = lower.as_ref().unwrap(); if !parser.toks().next_char_is('(') { match plain { "null" => return Ok(AstExpr::Null.span(parser.toks_mut().span_from(start))), "true" => return Ok(AstExpr::True.span(parser.toks_mut().span_from(start))), "false" => return Ok(AstExpr::False.span(parser.toks_mut().span_from(start))), _ => {} } if let Some(color) = NAMED_COLORS.get_by_name(lower_ref.as_str()) { return Ok(AstExpr::Color(Arc::new(Color::new( color[0], color[1], color[2], color[3], plain.to_owned(), ))) .span(parser.toks_mut().span_from(start))); } } if let Some(func) = ValueParser::try_parse_special_function(parser, lower_ref, start)? { return Ok(func); } } match parser.toks().peek() { Some(Token { kind: '.', .. }) => { if matches!(parser.toks().peek_n(1), Some(Token { kind: '.', .. })) { return Ok(AstExpr::String( StringExpr(identifier, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start))); } parser.toks_mut().next(); match plain { Some(s) => Self::namespaced_expression( Spanned { node: Identifier::from(s), span: ident_span, }, start, parser, ), None => Err(("Interpolation isn't allowed in namespaces.", ident_span).into()), } } Some(Token { kind: '(', .. }) => { if let Some(plain) = plain { let arguments = parser.parse_argument_invocation(false, lower.as_deref() == Some("var"))?; Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: None, name: Identifier::from(plain), arguments: Arc::new(arguments), span: parser.toks_mut().span_from(start), }) .span(parser.toks_mut().span_from(start))) } else { let arguments = parser.parse_argument_invocation(false, false)?; Ok( AstExpr::InterpolatedFunction(Arc::new(InterpolatedFunction { name: identifier, arguments, span: parser.toks_mut().span_from(start), })) .span(parser.toks_mut().span_from(start)), ) } } _ => Ok(AstExpr::String( StringExpr(identifier, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start))), } } fn namespaced_expression( namespace: Spanned, start: usize, parser: &mut P, ) -> SassResult> { if parser.toks().next_char_is('$') { let name_start = parser.toks().cursor(); let name = parser.parse_variable_name()?; let span = parser.toks_mut().span_from(start); P::assert_public(&name, span)?; if parser.is_plain_css() { return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); } return Ok(AstExpr::Variable { name: Spanned { node: Identifier::from(name), span: parser.toks_mut().span_from(name_start), }, namespace: Some(namespace), } .span(span)); } let name = parser.parse_public_identifier()?; let args = parser.parse_argument_invocation(false, false)?; let span = parser.toks_mut().span_from(start); if parser.is_plain_css() { return Err(("Module namespaces aren't allowed in plain CSS.", span).into()); } Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: Some(namespace), name: Identifier::from(name), arguments: Arc::new(args), span, }) .span(span)) } fn parse_unicode_range(parser: &mut P) -> SassResult> { let start = parser.toks().cursor(); parser.expect_ident_char('u', false)?; parser.expect_char('+')?; let mut first_range_length = 0; while let Some(next) = parser.toks().peek() { if !next.kind.is_ascii_hexdigit() { break; } parser.toks_mut().next(); first_range_length += 1; } let mut has_question_mark = false; while parser.scan_char('?') { has_question_mark = true; first_range_length += 1; } let span = parser.toks_mut().span_from(start); if first_range_length == 0 { return Err(("Expected hex digit or \"?\".", parser.toks().current_span()).into()); } else if first_range_length > 6 { return Err(("Expected at most 6 digits.", span).into()); } else if has_question_mark { return Ok(AstExpr::String( StringExpr( Interpolation::new_plain(parser.toks_mut().raw_text(start)), QuoteKind::None, ), span, ) .span(span)); } if parser.scan_char('-') { let second_range_start = parser.toks().cursor(); let mut second_range_length = 0; while let Some(next) = parser.toks().peek() { if !next.kind.is_ascii_hexdigit() { break; } parser.toks_mut().next(); second_range_length += 1; } if second_range_length == 0 { return Err(("Expected hex digit.", parser.toks().current_span()).into()); } else if second_range_length > 6 { return Err(( "Expected at most 6 digits.", parser.toks_mut().span_from(second_range_start), ) .into()); } } if parser.looking_at_interpolated_identifier_body() { return Err(("Expected end of identifier.", parser.toks().current_span()).into()); } let span = parser.toks_mut().span_from(start); Ok(AstExpr::String( StringExpr( Interpolation::new_plain(parser.toks_mut().raw_text(start)), QuoteKind::None, ), span, ) .span(span)) } pub(crate) fn try_parse_special_function( parser: &mut P, name: &str, start: usize, ) -> SassResult>> { if matches!(parser.toks().peek(), Some(Token { kind: '(', .. })) { if let Some(calculation) = ValueParser::try_parse_calculation(parser, name, start)? { return Ok(Some(calculation)); } } let normalized = unvendor(name); let mut buffer; match normalized { "calc" | "element" | "expression" => { if !parser.scan_char('(') { return Ok(None); } buffer = Interpolation::new_plain(name.to_owned()); buffer.add_char('('); } "progid" => { if !parser.scan_char(':') { return Ok(None); } buffer = Interpolation::new_plain(name.to_owned()); buffer.add_char(':'); while let Some(Token { kind, .. }) = parser.toks().peek() { if !kind.is_alphabetic() && kind != '.' { break; } buffer.add_char(kind); parser.toks_mut().next(); } parser.expect_char('(')?; buffer.add_char('('); } "url" => { return Ok(parser.try_url_contents(None)?.map(|contents| { AstExpr::String( StringExpr(contents, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start)) })) } _ => return Ok(None), } buffer.add_interpolation(parser.parse_interpolated_declaration_value(false, true, true)?); parser.expect_char(')')?; buffer.add_char(')'); Ok(Some( AstExpr::String( StringExpr(buffer, QuoteKind::None), parser.toks_mut().span_from(start), ) .span(parser.toks_mut().span_from(start)), )) } fn contains_calculation_interpolation(parser: &mut P) -> SassResult { let mut parens = 0; let mut brackets = Vec::new(); let start = parser.toks().cursor(); while let Some(next) = parser.toks().peek() { match next.kind { '\\' => { parser.toks_mut().next(); // todo: i wonder if this can be broken (not for us but dart-sass) parser.toks_mut().next(); } '/' => { if !parser.scan_comment()? { parser.toks_mut().next(); } } '\'' | '"' => { parser.parse_interpolated_string()?; } '#' => { if parens == 0 && matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { parser.toks_mut().set_cursor(start); return Ok(true); } parser.toks_mut().next(); } '(' | '{' | '[' => { if next.kind == '(' { parens += 1; } brackets.push(opposite_bracket(next.kind)); parser.toks_mut().next(); } ')' | '}' | ']' => { if next.kind == ')' { parens -= 1; } if brackets.is_empty() || brackets.pop() != Some(next.kind) { parser.toks_mut().set_cursor(start); return Ok(false); } parser.toks_mut().next(); } _ => { parser.toks_mut().next(); } } } parser.toks_mut().set_cursor(start); Ok(false) } fn try_parse_calculation_interpolation( parser: &mut P, start: usize, ) -> SassResult> { Ok( if ValueParser::contains_calculation_interpolation(parser)? { Some(AstExpr::String( StringExpr( parser.parse_interpolated_declaration_value(false, false, true)?, QuoteKind::None, ), parser.toks_mut().span_from(start), )) } else { None }, ) } fn parse_calculation_value(parser: &mut P) -> SassResult> { match parser.toks().peek() { Some(Token { kind: '+' | '-' | '.' | '0'..='9', .. }) => ValueParser::parse_number(parser), Some(Token { kind: '$', .. }) => ValueParser::parse_variable(parser), Some(Token { kind: '(', .. }) => { let start = parser.toks().cursor(); parser.toks_mut().next(); let value = match ValueParser::try_parse_calculation_interpolation(parser, start)? { Some(v) => v, None => { parser.whitespace()?; ValueParser::parse_calculation_sum(parser)?.node } }; parser.whitespace()?; parser.expect_char(')')?; Ok(AstExpr::Paren(Arc::new(value)).span(parser.toks_mut().span_from(start))) } _ if !parser.looking_at_identifier() => Err(( "Expected number, variable, function, or calculation.", parser.toks().current_span(), ) .into()), _ => { let start = parser.toks().cursor(); let ident = parser.parse_identifier(false, false)?; let ident_span = parser.toks_mut().span_from(start); if parser.scan_char('.') { return ValueParser::namespaced_expression( Spanned { node: Identifier::from(&ident), span: ident_span, }, start, parser, ); } if !parser.toks().next_char_is('(') { return Err(("Expected \"(\" or \".\".", parser.toks().current_span()).into()); } let lowercase = ident.to_ascii_lowercase(); let calculation = ValueParser::try_parse_calculation(parser, &lowercase, start)?; if let Some(calc) = calculation { Ok(calc) } else if lowercase == "if" { Ok(AstExpr::If(Arc::new(Ternary( parser.parse_argument_invocation(false, false)?, ))) .span(parser.toks_mut().span_from(start))) } else { Ok(AstExpr::FunctionCall(FunctionCallExpr { namespace: None, name: Identifier::from(ident), arguments: Arc::new(parser.parse_argument_invocation(false, false)?), span: parser.toks_mut().span_from(start), }) .span(parser.toks_mut().span_from(start))) } } } } fn parse_calculation_product(parser: &mut P) -> SassResult> { let mut product = ValueParser::parse_calculation_value(parser)?; loop { parser.whitespace()?; match parser.toks().peek() { Some(Token { kind: op @ ('*' | '/'), .. }) => { parser.toks_mut().next(); parser.whitespace()?; let rhs = ValueParser::parse_calculation_value(parser)?; let span = product.span.merge(rhs.span); product.node = AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: product.node, op: if op == '*' { BinaryOp::Mul } else { BinaryOp::Div }, rhs: rhs.node, allows_slash: false, span, })); product.span = span; } _ => return Ok(product), } } } fn parse_calculation_sum(parser: &mut P) -> SassResult> { let mut sum = ValueParser::parse_calculation_product(parser)?; loop { match parser.toks().peek() { Some(Token { kind: next @ ('+' | '-'), .. }) => { if !matches!( parser.toks().peek_n_backwards(1), Some(Token { kind: ' ' | '\t' | '\r' | '\n', .. }) ) || !matches!( parser.toks().peek_n(1), Some(Token { kind: ' ' | '\t' | '\r' | '\n', .. }) ) { return Err(( "\"+\" and \"-\" must be surrounded by whitespace in calculations.", parser.toks().current_span(), ) .into()); } parser.toks_mut().next(); parser.whitespace()?; let rhs = ValueParser::parse_calculation_product(parser)?; let span = sum.span.merge(rhs.span); sum = AstExpr::BinaryOp(Arc::new(BinaryOpExpr { lhs: sum.node, op: if next == '+' { BinaryOp::Plus } else { BinaryOp::Minus }, rhs: rhs.node, allows_slash: false, span, })) .span(span); } _ => return Ok(sum), } } } fn parse_calculation_arguments( parser: &mut P, max_args: Option, start: usize, ) -> SassResult> { parser.expect_char('(')?; if let Some(interpolation) = ValueParser::try_parse_calculation_interpolation(parser, start)? { parser.expect_char(')')?; return Ok(vec![interpolation]); } parser.whitespace()?; let mut arguments = vec![ValueParser::parse_calculation_sum(parser)?.node]; while (max_args.is_none() || arguments.len() < max_args.unwrap()) && parser.scan_char(',') { parser.whitespace()?; arguments.push(ValueParser::parse_calculation_sum(parser)?.node); } parser.expect_char_with_message( ')', if Some(arguments.len()) == max_args { r#""+", "-", "*", "/", or ")""# } else { r#""+", "-", "*", "/", ",", or ")""# }, )?; Ok(arguments) } fn try_parse_calculation( parser: &mut P, name: &str, start: usize, ) -> SassResult>> { debug_assert!(parser.toks().next_char_is('(')); Ok(Some(match name { "calc" => { let args = ValueParser::parse_calculation_arguments(parser, Some(1), start)?; AstExpr::Calculation { name: CalculationName::Calc, args, } .span(parser.toks_mut().span_from(start)) } "min" | "max" => { // min() and max() are parsed as calculations if possible, and otherwise // are parsed as normal Sass functions. let before_args = parser.toks().cursor(); let args = match ValueParser::parse_calculation_arguments(parser, None, start) { Ok(args) => args, Err(..) => { parser.toks_mut().set_cursor(before_args); return Ok(None); } }; AstExpr::Calculation { name: if name == "min" { CalculationName::Min } else { CalculationName::Max }, args, } .span(parser.toks_mut().span_from(start)) } "clamp" => { let args = ValueParser::parse_calculation_arguments(parser, Some(3), start)?; AstExpr::Calculation { name: CalculationName::Clamp, args, } .span(parser.toks_mut().span_from(start)) } _ => return Ok(None), })) } fn reset_state(&mut self, parser: &mut P) -> SassResult<()> { self.comma_expressions = None; self.space_expressions = None; self.binary_operators = None; self.operands = None; parser.toks_mut().set_cursor(self.start); self.allow_slash = true; self.single_expression = Some(self.parse_single_expression(parser)?); Ok(()) } } grass_compiler-0.13.4/src/selector/attribute.rs000064400000000000000000000165101046102023000177050ustar 00000000000000use std::{ fmt::{self, Display, Write}, hash::{Hash, Hasher}, }; use codemap::Span; use crate::{ common::QuoteKind, error::SassResult, parse::BaseParser, utils::is_ident, value::Value, Token, }; use super::{Namespace, QualifiedName, SelectorParser}; #[derive(Clone, Debug)] pub(crate) struct Attribute { attr: QualifiedName, value: String, modifier: Option, op: AttributeOp, span: Span, } impl PartialEq for Attribute { fn eq(&self, other: &Self) -> bool { self.attr == other.attr && self.value == other.value && self.modifier == other.modifier && self.op == other.op } } impl Eq for Attribute {} impl Hash for Attribute { fn hash(&self, state: &mut H) { self.attr.hash(state); self.value.hash(state); self.modifier.hash(state); self.op.hash(state); } } // todo: rewrite fn attribute_name(parser: &mut SelectorParser) -> SassResult { let next = parser .toks .peek() .ok_or_else(|| ("Expected identifier.", parser.toks.current_span()))?; if next.kind == '*' { parser.toks.next(); parser.expect_char('|')?; let ident = parser.parse_identifier(false, false)?; return Ok(QualifiedName { ident, namespace: Namespace::Asterisk, }); } let name_or_namespace = parser.parse_identifier(false, false)?; match parser.toks.peek() { Some(v) if v.kind != '|' => { return Ok(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }); } Some(..) => {} None => return Err(("expected more input.", parser.toks.current_span()).into()), } match parser.toks.peek_n(1) { Some(v) if v.kind == '=' => { return Ok(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }); } Some(..) => {} None => return Err(("expected more input.", parser.toks.current_span()).into()), } parser.toks.next(); let ident = parser.parse_identifier(false, false)?; Ok(QualifiedName { ident, namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } fn attribute_operator(parser: &mut SelectorParser) -> SassResult { let op = match parser.toks.next() { Some(Token { kind: '=', .. }) => return Ok(AttributeOp::Equals), Some(Token { kind: '~', .. }) => AttributeOp::Include, Some(Token { kind: '|', .. }) => AttributeOp::Dash, Some(Token { kind: '^', .. }) => AttributeOp::Prefix, Some(Token { kind: '$', .. }) => AttributeOp::Suffix, Some(Token { kind: '*', .. }) => AttributeOp::Contains, Some(..) | None => return Err(("Expected \"]\".", parser.toks.current_span()).into()), }; parser.expect_char('=')?; Ok(op) } impl Attribute { pub fn from_tokens(parser: &mut SelectorParser) -> SassResult { let start = parser.toks.cursor(); parser.whitespace_without_comments(); let attr = attribute_name(parser)?; parser.whitespace_without_comments(); if parser .toks .peek() .ok_or_else(|| ("expected more input.", parser.toks.current_span()))? .kind == ']' { parser.toks.next(); return Ok(Attribute { attr, value: String::new(), modifier: None, op: AttributeOp::Any, span: parser.toks.span_from(start), }); } let op = attribute_operator(parser)?; parser.whitespace_without_comments(); let peek = parser .toks .peek() .ok_or_else(|| ("expected more input.", parser.toks.current_span()))?; let value = match peek.kind { '\'' | '"' => parser.parse_string()?, _ => parser.parse_identifier(false, false)?, }; parser.whitespace_without_comments(); let modifier = match parser.toks.peek() { Some(Token { kind: c @ 'a'..='z', .. }) | Some(Token { kind: c @ 'A'..='Z', .. }) => { parser.toks.next(); parser.whitespace_without_comments(); Some(c) } _ => None, }; parser.expect_char(']')?; Ok(Attribute { op, attr, value, modifier, span: parser.toks.span_from(start), }) } } impl Display for Attribute { #[allow(clippy::branches_sharing_code)] fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_char('[')?; write!(f, "{}", self.attr)?; if self.op != AttributeOp::Any { f.write_str(self.op.into())?; if is_ident(&self.value) && !self.value.starts_with("--") { f.write_str(&self.value)?; if self.modifier.is_some() { f.write_char(' ')?; } } else { // todo: remove unwrap by not doing this in display // or having special emitter for quoted strings? // (also avoids the clone because we can consume/modify self) f.write_str( &Value::String(self.value.clone(), QuoteKind::Quoted) .to_css_string(self.span, false) .unwrap(), )?; // todo: this space is not emitted when `compressed` output if self.modifier.is_some() { f.write_char(' ')?; } } if let Some(c) = self.modifier { f.write_char(c)?; } } f.write_char(']')?; Ok(()) } } #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] enum AttributeOp { /// \[attr\] /// /// Represents elements with an attribute name of `attr` Any, /// [attr=value] /// /// Represents elements with an attribute name of `attr` /// whose value is exactly `value` Equals, /// [attr~=value] /// /// Represents elements with an attribute name of `attr` /// whose value is a whitespace-separated list of words, /// one of which is exactly `value` Include, /// [attr|=value] /// /// Represents elements with an attribute name of `attr` /// whose value can be exactly value or can begin with /// `value` immediately followed by a hyphen (`-`) Dash, /// [attr^=value] Prefix, /// [attr$=value] Suffix, /// [attr*=value] /// /// Represents elements with an attribute name of `attr` /// whose value contains at least one occurrence of /// `value` within the string Contains, } impl From for &'static str { #[inline] fn from(op: AttributeOp) -> Self { match op { AttributeOp::Any => "", AttributeOp::Equals => "=", AttributeOp::Include => "~=", AttributeOp::Dash => "|=", AttributeOp::Prefix => "^=", AttributeOp::Suffix => "$=", AttributeOp::Contains => "*=", } } } grass_compiler-0.13.4/src/selector/common.rs000064400000000000000000000024141046102023000171700ustar 00000000000000use std::fmt; /// The selector namespace. /// /// If this is `None`, this matches all elements in the default namespace. If /// it's `Empty`, this matches all elements that aren't in any /// namespace. If it's `Asterisk`, this matches all elements in any namespace. /// Otherwise, it matches all elements in the given namespace. #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum Namespace { Empty, Asterisk, Other(Box), None, } impl fmt::Display for Namespace { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Empty => write!(f, "|"), Self::Asterisk => write!(f, "*|"), Self::Other(namespace) => write!(f, "{}|", namespace), Self::None => Ok(()), } } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct QualifiedName { pub ident: String, pub namespace: Namespace, } impl fmt::Display for QualifiedName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.namespace)?; f.write_str(&self.ident) } } pub(crate) struct Specificity { pub min: i32, pub max: i32, } impl Specificity { pub const fn new(min: i32, max: i32) -> Self { Specificity { min, max } } } grass_compiler-0.13.4/src/selector/complex.rs000064400000000000000000000251621046102023000173540ustar 00000000000000use std::{ collections::HashSet, fmt::{self, Display, Write}, hash::{Hash, Hasher}, sync::atomic::{AtomicU32, Ordering as AtomicOrdering}, }; use codemap::Span; use crate::error::SassResult; use super::{CompoundSelector, Pseudo, SelectorList, SimpleSelector, Specificity}; pub(crate) static COMPLEX_SELECTOR_UNIQUE_ID: AtomicU32 = AtomicU32::new(0); #[derive(Clone, Debug)] pub(crate) struct ComplexSelectorHashSet(HashSet); impl ComplexSelectorHashSet { pub fn new() -> Self { Self(HashSet::new()) } pub fn insert(&mut self, complex: &ComplexSelector) -> bool { self.0.insert(complex.unique_id) } pub fn contains(&self, complex: &ComplexSelector) -> bool { self.0.contains(&complex.unique_id) } pub fn extend<'a>(&mut self, complexes: impl Iterator) { self.0.extend(complexes.map(|complex| complex.unique_id)); } } /// A complex selector. /// /// A complex selector is composed of `CompoundSelector`s separated by /// `Combinator`s. It selects elements based on their parent selectors. #[derive(Clone, Debug)] pub(crate) struct ComplexSelector { /// The components of this selector. /// /// This is never empty. /// /// Descendant combinators aren't explicitly represented here. If two /// `CompoundSelector`s are adjacent to one another, there's an implicit /// descendant combinator between them. /// /// It's possible for multiple `Combinator`s to be adjacent to one another. /// This isn't valid CSS, but Sass supports it for CSS hack purposes. pub components: Vec, /// Whether a line break should be emitted *before* this selector. pub line_break: bool, /// A unique identifier for this complex selector. Used to perform a pointer /// equality check, like would be done for objects in a language like JavaScript /// or dart unique_id: u32, } impl PartialEq for ComplexSelector { fn eq(&self, other: &Self) -> bool { self.components == other.components } } impl Eq for ComplexSelector {} impl Hash for ComplexSelector { fn hash(&self, state: &mut H) { self.components.hash(state); } } impl fmt::Display for ComplexSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut last_component = None; for component in &self.components { if let Some(c) = last_component { if !omit_spaces_around(c) && !omit_spaces_around(component) { f.write_char(' ')?; } } write!(f, "{}", component)?; last_component = Some(component); } Ok(()) } } /// When `style` is `OutputStyle::compressed`, omit spaces around combinators. fn omit_spaces_around(component: &ComplexSelectorComponent) -> bool { // todo: compressed let is_compressed = false; is_compressed && matches!(component, ComplexSelectorComponent::Combinator(..)) } impl ComplexSelector { pub fn new(components: Vec, line_break: bool) -> Self { Self { components, line_break, unique_id: COMPLEX_SELECTOR_UNIQUE_ID.fetch_add(1, AtomicOrdering::Relaxed), } } pub fn max_specificity(&self) -> i32 { self.specificity().min } pub fn min_specificity(&self) -> i32 { self.specificity().max } pub fn specificity(&self) -> Specificity { let mut min = 0; let mut max = 0; for component in &self.components { if let ComplexSelectorComponent::Compound(compound) = component { min += compound.min_specificity(); max += compound.max_specificity(); } } Specificity::new(min, max) } pub fn is_invisible(&self) -> bool { self.components .iter() .any(ComplexSelectorComponent::is_invisible) } /// Returns whether `self` is a superselector of `other`. /// /// That is, whether `self` matches every element that `other` matches, as well /// as possibly additional elements. pub fn is_super_selector(&self, other: &Self) -> bool { if let Some(ComplexSelectorComponent::Combinator(..)) = self.components.last() { return false; } if let Some(ComplexSelectorComponent::Combinator(..)) = other.components.last() { return false; } let mut i1 = 0; let mut i2 = 0; loop { let remaining1 = self.components.len() - i1; let remaining2 = other.components.len() - i2; if remaining1 == 0 || remaining2 == 0 || remaining1 > remaining2 { return false; } let compound1 = match self.components.get(i1) { Some(ComplexSelectorComponent::Compound(c)) => c, Some(ComplexSelectorComponent::Combinator(..)) => return false, None => unreachable!(), }; if let ComplexSelectorComponent::Combinator(..) = other.components[i2] { return false; } if remaining1 == 1 { let parents = other .components .iter() .take(other.components.len() - 1) .skip(i2) .cloned() .collect(); return compound1.is_super_selector( other.components.last().unwrap().as_compound(), &Some(parents), ); } let mut after_super_selector = i2 + 1; while after_super_selector < other.components.len() { if let Some(ComplexSelectorComponent::Compound(compound2)) = other.components.get(after_super_selector - 1) { if compound1.is_super_selector( compound2, &Some( other .components .iter() .take(after_super_selector - 1) .skip(i2 + 1) .cloned() .collect(), ), ) { break; } } after_super_selector += 1; } if after_super_selector == other.components.len() { return false; } if let Some(ComplexSelectorComponent::Combinator(combinator1)) = self.components.get(i1 + 1) { let combinator2 = match other.components.get(after_super_selector) { Some(ComplexSelectorComponent::Combinator(c)) => c, Some(ComplexSelectorComponent::Compound(..)) => return false, None => unreachable!(), }; if combinator1 == &Combinator::FollowingSibling { if combinator2 == &Combinator::Child { return false; } } else if combinator1 != combinator2 { return false; } if remaining1 == 3 && remaining2 > 3 { return false; } i1 += 2; i2 = after_super_selector + 1; } else if let Some(ComplexSelectorComponent::Combinator(combinator2)) = other.components.get(after_super_selector) { if combinator2 != &Combinator::Child { return false; } i1 += 1; i2 = after_super_selector + 1; } else { i1 += 1; i2 = after_super_selector; } } } pub fn contains_parent_selector(&self) -> bool { self.components.iter().any(|c| { if let ComplexSelectorComponent::Compound(compound) = c { compound.components.iter().any(|simple| { if simple.is_parent() { return true; } if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), .. }) = simple { return sel.contains_parent_selector(); } false }) } else { false } }) } } #[derive(Clone, Debug, Eq, PartialEq, Copy, Hash)] pub(crate) enum Combinator { /// Matches the right-hand selector if it's immediately adjacent to the /// left-hand selector in the DOM tree. /// /// `'+'` NextSibling, /// Matches the right-hand selector if it's a direct child of the left-hand /// selector in the DOM tree. /// /// `'>'` Child, /// Matches the right-hand selector if it comes after the left-hand selector /// in the DOM tree. /// /// `'~'` FollowingSibling, } impl Display for Combinator { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_char(match self { Self::NextSibling => '+', Self::Child => '>', Self::FollowingSibling => '~', }) } } #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum ComplexSelectorComponent { Combinator(Combinator), Compound(CompoundSelector), } impl ComplexSelectorComponent { pub fn is_invisible(&self) -> bool { match self { Self::Combinator(..) => false, Self::Compound(c) => c.is_invisible(), } } pub fn is_compound(&self) -> bool { matches!(self, Self::Compound(..)) } pub fn is_combinator(&self) -> bool { matches!(self, Self::Combinator(..)) } pub fn resolve_parent_selectors( self, span: Span, parent: SelectorList, ) -> SassResult>> { match self { Self::Compound(c) => c.resolve_parent_selectors(span, parent), Self::Combinator(..) => todo!(), } } pub fn as_compound(&self) -> &CompoundSelector { match self { Self::Compound(c) => c, Self::Combinator(..) => unreachable!(), } } } impl Display for ComplexSelectorComponent { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Compound(c) => write!(f, "{}", c), Self::Combinator(c) => write!(f, "{}", c), } } } grass_compiler-0.13.4/src/selector/compound.rs000064400000000000000000000174351046102023000175350ustar 00000000000000use std::fmt::{self, Write}; use codemap::Span; use crate::error::SassResult; use super::{ ComplexSelector, ComplexSelectorComponent, Namespace, Pseudo, SelectorList, SimpleSelector, Specificity, }; /// A compound selector is composed of several /// simple selectors #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct CompoundSelector { pub components: Vec, } impl fmt::Display for CompoundSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut did_write = false; for simple in &self.components { if did_write { write!(f, "{}", simple)?; } else { let s = simple.to_string(); if !s.is_empty() { did_write = true; } write!(f, "{}", s)?; } } // If we emit an empty compound, it's because all of the components got // optimized out because they match all selectors, so we just emit the // universal selector. if !did_write { f.write_char('*')?; } Ok(()) } } impl CompoundSelector { pub fn max_specificity(&self) -> i32 { self.specificity().max } pub fn min_specificity(&self) -> i32 { self.specificity().min } /// Returns tuple of (min, max) specificity pub fn specificity(&self) -> Specificity { let mut min = 0; let mut max = 0; for simple in &self.components { min += simple.min_specificity(); max += simple.max_specificity(); } Specificity::new(min, max) } pub fn is_invisible(&self) -> bool { self.components.iter().any(SimpleSelector::is_invisible) } pub fn is_super_selector( &self, other: &Self, parents: &Option>, ) -> bool { for simple1 in &self.components { if let SimpleSelector::Pseudo( pseudo @ Pseudo { selector: Some(..), .. }, ) = simple1 { if !pseudo.is_super_selector(other, parents.clone()) { return false; } } else if !simple1.is_super_selector_of_compound(other) { return false; } } for simple2 in &other.components { if let SimpleSelector::Pseudo(Pseudo { is_class: false, selector: None, .. }) = simple2 { if !simple2.is_super_selector_of_compound(self) { return false; } } } true } /// Returns a new `CompoundSelector` based on `compound` with all /// `SimpleSelector::Parent`s replaced with `parent`. /// /// Returns `None` if `compound` doesn't contain any `SimpleSelector::Parent`s. pub fn resolve_parent_selectors( self, span: Span, parent: SelectorList, ) -> SassResult>> { let contains_selector_pseudo = self.components.iter().any(|simple| { if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), .. }) = simple { sel.contains_parent_selector() } else { false } }); if !contains_selector_pseudo && !self.components[0].is_parent() { return Ok(None); } let resolved_members: Vec = if contains_selector_pseudo { self.components .clone() .into_iter() .map(|simple| { if let SimpleSelector::Pseudo(mut pseudo) = simple { if let Some(sel) = pseudo.selector.clone() { if !sel.contains_parent_selector() { return Ok(SimpleSelector::Pseudo(pseudo)); } pseudo.selector = Some(Box::new( sel.resolve_parent_selectors(Some(parent.clone()), false)?, )); } Ok(SimpleSelector::Pseudo(pseudo)) } else { Ok(simple) } }) .collect::>>()? } else { self.components.clone() }; if let Some(SimpleSelector::Parent(suffix)) = self.components.first() { if self.components.len() == 1 && suffix.is_none() { return Ok(Some(parent.components)); } } else { return Ok(Some(vec![ComplexSelector::new( vec![ComplexSelectorComponent::Compound(CompoundSelector { components: resolved_members, })], false, )])); } let parent_span = parent.span; Ok(Some( parent .components .into_iter() .map(move |mut complex| { let last_component = complex.components.last(); let last = if let Some(ComplexSelectorComponent::Compound(c)) = last_component { c.clone() } else { return Err(( format!("Parent \"{}\" is incompatible with this selector.", complex), span, ) .into()); }; let mut components = last.components; if let Some(SimpleSelector::Parent(Some(suffix))) = self.components.first() { let mut end = components.pop().unwrap(); end.add_suffix(suffix, parent_span)?; components.push(end); } components.extend(resolved_members.clone().into_iter().skip(1)); let last = CompoundSelector { components }; complex.components.pop(); let mut components = complex.components; components.push(ComplexSelectorComponent::Compound(last)); Ok(ComplexSelector::new(components, complex.line_break)) }) .collect::>>()?, )) } /// Returns a `CompoundSelector` that matches only elements that are matched by /// both `compound1` and `compound2`. /// /// If no such selector can be produced, returns `None`. pub fn unify(self, other: Self) -> Option { let mut components = other.components; for simple in self.components { components = simple.unify(std::mem::take(&mut components))?; } Some(Self { components }) } /// Adds a `SimpleSelector::Parent` to the beginning of `compound`, or returns `None` if /// that wouldn't produce a valid selector. pub fn prepend_parent(mut self) -> Option { Some(match self.components.first()? { SimpleSelector::Universal(..) => return None, SimpleSelector::Type(name) => { if name.namespace != Namespace::None { return None; } let mut components = vec![SimpleSelector::Parent(Some(name.ident.clone()))]; components.extend(self.components.into_iter().skip(1)); Self { components } } _ => { let mut components = vec![SimpleSelector::Parent(None)]; components.append(&mut self.components); Self { components } } }) } } grass_compiler-0.13.4/src/selector/extend/extended_selector.rs000064400000000000000000000043011046102023000226640ustar 00000000000000use std::{ cell::RefCell, collections::{hash_set::IntoIter, HashSet}, hash::{Hash, Hasher}, ops::Deref, ptr, rc::Rc, }; use crate::selector::{Selector, SelectorList}; #[derive(Debug, Clone)] pub(crate) struct ExtendedSelector(Rc>); impl PartialEq for ExtendedSelector { fn eq(&self, other: &Self) -> bool { self.0 == other.0 } } impl Eq for ExtendedSelector {} impl Hash for ExtendedSelector { // We hash the ptr here for efficiency. // TODO: is this an issue? it probably is, // but I haven't managed to find a test case // that exhibits it. fn hash(&self, state: &mut H) { ptr::hash(&*self.0, state); // in case we need to hash the actual value: // self.0.borrow().hash(state); } } impl ExtendedSelector { pub fn new(selector: SelectorList) -> Self { Self(Rc::new(RefCell::new(selector))) } pub fn is_invisible(&self) -> bool { (*self.0).borrow().is_invisible() } pub fn into_selector(self) -> Selector { Selector(match Rc::try_unwrap(self.0) { Ok(v) => v.into_inner(), Err(v) => v.borrow().clone(), }) } pub fn as_selector_list(&self) -> impl Deref + '_ { self.0.borrow() } pub fn set_inner(&mut self, selector: SelectorList) { self.0.replace(selector); } } /// There is the potential for danger here by modifying the hash /// through `RefCell`, but I haven't come up with a good solution /// for this yet (we can't just use a `Vec` because linear insert) /// is too big of a penalty /// /// In practice, I have yet to find a test case that can demonstrate /// an issue with storing a `RefCell`. #[derive(Clone, Debug)] pub(crate) struct SelectorHashSet(HashSet); impl SelectorHashSet { pub fn new() -> Self { Self(HashSet::new()) } pub fn insert(&mut self, selector: ExtendedSelector) { self.0.insert(selector); } } impl IntoIterator for SelectorHashSet { type Item = ExtendedSelector; type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } grass_compiler-0.13.4/src/selector/extend/extension.rs000064400000000000000000000046641046102023000212140ustar 00000000000000use codemap::Span; use crate::ast::CssMediaQuery; use super::{ComplexSelector, SimpleSelector}; #[derive(Clone, Debug)] pub(crate) struct Extension { /// The selector in which the `@extend` appeared. pub extender: ComplexSelector, /// The selector that's being extended. /// /// `None` for one-off extensions. pub target: Option, /// The minimum specificity required for any selector generated from this /// extender. pub specificity: i32, /// Whether this extension is optional. pub is_optional: bool, /// Whether this is a one-off extender representing a selector that was /// originally in the document, rather than one defined with `@extend`. pub is_original: bool, /// The media query context to which this extend is restricted, or `None` if /// it can apply within any context. pub media_context: Option>, /// The span in which `extender` was defined. pub span: Span, #[allow(dead_code)] pub left: Option>, #[allow(dead_code)] pub right: Option>, } impl Extension { pub fn one_off( extender: ComplexSelector, specificity: Option, is_original: bool, span: Span, ) -> Self { Self { specificity: specificity.unwrap_or_else(|| extender.max_specificity()), extender, target: None, span, is_optional: true, is_original, media_context: None, left: None, right: None, } } /// Asserts that the `media_context` for a selector is compatible with the /// query context for this extender. // todo: this should return a `Result`. it currently does not because the cascade effect // from this returning a `Result` will make some code returning `Option`s much uglier (we can't // use `?` to return both `Option` and `Result` from the same function) #[allow(clippy::needless_return)] pub fn assert_compatible_media_context(&self, media_context: &Option>) { if &self.media_context == media_context { return; } // Err(("You may not @extend selectors across media queries.", self.span).into()) } #[allow(clippy::missing_const_for_fn)] pub fn with_extender(mut self, extender: ComplexSelector) -> Self { self.extender = extender; self } } grass_compiler-0.13.4/src/selector/extend/functions.rs000064400000000000000000000670041046102023000212050ustar 00000000000000#![allow(clippy::similar_names)] use std::collections::VecDeque; use super::super::{ Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SimpleSelector, }; /// Returns the contents of a `SelectorList` that matches only elements that are /// matched by both `complex_one` and `complex_two`. /// /// If no such list can be produced, returns `None`. pub(crate) fn unify_complex( complexes: Vec>, ) -> Option>> { debug_assert!(!complexes.is_empty()); if complexes.len() == 1 { return Some(complexes); } let mut unified_base: Option> = None; for complex in &complexes { let base = complex.last()?; if let ComplexSelectorComponent::Compound(base) = base { if let Some(mut some_unified_base) = unified_base.clone() { for simple in base.components.clone() { some_unified_base = simple.unify(some_unified_base.clone())?; } unified_base = Some(some_unified_base); } else { unified_base = Some(base.components.clone()); } } else { return None; } } let mut complexes_without_bases: Vec> = complexes .into_iter() .map(|mut complex| { complex.pop(); complex }) .collect(); complexes_without_bases .last_mut() .unwrap() .push(ComplexSelectorComponent::Compound(CompoundSelector { components: unified_base?, })); Some(weave(complexes_without_bases)) } /// Expands "parenthesized selectors" in `complexes`. /// /// That is, if we have `.A .B {@extend .C}` and `.D .C {...}`, this /// conceptually expands into `.D .C, .D (.A .B)`, and this function translates /// `.D (.A .B)` into `.D .A .B, .A .D .B`. For thoroughness, `.A.D .B` would /// also be required, but including merged selectors results in exponential /// output for very little gain. /// /// The selector `.D (.A .B)` is represented as the list `[[.D], [.A, .B]]`. pub(crate) fn weave( mut complexes: Vec>, ) -> Vec> { let mut prefixes: Vec> = vec![complexes.remove(0)]; for mut complex in complexes { let target = match complex.pop() { Some(c) => c, None => continue, }; if complex.is_empty() { for prefix in &mut prefixes { prefix.push(target.clone()); } continue; } let parents: Vec = complex; let mut new_prefixes: Vec> = Vec::new(); for prefix in prefixes { if let Some(parent_prefixes) = weave_parents(prefix, parents.clone()) { for mut parent_prefix in parent_prefixes { parent_prefix.push(target.clone()); new_prefixes.push(parent_prefix); } } } prefixes = new_prefixes; } prefixes } /// Interweaves `parents_one` and `parents_two` as parents of the same target selector. /// /// Returns all possible orderings of the selectors in the inputs (including /// using unification) that maintain the relative ordering of the input. For /// example, given `.foo .bar` and `.baz .bang`, this would return `.foo .bar /// .baz .bang`, `.foo .bar.baz .bang`, `.foo .baz .bar .bang`, `.foo .baz /// .bar.bang`, `.foo .baz .bang .bar`, and so on until `.baz .bang .foo .bar`. /// /// Semantically, for selectors A and B, this returns all selectors `AB_i` /// such that the union over all i of elements matched by `AB_i X` is /// identical to the intersection of all elements matched by `A X` and all /// elements matched by `B X`. Some `AB_i` are elided to reduce the size of /// the output. fn weave_parents( parents_one: Vec, parents_two: Vec, ) -> Option>> { let mut queue_one = VecDeque::from(parents_one); let mut queue_two = VecDeque::from(parents_two); let initial_combinators = merge_initial_combinators(&mut queue_one, &mut queue_two)?; let mut final_combinators = merge_final_combinators(&mut queue_one, &mut queue_two, None)?; match (first_if_root(&mut queue_one), first_if_root(&mut queue_two)) { (Some(root_one), Some(root_two)) => { let root = ComplexSelectorComponent::Compound(root_one.unify(root_two)?); queue_one.push_front(root.clone()); queue_two.push_front(root); } (Some(root_one), None) => { queue_two.push_front(ComplexSelectorComponent::Compound(root_one)); } (None, Some(root_two)) => { queue_one.push_front(ComplexSelectorComponent::Compound(root_two)); } (None, None) => {} } let mut groups_one = group_selectors(Vec::from(queue_one)); let mut groups_two = group_selectors(Vec::from(queue_two)); let lcs = longest_common_subsequence( groups_two.as_slices().0, groups_one.as_slices().0, Some(&|group_one, group_two| { if group_one == group_two { return Some(group_one); } if let ComplexSelectorComponent::Combinator(..) = group_one.first()? { return None; } if let ComplexSelectorComponent::Combinator(..) = group_two.first()? { return None; } if complex_is_parent_superselector(group_one.clone(), group_two.clone()) { return Some(group_two); } if complex_is_parent_superselector(group_two.clone(), group_one.clone()) { return Some(group_one); } if !must_unify(&group_one, &group_two) { return None; } let unified = unify_complex(vec![group_one, group_two])?; if unified.len() > 1 { return None; } unified.first().cloned() }), ); let mut choices = vec![vec![initial_combinators .into_iter() .map(ComplexSelectorComponent::Combinator) .collect::>()]]; for group in lcs { choices.push( chunks(&mut groups_one, &mut groups_two, |sequence| { complex_is_parent_superselector( match sequence.front() { Some(v) => v.clone(), None => return true, }, group.clone(), ) }) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ); choices.push(vec![group]); groups_one.pop_front(); groups_two.pop_front(); } choices.push( chunks(&mut groups_one, &mut groups_two, VecDeque::is_empty) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ); choices.append(&mut final_combinators); Some( paths( choices .into_iter() .filter(|choice| !choice.is_empty()) .collect(), ) .into_iter() .map(|chunk| chunk.into_iter().flatten().collect()) .collect(), ) } /// Extracts leading `Combinator`s from `components_one` and `components_two` and /// merges them together into a single list of combinators. /// /// If there are no combinators to be merged, returns an empty list. If the /// combinators can't be merged, returns `None`. fn merge_initial_combinators( components_one: &mut VecDeque, components_two: &mut VecDeque, ) -> Option> { let mut combinators_one: Vec = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(c)) = components_one.front() { combinators_one.push(*c); components_one.pop_front(); } let mut combinators_two = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(c)) = components_two.front() { combinators_two.push(*c); components_two.pop_front(); } let lcs = longest_common_subsequence(&combinators_one, &combinators_two, None); if lcs == combinators_one { Some(combinators_two) } else if lcs == combinators_two { Some(combinators_one) } else { // If neither sequence of combinators is a subsequence of the other, they // cannot be merged successfully. None } } /// Returns the longest common subsequence between `list_one` and `list_two`. /// /// If there are more than one equally long common subsequence, returns the one /// which starts first in `list_one`. /// /// If `select` is passed, it's used to check equality between elements in each /// list. If it returns `None`, the elements are considered unequal; otherwise, /// it should return the element to include in the return value. fn longest_common_subsequence( list_one: &[T], list_two: &[T], select: Option<&dyn Fn(T, T) -> Option>, ) -> Vec { let select = select.unwrap_or(&|element_one, element_two| { if element_one == element_two { Some(element_one) } else { None } }); let mut lengths = vec![vec![0; list_two.len() + 1]; list_one.len() + 1]; let mut selections: Vec>> = vec![vec![None; list_two.len()]; list_one.len()]; for i in 0..list_one.len() { for j in 0..list_two.len() { let selection = select( list_one.get(i).unwrap().clone(), list_two.get(j).unwrap().clone(), ); selections[i][j] = selection.clone(); lengths[i + 1][j + 1] = if selection.is_none() { std::cmp::max(lengths[i + 1][j], lengths[i][j + 1]) } else { lengths[i][j] + 1 }; } } fn backtrack( i: isize, j: isize, lengths: Vec>, selections: &mut Vec>>, ) -> Vec { if i == -1 || j == -1 { return Vec::new(); } let selection = selections.get(i as usize).cloned().unwrap_or_default(); if let Some(Some(selection)) = selection.get(j as usize) { let mut tmp = backtrack(i - 1, j - 1, lengths, selections); tmp.push(selection.clone()); return tmp; } if lengths[(i + 1) as usize][j as usize] > lengths[i as usize][(j + 1) as usize] { backtrack(i, j - 1, lengths, selections) } else { backtrack(i - 1, j, lengths, selections) } } backtrack( (list_one.len() as isize).saturating_sub(1), (list_two.len() as isize).saturating_sub(1), lengths, &mut selections, ) } /// Extracts trailing `Combinator`s, and the selectors to which they apply, from /// `components_one` and `components_two` and merges them together into a single list. /// /// If there are no combinators to be merged, returns an empty list. If the /// sequences can't be merged, returns `None`. #[allow(clippy::cognitive_complexity)] fn merge_final_combinators( components_one: &mut VecDeque, components_two: &mut VecDeque, result: Option>>>, ) -> Option>>> { let mut result = result.unwrap_or_default(); if (components_one.is_empty() || !components_one.back().unwrap().is_combinator()) && (components_two.is_empty() || !components_two.back().unwrap().is_combinator()) { return Some(Vec::from(result)); } let mut combinators_one = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(combinator)) = components_one.get(components_one.len().saturating_sub(1)) { combinators_one.push(*combinator); components_one.pop_back(); } let mut combinators_two = Vec::new(); while let Some(ComplexSelectorComponent::Combinator(combinator)) = components_two.get(components_two.len().saturating_sub(1)) { combinators_two.push(*combinator); components_two.pop_back(); } if combinators_one.len() > 1 || combinators_two.len() > 1 { // If there are multiple combinators, something hacky's going on. If one // is a supersequence of the other, use that, otherwise give up. let lcs = longest_common_subsequence(&combinators_one, &combinators_two, None); if lcs == combinators_one { result.push_front(vec![combinators_two .into_iter() .map(ComplexSelectorComponent::Combinator) .rev() .collect()]); } else if lcs == combinators_two { result.push_front(vec![combinators_one .into_iter() .map(ComplexSelectorComponent::Combinator) .rev() .collect()]); } else { return None; } return Some(Vec::from(result)); } let combinator_one = combinators_one.first(); let combinator_two = combinators_two.first(); // This code looks complicated, but it's actually just a bunch of special // cases for interactions between different combinators. match (combinator_one, combinator_two) { (Some(combinator_one), Some(combinator_two)) => { let compound_one = match components_one.pop_back() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!(), }; let compound_two = match components_two.pop_back() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => unreachable!(), }; match (combinator_one, combinator_two) { (Combinator::FollowingSibling, Combinator::FollowingSibling) => { if compound_one.is_super_selector(&compound_two, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_two), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]]); } else if compound_two.is_super_selector(&compound_one, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_one), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]]); } else { let mut choices = vec![ vec![ ComplexSelectorComponent::Compound(compound_one.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(compound_two.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ], vec![ ComplexSelectorComponent::Compound(compound_two.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(compound_one.clone()), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ], ]; if let Some(unified) = compound_one.unify(compound_two) { choices.push(vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ]); } result.push_front(choices); } } (Combinator::FollowingSibling, Combinator::NextSibling) | (Combinator::NextSibling, Combinator::FollowingSibling) => { let following_sibling_selector = if combinator_one == &Combinator::FollowingSibling { compound_one.clone() } else { compound_two.clone() }; let next_sibling_selector = if combinator_one == &Combinator::FollowingSibling { compound_two.clone() } else { compound_one.clone() }; if following_sibling_selector.is_super_selector(&next_sibling_selector, &None) { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(next_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]]); } else { let mut v = vec![vec![ ComplexSelectorComponent::Compound(following_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling), ComplexSelectorComponent::Compound(next_sibling_selector), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]]; if let Some(unified) = compound_one.unify(compound_two) { v.push(vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(Combinator::NextSibling), ]); } result.push_front(v); } } (Combinator::Child, Combinator::NextSibling) | (Combinator::Child, Combinator::FollowingSibling) => { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_two), ComplexSelectorComponent::Combinator(*combinator_two), ]]); components_one.push_back(ComplexSelectorComponent::Compound(compound_one)); components_one .push_back(ComplexSelectorComponent::Combinator(Combinator::Child)); } (Combinator::NextSibling, Combinator::Child) | (Combinator::FollowingSibling, Combinator::Child) => { result.push_front(vec![vec![ ComplexSelectorComponent::Compound(compound_one), ComplexSelectorComponent::Combinator(*combinator_one), ]]); components_two.push_back(ComplexSelectorComponent::Compound(compound_two)); components_two .push_back(ComplexSelectorComponent::Combinator(Combinator::Child)); } (..) => { if combinator_one != combinator_two { return None; } let unified = compound_one.unify(compound_two)?; result.push_front(vec![vec![ ComplexSelectorComponent::Compound(unified), ComplexSelectorComponent::Combinator(*combinator_one), ]]); } } merge_final_combinators(components_one, components_two, Some(result)) } (Some(combinator_one), None) => { if *combinator_one == Combinator::Child && !components_two.is_empty() { if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() { if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() { if c2.is_super_selector(c1, &None) { components_two.pop_back(); } } } } result.push_front(vec![vec![ components_one.pop_back().unwrap(), ComplexSelectorComponent::Combinator(*combinator_one), ]]); merge_final_combinators(components_one, components_two, Some(result)) } (None, Some(combinator_two)) => { if *combinator_two == Combinator::Child && !components_one.is_empty() { if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() { if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() { if c1.is_super_selector(c2, &None) { components_one.pop_back(); } } } } result.push_front(vec![vec![ components_two.pop_back().unwrap(), ComplexSelectorComponent::Combinator(*combinator_two), ]]); merge_final_combinators(components_one, components_two, Some(result)) } (None, None) => unreachable!(), } } /// If the first element of `queue` has a `::root` selector, removes and returns /// that element. fn first_if_root(queue: &mut VecDeque) -> Option { if queue.is_empty() { return None; } if let Some(ComplexSelectorComponent::Compound(c)) = queue.front() { if !has_root(c) { return None; } let compound = c.clone(); queue.pop_front(); Some(compound) } else { None } } /// Returns whether or not `compound` contains a `::root` selector. fn has_root(compound: &CompoundSelector) -> bool { compound.components.iter().any(|simple| { if let SimpleSelector::Pseudo(pseudo) = simple { pseudo.is_class && pseudo.normalized_name() == "root" } else { false } }) } /// Returns `complex`, grouped into sub-lists such that no sub-list contains two /// adjacent `ComplexSelector`s. /// /// For example, `(A B > C D + E ~ > G)` is grouped into /// `[(A) (B > C) (D + E ~ > G)]`. fn group_selectors( complex: Vec, ) -> VecDeque> { let mut groups = VecDeque::new(); let mut iter = complex.into_iter(); groups.push_back(if let Some(c) = iter.next() { vec![c] } else { return groups; }); for c in iter { let mut last_group = groups.pop_back().unwrap(); if last_group .last() .map_or(false, ComplexSelectorComponent::is_combinator) || c.is_combinator() { last_group.push(c); groups.push_back(last_group); } else { groups.push_back(last_group); groups.push_back(vec![c]); } } groups } /// Returns all orderings of initial subseqeuences of `queue_one` and `queue_two`. /// /// The `done` callback is used to determine the extent of the initial /// subsequences. It's called with each queue until it returns `true`. /// /// This destructively removes the initial subsequences of `queue_one` and /// `queue_two`. /// /// For example, given `(A B C | D E)` and `(1 2 | 3 4 5)` (with `|` denoting /// the boundary of the initial subsequence), this would return `[(A B C 1 2), /// (1 2 A B C)]`. The queues would then contain `(D E)` and `(3 4 5)`. fn chunks( queue_one: &mut VecDeque, queue_two: &mut VecDeque, done: impl Fn(&VecDeque) -> bool, ) -> Vec> { let mut chunk_one = Vec::new(); while !done(queue_one) { chunk_one.push(queue_one.pop_front().unwrap()); } let mut chunk_two = Vec::new(); while !done(queue_two) { chunk_two.push(queue_two.pop_front().unwrap()); } match (chunk_one.is_empty(), chunk_two.is_empty()) { (true, true) => Vec::new(), (true, false) => vec![chunk_two], (false, true) => vec![chunk_one], (false, false) => { let mut l1 = chunk_one.clone(); l1.append(&mut chunk_two.clone()); let mut l2 = chunk_two; l2.append(&mut chunk_one); vec![l1, l2] } } } /// Like `complex_is_superselector`, but compares `complex_one` and `complex_two` as /// though they shared an implicit base `SimpleSelector`. /// /// For example, `B` is not normally a superselector of `B A`, since it doesn't /// match elements that match `A`. However, it *is* a parent superselector, /// since `B X` is a superselector of `B A X`. fn complex_is_parent_superselector( mut complex_one: Vec, mut complex_two: Vec, ) -> bool { if let Some(ComplexSelectorComponent::Combinator(..)) = complex_one.first() { return false; } if let Some(ComplexSelectorComponent::Combinator(..)) = complex_two.first() { return false; } if complex_one.len() > complex_two.len() { return false; } let base = CompoundSelector { components: vec![SimpleSelector::Placeholder(String::new())], }; complex_one.push(ComplexSelectorComponent::Compound(base.clone())); complex_two.push(ComplexSelectorComponent::Compound(base)); ComplexSelector::new(complex_one, false) .is_super_selector(&ComplexSelector::new(complex_two, false)) } /// Returns a list of all possible paths through the given lists. /// /// For example, given `[[1, 2], [3, 4], [5]]`, this returns: /// /// ```no_run /// [[1, 3, 5], /// [2, 3, 5], /// [1, 4, 5], /// [2, 4, 5]]; /// ``` pub(crate) fn paths(choices: Vec>) -> Vec> { choices.into_iter().fold(vec![vec![]], |paths, choice| { choice .into_iter() .flat_map(move |option| { paths.clone().into_iter().map(move |mut path| { path.push(option.clone()); path }) }) .collect() }) } /// Returns whether `complex_one` and `complex_two` need to be unified to produce a /// valid combined selector. /// /// This is necessary when both selectors contain the same unique simple /// selector, such as an ID. fn must_unify( complex_one: &[ComplexSelectorComponent], complex_two: &[ComplexSelectorComponent], ) -> bool { let mut unique_selectors = Vec::new(); for component in complex_one { if let ComplexSelectorComponent::Compound(c) = component { unique_selectors.extend(c.components.iter().filter(|f| is_unique(f))); } } if unique_selectors.is_empty() { return false; } complex_two.iter().any(|component| { if let ComplexSelectorComponent::Compound(compound) = component { compound .components .iter() .any(|simple| is_unique(simple) && unique_selectors.contains(&simple)) } else { false } }) } /// Returns whether a `CompoundSelector` may contain only one simple selector of /// the same type as `simple`. fn is_unique(simple: &SimpleSelector) -> bool { matches!( simple, SimpleSelector::Id(..) | SimpleSelector::Pseudo(Pseudo { is_class: false, .. }) ) } grass_compiler-0.13.4/src/selector/extend/merged.rs000064400000000000000000000041521046102023000204330ustar 00000000000000use crate::error::SassResult; use super::Extension; /// An `Extension` created by merging two `Extension`s with the same extender /// and target. /// /// This is used when multiple mandatory extensions exist to ensure that both of /// them are marked as resolved. pub(super) struct MergedExtension; impl MergedExtension { /// Returns an extension that combines `left` and `right`. /// /// Returns an `Err` if `left` and `right` have incompatible media /// contexts. /// /// Returns an `Err` if `left` and `right` don't have the same /// extender and target. pub fn merge(left: Extension, right: Extension) -> SassResult { if left.extender != right.extender || left.target != right.target { return Err(( format!( "{} and {} aren't the same extension.", left.extender, right.extender ), left.span.merge(right.span), ) .into()); } if left.media_context.is_some() && right.media_context.is_some() && left.media_context != right.media_context { return Err(( "You may not @extend the same selector from within different media queries.", right.span, ) .into()); } if right.is_optional && right.media_context.is_none() { return Ok(left); } if left.is_optional && left.media_context.is_none() { return Ok(right); } Ok(MergedExtension::into_extension(left, right)) } fn into_extension(left: Extension, right: Extension) -> Extension { Extension { extender: left.extender, target: left.target, span: left.span, media_context: match left.media_context { Some(v) => Some(v), None => right.media_context, }, specificity: left.specificity, is_optional: true, is_original: false, left: None, right: None, } } } grass_compiler-0.13.4/src/selector/extend/mod.rs000064400000000000000000001322001046102023000177430ustar 00000000000000use std::{ collections::{HashMap, HashSet, VecDeque}, hash::Hash, }; use codemap::Span; use indexmap::IndexMap; use crate::{ast::CssMediaQuery, error::SassResult}; use super::{ ComplexSelector, ComplexSelectorComponent, ComplexSelectorHashSet, CompoundSelector, Pseudo, SelectorList, SimpleSelector, }; pub(crate) use extended_selector::ExtendedSelector; use extended_selector::SelectorHashSet; use extension::Extension; pub(crate) use functions::unify_complex; use functions::{paths, weave}; use merged::MergedExtension; pub(crate) use rule::ExtendRule; mod extended_selector; mod extension; mod functions; mod merged; mod rule; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] /// Different modes in which extension can run. enum ExtendMode { /// Normal mode, used with the `@extend` rule. /// /// This preserves existing selectors and extends each target individually. Normal, /// Replace mode, used by the `selector-replace()` function. /// /// This replaces existing selectors and requires every target to match to /// extend a given compound selector. Replace, /// All-targets mode, used by the `selector-extend()` function. /// /// This preserves existing selectors but requires every target to match to /// extend a given compound selector. AllTargets, } impl Default for ExtendMode { fn default() -> Self { Self::Normal } } #[derive(Clone, Debug)] pub(crate) struct ExtensionStore { /// A map from all simple selectors in the stylesheet to the selector lists /// that contain them. /// /// This is used to find which selectors an `@extend` applies to and adjust /// them. selectors: HashMap, /// A map from all extended simple selectors to the sources of those /// extensions. extensions: HashMap>, /// A map from all simple selectors in extenders to the extensions that those /// extenders define. extensions_by_extender: HashMap>, /// A map from CSS selectors to the media query contexts they're defined in. /// /// This tracks the contexts in which each selector's style rule is defined. /// If a rule is defined at the top level, it doesn't have an entry. media_contexts: HashMap>, /// A map from `SimpleSelector`s to the specificity of their source /// selectors. /// /// This tracks the maximum specificity of the `ComplexSelector` that /// originally contained each `SimpleSelector`. This allows us to ensure that /// we don't trim any selectors that need to exist to satisfy the [second law /// of extend][]. /// /// [second law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 source_specificity: HashMap, /// A set of `ComplexSelector`s that were originally part of /// their component `SelectorList`s, as opposed to being added by `@extend`. /// /// This allows us to ensure that we don't trim any selectors that need to /// exist to satisfy the [first law of extend][]. /// /// [first law of extend]: https://github.com/sass/sass/issues/324#issuecomment-4607184 originals: ComplexSelectorHashSet, /// The mode that controls this extender's behavior. mode: ExtendMode, span: Span, } impl ExtensionStore { /// An `Extender` that contains no extensions and can have no extensions added. // TODO: empty extender #[allow(dead_code)] const EMPTY: () = (); pub fn extend( selector: SelectorList, source: SelectorList, targets: SelectorList, span: Span, ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::AllTargets, span) } pub fn new(span: Span) -> Self { Self { selectors: HashMap::new(), extensions: HashMap::new(), extensions_by_extender: HashMap::new(), media_contexts: HashMap::new(), source_specificity: HashMap::new(), originals: ComplexSelectorHashSet::new(), mode: ExtendMode::Normal, span, } } pub fn replace( selector: SelectorList, source: SelectorList, targets: SelectorList, span: Span, ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::Replace, span) } fn extend_or_replace( selector: SelectorList, source: SelectorList, targets: SelectorList, mode: ExtendMode, span: Span, ) -> SassResult { let extenders: IndexMap = source .components .into_iter() .map(|complex| { ( complex.clone(), Extension::one_off(complex, None, false, span), ) }) .collect(); let compound_targets = targets .components .into_iter() .map(|complex| { if complex.components.len() == 1 { Ok(complex.components.first().unwrap().as_compound().clone()) } else { Err((format!("Can't extend complex selector {}.", complex), span).into()) } }) .collect::>>()?; let extensions: HashMap> = compound_targets .into_iter() .flat_map(|compound| { compound .components .into_iter() .map(|simple| (simple, extenders.clone())) }) .collect(); let mut extender = ExtensionStore::with_mode(mode, span); if !selector.is_invisible() { extender.originals.extend(selector.components.iter()); } Ok(extender.extend_list(selector, Some(&extensions), &None)) } fn with_mode(mode: ExtendMode, span: Span) -> Self { Self { mode, ..ExtensionStore::new(span) } } /// Extends `list` using `extensions`. fn extend_list( &mut self, list: SelectorList, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> SelectorList { // This could be written more simply using Vec>, but we want to avoid // any allocations in the common case where no extends apply. let mut extended: Option> = None; for (i, complex) in list.components.iter().enumerate() { if let Some(result) = self.extend_complex(complex.clone(), extensions, media_query_context) { if extended.is_none() { extended = Some(if i == 0 { Vec::new() } else { list.components[0..i].to_vec() }); } match extended.as_mut() { Some(v) => v.extend(result.into_iter()), None => unreachable!(), } } else if let Some(extended) = extended.as_mut() { extended.push(complex.clone()); } } let extended = match extended { Some(v) => v, None => return list, }; SelectorList { components: self.trim(extended, &|complex| self.originals.contains(complex)), span: self.span, } } /// Extends `complex` using `extensions`, and returns the contents of a /// `SelectorList`. fn extend_complex( &mut self, complex: ComplexSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> Option> { // The complex selectors that each compound selector in `complex.components` // can expand to. // // For example, given // // .a .b {...} // .x .y {@extend .b} // // this will contain // // [ // [.a], // [.b, .x .y] // ] // // This could be written more simply using `Vec::into_iter::map`, but we want to avoid // any allocations in the common case where no extends apply. let mut extended_not_expanded: Option>> = None; let complex_has_line_break = complex.line_break; let is_original = self.originals.contains(&complex); for (i, component) in complex.components.iter().enumerate() { if let ComplexSelectorComponent::Compound(component) = component { if let Some(extended) = self.extend_compound(component, extensions, media_query_context, is_original) { if extended_not_expanded.is_none() { extended_not_expanded = Some( complex .components .clone() .into_iter() .take(i) .map(|component| { vec![ComplexSelector::new(vec![component], complex.line_break)] }) .collect(), ); } match extended_not_expanded.as_mut() { Some(v) => v.push(extended), None => unreachable!(), } } else { match extended_not_expanded.as_mut() { Some(v) => v.push(vec![ComplexSelector::new( vec![ComplexSelectorComponent::Compound(component.clone())], false, )]), None => {} } } } else if component.is_combinator() { match extended_not_expanded.as_mut() { Some(v) => v.push(vec![ComplexSelector::new(vec![component.clone()], false)]), None => {} } } } let extended_not_expanded = extended_not_expanded?; let mut first = true; Some( paths(extended_not_expanded) .into_iter() .flat_map(move |path| { weave( path.clone() .into_iter() .map(move |complex| complex.components) .collect(), ) .into_iter() .map(|components| { let output_complex = ComplexSelector::new( components, complex_has_line_break || path.iter().any(|input_complex| input_complex.line_break), ); // Make sure that copies of `complex` retain their status as "original" // selectors. This includes selectors that are modified because a :not() // was extended into. if first && self.originals.contains(&complex) { self.originals.insert(&output_complex); } first = false; output_complex }) .collect::>() }) .collect(), ) } /// Extends `compound` using `extensions`, and returns the contents of a /// `SelectorList`. /// /// The `in_original` parameter indicates whether this is in an original /// complex selector, meaning that `compound` should not be trimmed out. fn extend_compound( &mut self, compound: &CompoundSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, in_original: bool, ) -> Option> { // If there's more than one target and they all need to match, we track // which targets are actually extended. let mut targets_used: HashSet = HashSet::new(); let mut options: Option>> = None; for i in 0..compound.components.len() { let simple = compound.components.get(i).cloned().unwrap(); match self.extend_simple( simple.clone(), extensions, media_query_context, &mut targets_used, ) { Some(extended) => { if options.is_none() { let mut new_options = Vec::new(); if i != 0 { new_options.push(vec![ self.extension_for_compound(compound.components[..i].to_vec()) ]); } options.replace(new_options); } match options.as_mut() { Some(v) => v.extend(extended.into_iter()), None => unreachable!(), } } None => match options.as_mut() { Some(v) => v.push(vec![self.extension_for_simple(simple)]), None => {} }, } } let options = options?; // If `self.mode` isn't `ExtendMode::Normal` and we didn't use all the targets in // `extensions`, extension fails for `compound`. // todo: test for `extensions.len() > 2`. may cause issues if !targets_used.is_empty() && targets_used.len() != extensions.map_or(self.extensions.len(), HashMap::len) && self.mode != ExtendMode::Normal { return None; } // Optimize for the simple case of a single simple selector that doesn't // need any unification. if options.len() == 1 { return Some( options .first()? .clone() .into_iter() .map(|state| { state.assert_compatible_media_context(media_query_context); state.extender }) .collect(), ); } // Find all paths through `options`. In this case, each path represents a // different unification of the base selector. For example, if we have: // // .a.b {...} // .w .x {@extend .a} // .y .z {@extend .b} // // then `options` is `[[.a, .w .x], [.b, .y .z]]` and `paths(options)` is // // [ // [.a, .b], // [.a, .y .z], // [.w .x, .b], // [.w .x, .y .z] // ] // // We then unify each path to get a list of complex selectors: // // [ // [.a.b], // [.y .a.z], // [.w .x.b], // [.w .y .x.z, .y .w .x.z] // ] let mut first = self.mode != ExtendMode::Replace; let unified_paths = paths(options).into_iter().map(|path| { let complexes: Vec> = if first { // The first path is always the original selector. We can't just // return `compound` directly because pseudo selectors may be // modified, but we don't have to do any unification. first = false; vec![vec![ComplexSelectorComponent::Compound(CompoundSelector { components: path .clone() .into_iter() .flat_map(|state| { debug_assert!(state.extender.components.len() == 1); match state.extender.components.last().cloned() { Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(..) | None => unreachable!(), } }) .collect(), })]] } else { let mut to_unify: VecDeque> = VecDeque::new(); let mut originals: Vec = Vec::new(); for state in path.clone() { if state.is_original { originals.extend(match state.extender.components.last().cloned() { Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(..) | None => unreachable!(), }); } else { to_unify.push_back(state.extender.components.clone()); } } if !originals.is_empty() { to_unify.push_front(vec![ComplexSelectorComponent::Compound( CompoundSelector { components: originals, }, )]); } unify_complex(Vec::from(to_unify))? }; let mut line_break = false; for state in path { state.assert_compatible_media_context(media_query_context); line_break = line_break || state.extender.line_break; } Some( complexes .into_iter() .map(|components| ComplexSelector::new(components, line_break)) .collect::>(), ) }); let unified_paths: Vec = unified_paths.flatten().flatten().collect(); Some(if in_original && self.mode != ExtendMode::Replace { let original = unified_paths.first().cloned(); self.trim(unified_paths, &|complex| Some(complex) == original.as_ref()) } else { self.trim(unified_paths, &|_| false) }) } fn extend_simple( &mut self, simple: SimpleSelector, extensions: Option<&HashMap>>, media_query_context: &Option>, targets_used: &mut HashSet, ) -> Option>> { if let SimpleSelector::Pseudo(Pseudo { selector: Some(..), .. }) = &simple { let simple = if let SimpleSelector::Pseudo(pseudo) = simple.clone() { pseudo } else { unreachable!() }; if let Some(extended) = self.extend_pseudo(simple, extensions, media_query_context) { return Some( extended .into_iter() .map(move |pseudo| { self.without_pseudo( SimpleSelector::Pseudo(pseudo.clone()), extensions, targets_used, self.mode, ) .unwrap_or_else(|| { vec![self.extension_for_simple(SimpleSelector::Pseudo(pseudo))] }) }) .collect(), ); } } self.without_pseudo(simple, extensions, targets_used, self.mode) .map(|v| vec![v]) } /// Extends `pseudo` using `extensions`, and returns a list of resulting /// pseudo selectors. fn extend_pseudo( &mut self, pseudo: Pseudo, extensions: Option<&HashMap>>, media_query_context: &Option>, ) -> Option> { let extended = self.extend_list( pseudo .selector .as_deref() .cloned() .unwrap_or_else(|| SelectorList::new(self.span)), extensions, media_query_context, ); /*todo: identical(extended, pseudo.selector)*/ if Some(&extended) == pseudo.selector.as_deref() { return None; } // For `:not()`, we usually want to get rid of any complex selectors because // that will cause the selector to fail to parse on all browsers at time of // writing. We can keep them if either the original selector had a complex // selector, or the result of extending has only complex selectors, because // either way we aren't breaking anything that isn't already broken. let mut complexes = if pseudo.normalized_name() == "not" && !pseudo .selector .clone() .unwrap() .components .iter() .any(|complex| complex.components.len() > 1) && extended .components .iter() .any(|complex| complex.components.len() == 1) { extended .components .into_iter() .filter(|complex| complex.components.len() <= 1) .collect() } else { extended.components }; complexes = complexes .into_iter() .flat_map(|complex| { if complex.components.len() != 1 { return vec![complex]; } let compound = match complex.components.first() { Some(ComplexSelectorComponent::Compound(c)) => c, Some(..) | None => return vec![complex], }; if compound.components.len() != 1 { return vec![complex]; } if !compound.components.first().unwrap().is_pseudo() { return vec![complex]; } let inner_pseudo = match compound.components.first() { Some(SimpleSelector::Pseudo(pseudo)) => pseudo, Some(..) | None => return vec![complex], }; if inner_pseudo.selector.is_none() { return vec![complex]; } match pseudo.normalized_name() { "not" => { // In theory, if there's a `:not` nested within another `:not`, the // inner `:not`'s contents should be unified with the return value. // For example, if `:not(.foo)` extends `.bar`, `:not(.bar)` should // become `.foo:not(.bar)`. However, this is a narrow edge case and // supporting it properly would make this code and the code calling it // a lot more complicated, so it's not supported for now. let inner_pseudo_normalized = inner_pseudo.normalized_name(); if ["matches", "is", "where"].contains(&inner_pseudo_normalized) { inner_pseudo.selector.clone().unwrap().components } else { Vec::new() } } "matches" | "where" | "is" | "any" | "current" | "nth-child" | "nth-last-child" => { // As above, we could theoretically support :not within :matches, but // doing so would require this method and its callers to handle much // more complex cases that likely aren't worth the pain. if inner_pseudo.name != pseudo.name || inner_pseudo.argument != pseudo.argument { Vec::new() } else { inner_pseudo.selector.clone().unwrap().components } } "has" | "host" | "host-context" | "slotted" => { // We can't expand nested selectors here, because each layer adds an // additional layer of semantics. For example, `:has(:has(img))` // doesn't match `
` but `:has(img)` does. vec![complex] } _ => Vec::new(), } }) .collect(); // Older browsers support `:not`, but only with a single complex selector. // In order to support those browsers, we break up the contents of a `:not` // unless it originally contained a selector list. if pseudo.normalized_name() == "not" && pseudo.selector.clone().unwrap().components.len() == 1 { let result = complexes .into_iter() .map(|complex| { pseudo.clone().with_selector(Some(Box::new(SelectorList { components: vec![complex], span: self.span, }))) }) .collect::>(); if result.is_empty() { None } else { Some(result) } } else { Some(vec![pseudo.with_selector(Some(Box::new(SelectorList { components: complexes, span: self.span, })))]) } } /// Extends `simple` without extending the contents of any selector pseudos /// it contains. fn without_pseudo( &self, simple: SimpleSelector, extensions: Option<&HashMap>>, targets_used: &mut HashSet, mode: ExtendMode, ) -> Option> { let extenders = extensions.unwrap_or(&self.extensions).get(&simple)?; targets_used.insert(simple.clone()); if mode == ExtendMode::Replace { return Some(extenders.values().cloned().collect()); } let mut tmp = vec![self.extension_for_simple(simple)]; tmp.reserve(extenders.len()); tmp.extend(extenders.values().cloned()); Some(tmp) } /// Returns a one-off `Extension` whose extender is composed solely of /// `simple`. fn extension_for_simple(&self, simple: SimpleSelector) -> Extension { let specificity = Some(*self.source_specificity.get(&simple).unwrap_or(&0_i32)); Extension::one_off( ComplexSelector::new( vec![ComplexSelectorComponent::Compound(CompoundSelector { components: vec![simple], })], false, ), specificity, true, self.span, ) } /// Returns a one-off `Extension` whose extender is composed solely of a /// compound selector containing `simples`. fn extension_for_compound(&self, simples: Vec) -> Extension { let compound = CompoundSelector { components: simples, }; let specificity = Some(self.source_specificity_for(&compound)); Extension::one_off( ComplexSelector::new(vec![ComplexSelectorComponent::Compound(compound)], false), specificity, true, self.span, ) } /// Returns the maximum specificity for sources that went into producing /// `compound`. fn source_specificity_for(&self, compound: &CompoundSelector) -> i32 { let mut specificity = 0; for simple in &compound.components { specificity = specificity.max(*self.source_specificity.get(simple).unwrap_or(&0)); } specificity } /// Removes elements from `selectors` if they're subselectors of other /// elements. /// /// The `is_original` callback indicates which selectors are original to the /// document, and thus should never be trimmed. fn trim( &self, selectors: Vec, is_original: &dyn Fn(&ComplexSelector) -> bool, ) -> Vec { // Avoid truly horrific quadratic behavior. // // TODO(nweiz): I think there may be a way to get perfect trimming without // going quadratic by building some sort of trie-like data structure that // can be used to look up superselectors. if selectors.len() > 100 { return selectors; } // This is n² on the sequences, but only comparing between separate // sequences should limit the quadratic behavior. We iterate from last to // first and reverse the result so that, if two selectors are identical, we // keep the first one. let mut result: VecDeque = VecDeque::new(); let mut num_originals = 0; // :outer for i in (0..=(selectors.len().saturating_sub(1))).rev() { let mut should_continue_to_outer = false; let complex1 = selectors.get(i).unwrap(); if is_original(complex1) { // Make sure we don't include duplicate originals, which could happen if // a style rule extends a component of its own selector. for j in 0..num_originals { if result.get(j) == Some(complex1) { rotate_slice(&mut result, 0, j + 1); should_continue_to_outer = true; break; } } if should_continue_to_outer { continue; } num_originals += 1; result.push_front(complex1.clone()); continue; } // The maximum specificity of the sources that caused `complex1` to be // generated. In order for `complex1` to be removed, there must be another // selector that's a superselector of it *and* that has specificity // greater or equal to this. let mut max_specificity = 0; for component in &complex1.components { if let ComplexSelectorComponent::Compound(compound) = component { max_specificity = max_specificity.max(self.source_specificity_for(compound)); } } // Look in `result` rather than `selectors` for selectors after `i`. This // ensures that we aren't comparing against a selector that's already been // trimmed, and thus that if there are two identical selectors only one is // trimmed. let should_continue = result.iter().any(|complex2| { complex2.min_specificity() >= max_specificity && complex2.is_super_selector(complex1) }); if should_continue { continue; } let should_continue = selectors.iter().take(i).any(|complex2| { complex2.min_specificity() >= max_specificity && complex2.is_super_selector(complex1) }); if should_continue { continue; } result.push_front(complex1.clone()); } Vec::from(result) } /// Adds `selector` to this extender. /// /// Extends `selector` using any registered extensions, then returns the resulting /// selector. If any more relevant extensions are added, the returned selector /// is automatically updated. /// /// The `media_query_context` is the media query context in which the selector was /// defined, or `None` if it was defined at the top level of the document. pub fn add_selector( &mut self, mut selector: SelectorList, // span: Span, media_query_context: &Option>, ) -> ExtendedSelector { if !selector.is_invisible() { for complex in selector.components.clone() { self.originals.insert(&complex); } } if !self.extensions.is_empty() { selector = self.extend_list(selector, None, media_query_context); /* todo: when we have error handling } on SassException catch (error) { throw SassException( "From ${error.span.message('')}\n" "${error.message}", span); } */ } if let Some(mut media_query_context) = media_query_context.clone() { self.media_contexts .get_mut(&selector) .replace(&mut media_query_context); } let extended_selector = ExtendedSelector::new(selector.clone()); self.register_selector(selector, &extended_selector); extended_selector } /// Registers the `SimpleSelector`s in `list` to point to `selector` in /// `self.selectors`. fn register_selector(&mut self, list: SelectorList, selector: &ExtendedSelector) { for complex in list.components { for component in complex.components { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { // PERF: we compute the hash twice, which isn't great, but we avoid a superfluous // clone in cases where we have already seen a simple selector (common in // scenarios in which there is a lot of nesting) if let Some(entry) = self.selectors.get_mut(&simple) { entry.insert(selector.clone()); } else { self.selectors .entry(simple.clone()) .or_insert_with(SelectorHashSet::new) .insert(selector.clone()); } if let SimpleSelector::Pseudo(Pseudo { selector: Some(simple_selector), .. }) = simple { self.register_selector(*simple_selector, selector); } } } } } } /// Adds an extension to this extender. /// /// The `extender` is the selector for the style rule in which the extension /// is defined, and `target` is the selector passed to `@extend`. The `extend` /// provides the extend span and indicates whether the extension is optional. /// /// The `media_context` defines the media query context in which the extension /// is defined. It can only extend selectors within the same context. A `None` /// context indicates no media queries. pub fn add_extension( &mut self, extender: SelectorList, target: &SimpleSelector, extend: &ExtendRule, media_context: &Option>, span: Span, ) { let selectors = self.selectors.get(target).cloned(); let existing_extensions = self.extensions_by_extender.get(target).cloned(); let mut new_extensions: Option> = None; for complex in extender.components { let state = Extension { specificity: complex.max_specificity(), extender: complex.clone(), target: Some(target.clone()), span, media_context: media_context.clone(), is_optional: extend.is_optional, is_original: false, left: None, right: None, }; let sources = self .extensions .entry(target.clone()) .or_insert_with(IndexMap::new); if let Some(existing_state) = sources.get(&complex) { // If there's already an extend from `extender` to `target`, we don't need // to re-run the extension. We may need to mark the extension as // mandatory, though. let mut new_val = MergedExtension::merge(existing_state.clone(), state).unwrap(); sources.get_mut(&complex).replace(&mut new_val); continue; } sources.insert(complex.clone(), state.clone()); for component in complex.components.clone() { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { self.extensions_by_extender .entry(simple.clone()) .or_insert_with(Vec::new) .push(state.clone()); // Only source specificity for the original selector is relevant. // Selectors generated by `@extend` don't get new specificity. self.source_specificity .entry(simple.clone()) .or_insert_with(|| complex.max_specificity()); } } } if selectors.is_some() || existing_extensions.is_some() { new_extensions .get_or_insert_with(IndexMap::new) .insert(complex.clone(), state.clone()); } } let new_extensions = if let Some(new) = new_extensions { new } else { return; }; let mut new_extensions_by_target = HashMap::new(); new_extensions_by_target.insert(target.clone(), new_extensions); if let Some(existing_extensions) = existing_extensions { let additional_extensions = self.extend_existing_extensions(existing_extensions, &new_extensions_by_target); if let Some(additional_extensions) = additional_extensions { map_add_all_2(&mut new_extensions_by_target, additional_extensions); } } if let Some(selectors) = selectors { self.extend_existing_selectors(selectors, &new_extensions_by_target); } } /// Extend `extensions` using `new_extensions`. /// /// Note that this does duplicate some work done by /// `Extender::extend_existing_selectors`, but it's necessary to expand each extension's /// extender separately without reference to the full selector list, so that /// relevant results don't get trimmed too early. /// /// Returns extensions that should be added to `new_extensions` before /// extending selectors in order to properly handle extension loops such as: ///```foo /// .c {x: y; @extend .a} /// .x.y.a {@extend .b} /// .z.b {@extend .c} ///``` /// Returns `None` if there are no extensions to add. fn extend_existing_extensions( &mut self, extensions: Vec, new_extensions: &HashMap>, ) -> Option>> { let mut additional_extensions: Option< HashMap>, > = None; for extension in extensions { let mut sources = self .extensions .get(&extension.target.clone().unwrap()) .unwrap() .clone(); // `extend_existing_selectors` would have thrown already. let selectors: Vec = if let Some(v) = self.extend_complex( extension.extender.clone(), Some(new_extensions), &extension.media_context, ) { v } else { continue; }; // todo: when we add error handling, this error is special /* } on SassException catch (error) { throw SassException( "From ${extension.extenderSpan.message('')}\n" "${error.message}", error.span); } */ let contains_extension = selectors.first() == Some(&extension.extender); let mut first = false; for complex in selectors { // If the output contains the original complex selector, there's no // need to recreate it. if contains_extension && first { first = false; continue; } let with_extender = extension.clone().with_extender(complex.clone()); let existing_extension = sources.get(&complex); if let Some(existing_extension) = existing_extension.cloned() { sources.get_mut(&complex).replace( &mut MergedExtension::merge(existing_extension.clone(), with_extender) .unwrap(), ); } else { sources .get_mut(&complex) .replace(&mut with_extender.clone()); for component in complex.components.clone() { if let ComplexSelectorComponent::Compound(component) = component { for simple in component.components { self.extensions_by_extender .entry(simple) .or_insert_with(Vec::new) .push(with_extender.clone()); } } } if new_extensions.contains_key(&extension.target.clone().unwrap()) { additional_extensions .get_or_insert_with(HashMap::new) .entry(extension.target.clone().unwrap()) .or_insert_with(IndexMap::new) .insert(complex.clone(), with_extender.clone()); } } } // If `selectors` doesn't contain `extension.extender`, for example if it // was replaced due to :not() expansion, we must get rid of the old // version. if !contains_extension { // todo: evaluate whether we could get away with swap_remove sources.shift_remove(&extension.extender); } } additional_extensions } /// Extend `extensions` using `new_extensions`. fn extend_existing_selectors( &mut self, selectors: SelectorHashSet, new_extensions: &HashMap>, ) { for mut selector in selectors { let old_value = selector.clone().into_selector().0; selector.set_inner(self.extend_list( old_value.clone(), Some(new_extensions), &self.media_contexts.get(&old_value).cloned(), )); /* todo: error handling } on SassException catch (error) { throw SassException( "From ${selector.span.message('')}\n" "${error.message}", error.span); } */ // If no extends actually happened (for example because unification // failed), we don't need to re-register the selector. let selector_as_selector = selector.clone().into_selector().0; if old_value == selector_as_selector { continue; } self.register_selector(selector_as_selector, &selector); } } } /// Rotates the element in list from `start` (inclusive) to `end` (exclusive) /// one index higher, looping the final element back to `start`. fn rotate_slice(list: &mut VecDeque, start: usize, end: usize) { let mut element = list.get(end - 1).unwrap().clone(); for i in start..end { let next = list.get(i).unwrap().clone(); list[i] = element; element = next; } } /// Like `HashMap::extend`, but for two-layer maps. /// /// This avoids copying inner maps from `source` if possible. fn map_add_all_2( destination: &mut HashMap>, source: HashMap>, ) { for (key, mut inner) in source { if destination.contains_key(&key) { destination .get_mut(&key) .get_or_insert(&mut IndexMap::new()) .extend(inner); } else { destination.get_mut(&key).replace(&mut inner); } } } grass_compiler-0.13.4/src/selector/extend/rule.rs000064400000000000000000000001241046102023000201320ustar 00000000000000#[derive(Clone, Debug)] pub(crate) struct ExtendRule { pub is_optional: bool, } grass_compiler-0.13.4/src/selector/list.rs000064400000000000000000000227241046102023000166610ustar 00000000000000use std::{ collections::VecDeque, fmt::{self, Write}, hash::{Hash, Hasher}, mem, }; use codemap::Span; use super::{unify_complex, ComplexSelector, ComplexSelectorComponent}; use crate::{ common::{Brackets, ListSeparator, QuoteKind}, error::SassResult, value::Value, }; /// A selector list. /// /// A selector list is composed of `ComplexSelector`s. It matches an element /// that matches any of the component selectors. #[derive(Clone, Debug)] pub(crate) struct SelectorList { /// The components of this selector. /// /// This is never empty. pub components: Vec, pub span: Span, } impl PartialEq for SelectorList { fn eq(&self, other: &SelectorList) -> bool { self.components == other.components } } impl Eq for SelectorList {} impl Hash for SelectorList { fn hash(&self, state: &mut H) { self.components.hash(state); } } impl fmt::Display for SelectorList { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let complexes = self.components.iter().filter(|c| !c.is_invisible()); let mut first = true; for complex in complexes { if first { first = false; } else { f.write_char(',')?; if complex.line_break { f.write_char('\n')?; } else { f.write_char(' ')?; } } write!(f, "{}", complex)?; } Ok(()) } } impl SelectorList { pub fn is_invisible(&self) -> bool { self.components.iter().all(ComplexSelector::is_invisible) } pub fn contains_parent_selector(&self) -> bool { self.components .iter() .any(ComplexSelector::contains_parent_selector) } pub const fn new(span: Span) -> Self { Self { components: Vec::new(), span, } } pub fn is_empty(&self) -> bool { self.components.is_empty() } /// Returns a `SassScript` list that represents this selector. /// /// This has the same format as a list returned by `selector-parse()`. pub fn to_sass_list(self) -> Value { Value::List( self.components .into_iter() .map(|complex| { Value::List( complex .components .into_iter() .map(|complex_component| { Value::String(complex_component.to_string(), QuoteKind::None) }) .collect(), ListSeparator::Space, Brackets::None, ) }) .collect(), ListSeparator::Comma, Brackets::None, ) } /// Returns a `SelectorList` that matches only elements that are matched by /// both this and `other`. /// /// If no such list can be produced, returns `None`. pub fn unify(self, other: &Self) -> Option { let contents: Vec = self .components .into_iter() .flat_map(|c1| { other.clone().components.into_iter().flat_map(move |c2| { let unified: Option>> = unify_complex(vec![c1.components.clone(), c2.components]); if let Some(u) = unified { u.into_iter() .map(|c| ComplexSelector::new(c, false)) .collect() } else { Vec::new() } }) }) .collect(); if contents.is_empty() { return None; } Some(Self { components: contents, span: self.span.merge(other.span), }) } /// Returns a new list with all `SimpleSelector::Parent`s replaced with `parent`. /// /// If `implicit_parent` is true, this treats `ComplexSelector`s that don't /// contain an explicit `SimpleSelector::Parent` as though they began with one. /// /// The given `parent` may be `None`, indicating that this has no parents. If /// so, this list is returned as-is if it doesn't contain any explicit /// `SimpleSelector::Parent`s. If it does, this returns a `SassError`. pub fn resolve_parent_selectors( self, parent: Option, implicit_parent: bool, ) -> SassResult { let parent = match parent { Some(p) => p, None => { if !self.contains_parent_selector() { return Ok(self); } return Err(( "Top-level selectors may not contain the parent selector \"&\".", self.span, ) .into()); } }; Ok(Self { components: flatten_vertically( self.components .into_iter() .map(|complex| { if !complex.contains_parent_selector() { if !implicit_parent { return Ok(vec![complex]); } return Ok(parent .clone() .components .into_iter() .map(move |parent_complex| { let mut components = parent_complex.components; components.append(&mut complex.components.clone()); ComplexSelector::new( components, complex.line_break || parent_complex.line_break, ) }) .collect()); } let mut new_complexes: Vec> = vec![Vec::new()]; let mut line_breaks = vec![false]; for component in complex.components { if component.is_compound() { let resolved = match component .clone() .resolve_parent_selectors(self.span, parent.clone())? { Some(r) => r, None => { for new_complex in &mut new_complexes { new_complex.push(component.clone()); } continue; } }; let previous_complexes = mem::take(&mut new_complexes); let previous_line_breaks = mem::take(&mut line_breaks); for (i, new_complex) in previous_complexes.into_iter().enumerate() { // todo: use .get(i) let line_break = previous_line_breaks[i]; for mut resolved_complex in resolved.clone() { let mut new_this_complex = new_complex.clone(); new_this_complex.append(&mut resolved_complex.components); new_complexes.push(mem::take(&mut new_this_complex)); line_breaks.push(line_break || resolved_complex.line_break); } } } else { for new_complex in &mut new_complexes { new_complex.push(component.clone()); } } } let mut i = 0; Ok(new_complexes .into_iter() .map(|new_complex| { i += 1; ComplexSelector::new(new_complex, line_breaks[i - 1]) }) .collect()) }) .collect::>>>()?, ), span: self.span, }) } pub fn is_superselector(&self, other: &Self) -> bool { other.components.iter().all(|complex1| { self.components .iter() .any(|complex2| complex2.is_super_selector(complex1)) }) } } fn flatten_vertically(iterable: Vec>) -> Vec { let mut queues: Vec> = iterable.into_iter().map(VecDeque::from).collect(); let mut result = Vec::new(); while !queues.is_empty() { for queue in &mut queues { if queue.is_empty() { continue; } result.push(queue.pop_front().unwrap()); } queues.retain(|queue| !queue.is_empty()); } result } grass_compiler-0.13.4/src/selector/mod.rs000064400000000000000000000032031046102023000164540ustar 00000000000000use codemap::Span; use crate::{error::SassResult, value::Value}; pub(crate) use attribute::Attribute; pub(crate) use common::*; pub(crate) use complex::*; pub(crate) use compound::*; pub(crate) use extend::*; pub(crate) use list::*; pub(crate) use parse::*; pub(crate) use simple::*; mod attribute; mod common; mod complex; mod compound; mod extend; mod list; mod parse; mod simple; // todo: delete this selector wrapper #[derive(Clone, Debug, Eq, PartialEq)] pub struct Selector(pub(crate) SelectorList); impl Selector { /// Small wrapper around `SelectorList`'s method that turns an empty parent selector /// into `None`. This is a hack and in the future should be replaced. // todo: take Option for parent pub fn resolve_parent_selectors( &self, parent: &Self, implicit_parent: bool, ) -> SassResult { Ok(Self(self.0.clone().resolve_parent_selectors( if parent.is_empty() { None } else { Some(parent.0.clone()) }, implicit_parent, )?)) } pub fn is_super_selector(&self, other: &Self) -> bool { self.0.is_superselector(&other.0) } pub fn contains_parent_selector(&self) -> bool { self.0.contains_parent_selector() } pub fn is_empty(&self) -> bool { self.0.is_empty() } pub const fn new(span: Span) -> Selector { Selector(SelectorList::new(span)) } pub fn into_value(self) -> Value { self.0.to_sass_list() } pub fn unify(self, other: &Self) -> Option { Some(Selector(self.0.unify(&other.0)?)) } } grass_compiler-0.13.4/src/selector/parse.rs000064400000000000000000000410131046102023000170100ustar 00000000000000use codemap::Span; use crate::{common::unvendor, error::SassResult, lexer::Lexer, parse::BaseParser, Token}; use super::{ Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo, QualifiedName, SelectorList, SimpleSelector, }; #[derive(PartialEq)] enum DevouredWhitespace { /// Some whitespace was found Whitespace, /// A newline and potentially other whitespace was found Newline, /// No whitespace was found None, } /// Pseudo-class selectors that take unadorned selectors as arguments. const SELECTOR_PSEUDO_CLASSES: [&str; 9] = [ "not", "matches", "where", "is", "current", "any", "has", "host", "host-context", ]; /// Pseudo-element selectors that take unadorned selectors as arguments. const SELECTOR_PSEUDO_ELEMENTS: [&str; 1] = ["slotted"]; pub(crate) struct SelectorParser { /// Whether this parser allows the parent selector `&`. allows_parent: bool, /// Whether this parser allows placeholder selectors beginning with `%`. allows_placeholder: bool, pub toks: Lexer, span: Span, } impl BaseParser for SelectorParser { fn toks(&self) -> &Lexer { &self.toks } fn toks_mut(&mut self) -> &mut Lexer { &mut self.toks } } impl SelectorParser { pub fn new(toks: Lexer, allows_parent: bool, allows_placeholder: bool, span: Span) -> Self { Self { toks, allows_parent, allows_placeholder, span, } } pub fn parse(mut self) -> SassResult { let tmp = self.parse_selector_list()?; if self.toks.peek().is_some() { return Err(("expected selector.", self.span).into()); } Ok(tmp) } fn parse_selector_list(&mut self) -> SassResult { let mut components = vec![self.parse_complex_selector(false)?]; self.whitespace()?; let mut line_break = false; while self.scan_char(',') { line_break = self.eat_whitespace() == DevouredWhitespace::Newline || line_break; match self.toks.peek() { Some(Token { kind: ',', .. }) => continue, Some(..) => {} None => break, } components.push(self.parse_complex_selector(line_break)?); line_break = false; } Ok(SelectorList { components, span: self.span, }) } fn eat_whitespace(&mut self) -> DevouredWhitespace { let text = self.raw_text(Self::whitespace); if text.contains('\n') { DevouredWhitespace::Newline } else if !text.is_empty() { DevouredWhitespace::Whitespace } else { DevouredWhitespace::None } } /// Consumes a complex selector. /// /// If `line_break` is `true`, that indicates that there was a line break /// before this selector. fn parse_complex_selector(&mut self, line_break: bool) -> SassResult { let mut components = Vec::new(); loop { self.whitespace()?; // todo: can we do while let Some(..) = self.toks.peek() ? match self.toks.peek() { Some(Token { kind: '+', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::NextSibling, )); } Some(Token { kind: '>', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator(Combinator::Child)); } Some(Token { kind: '~', .. }) => { self.toks.next(); components.push(ComplexSelectorComponent::Combinator( Combinator::FollowingSibling, )); } Some(Token { kind: '[', .. }) | Some(Token { kind: '.', .. }) | Some(Token { kind: '#', .. }) | Some(Token { kind: '%', .. }) | Some(Token { kind: ':', .. }) // todo: ampersand? | Some(Token { kind: '&', .. }) | Some(Token { kind: '*', .. }) | Some(Token { kind: '|', .. }) => { components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } Some(..) => { if !self.looking_at_identifier() { break; } components.push(ComplexSelectorComponent::Compound( self.parse_compound_selector()?, )); if let Some(Token { kind: '&', .. }) = self.toks.peek() { return Err(("\"&\" may only used at the beginning of a compound selector.", self.span).into()); } } None => break, } } if components.is_empty() { return Err(("expected selector.", self.span).into()); } Ok(ComplexSelector::new(components, line_break)) } fn parse_compound_selector(&mut self) -> SassResult { let mut components = vec![self.parse_simple_selector(None)?]; while let Some(Token { kind, .. }) = self.toks.peek() { if !is_simple_selector_start(kind) { break; } components.push(self.parse_simple_selector(Some(false))?); } Ok(CompoundSelector { components }) } /// Consumes a simple selector. /// /// If `allows_parent` is `Some`, this will override `self.allows_parent`. If `allows_parent` /// is `None`, it will fallback to `self.allows_parent`. fn parse_simple_selector(&mut self, allows_parent: Option) -> SassResult { match self.toks.peek() { Some(Token { kind: '[', .. }) => self.parse_attribute_selector(), Some(Token { kind: '.', .. }) => self.parse_class_selector(), Some(Token { kind: '#', .. }) => self.parse_id_selector(), Some(Token { kind: '%', .. }) => { if !self.allows_placeholder { return Err(("Placeholder selectors aren't allowed here.", self.span).into()); } self.parse_placeholder_selector() } Some(Token { kind: ':', .. }) => self.parse_pseudo_selector(), Some(Token { kind: '&', .. }) => { let allows_parent = allows_parent.unwrap_or(self.allows_parent); if !allows_parent { return Err(("Parent selectors aren't allowed here.", self.span).into()); } self.parse_parent_selector() } _ => self.parse_type_or_universal_selector(), } } fn parse_attribute_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Attribute(Box::new(Attribute::from_tokens( self, )?))) } fn parse_class_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Class(self.parse_identifier(false, false)?)) } fn parse_id_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Id(self.parse_identifier(false, false)?)) } fn parse_pseudo_selector(&mut self) -> SassResult { self.toks.next(); let element = self.scan_char(':'); let name = self.parse_identifier(false, false)?; match self.toks.peek() { Some(Token { kind: '(', .. }) => self.toks.next(), _ => { return Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), name, selector: None, is_syntactic_class: !element, argument: None, span: self.span, })); } }; self.whitespace()?; let unvendored = unvendor(&name); let mut argument: Option> = None; let mut selector: Option> = None; if element { // todo: lowercase? if SELECTOR_PSEUDO_ELEMENTS.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.whitespace()?; } else { argument = Some(self.declaration_value(true)?.into_boxed_str()); } self.expect_char(')')?; } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { selector = Some(Box::new(self.parse_selector_list()?)); self.whitespace()?; self.expect_char(')')?; } else if unvendored == "nth-child" || unvendored == "nth-last-child" { let mut this_arg = self.parse_a_n_plus_b()?; self.whitespace()?; let last_was_whitespace = matches!( self.toks.peek_n_backwards(1), Some(Token { kind: ' ' | '\t' | '\n' | '\r', .. }) ); if last_was_whitespace && !matches!(self.toks.peek(), Some(Token { kind: ')', .. })) { self.expect_identifier("of", false)?; this_arg.push_str(" of"); self.whitespace()?; selector = Some(Box::new(self.parse_selector_list()?)); } self.expect_char(')')?; argument = Some(this_arg.into_boxed_str()); } else { argument = Some( self.declaration_value(true)? .trim_end() .to_owned() .into_boxed_str(), ); self.expect_char(')')?; } Ok(SimpleSelector::Pseudo(Pseudo { is_class: !element && !is_fake_pseudo_element(&name), name, selector, is_syntactic_class: !element, argument, span: self.span, })) } fn parse_parent_selector(&mut self) -> SassResult { self.toks.next(); let suffix = if self.looking_at_identifier_body() { let mut buffer = String::new(); self.parse_identifier_body(&mut buffer, false, false)?; Some(buffer) } else { None }; Ok(SimpleSelector::Parent(suffix)) } fn parse_placeholder_selector(&mut self) -> SassResult { self.toks.next(); Ok(SimpleSelector::Placeholder( self.parse_identifier(false, false)?, )) } /// Consumes a type selector or a universal selector. /// /// These are combined because either one could start with `*`. fn parse_type_or_universal_selector(&mut self) -> SassResult { match self.toks.peek() { Some(Token { kind: '*', .. }) => { self.toks.next(); if let Some(Token { kind: '|', .. }) = self.toks.peek() { self.toks.next(); if let Some(Token { kind: '*', .. }) = self.toks.peek() { self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Asterisk)); } return Ok(SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Asterisk, })); } return Ok(SimpleSelector::Universal(Namespace::None)); } Some(Token { kind: '|', .. }) => { self.toks.next(); match self.toks.peek() { Some(Token { kind: '*', .. }) => { self.toks.next(); return Ok(SimpleSelector::Universal(Namespace::Empty)); } _ => { return Ok(SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Empty, })); } } } _ => {} } let name_or_namespace = self.parse_identifier(false, false)?; Ok(match self.toks.peek() { Some(Token { kind: '|', .. }) => { self.toks.next(); if let Some(Token { kind: '*', .. }) = self.toks.peek() { self.toks.next(); SimpleSelector::Universal(Namespace::Other(name_or_namespace.into_boxed_str())) } else { SimpleSelector::Type(QualifiedName { ident: self.parse_identifier(false, false)?, namespace: Namespace::Other(name_or_namespace.into_boxed_str()), }) } } Some(..) | None => SimpleSelector::Type(QualifiedName { ident: name_or_namespace, namespace: Namespace::None, }), }) } /// Consumes an [`An+B` production][An+B] and returns its text. /// /// [An+B]: https://drafts.csswg.org/css-syntax-3/#anb-microsyntax fn parse_a_n_plus_b(&mut self) -> SassResult { let mut buf = String::new(); match self.toks.peek() { Some(Token { kind: 'e', .. }) | Some(Token { kind: 'E', .. }) => { self.expect_identifier("even", false)?; return Ok("even".to_owned()); } Some(Token { kind: 'o', .. }) | Some(Token { kind: 'O', .. }) => { self.expect_identifier("odd", false)?; return Ok("odd".to_owned()); } Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) => { buf.push(t.kind); self.toks.next(); } _ => {} } match self.toks.peek() { Some(t) if t.kind.is_ascii_digit() => { while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); self.toks.next(); } self.whitespace()?; if !self.scan_ident_char('n', false)? { return Ok(buf); } } Some(..) => self.expect_ident_char('n', false)?, None => return Err(("expected more input.", self.span).into()), } buf.push('n'); self.whitespace()?; if let Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) = self.toks.peek() { buf.push(t.kind); self.toks.next(); self.whitespace()?; match self.toks.peek() { Some(t) if !t.kind.is_ascii_digit() => { return Err(("Expected a number.", self.span).into()) } None => return Err(("Expected a number.", self.span).into()), Some(..) => {} } while let Some(t) = self.toks.peek() { if !t.kind.is_ascii_digit() { break; } buf.push(t.kind); self.toks.next(); } } Ok(buf) } } /// Returns whether `c` can start a simple selector other than a type /// selector. fn is_simple_selector_start(c: char) -> bool { matches!(c, '*' | '[' | '.' | '#' | '%' | ':') } /// Returns whether `name` is the name of a pseudo-element that can be written /// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or /// `:first-letter`) fn is_fake_pseudo_element(name: &str) -> bool { match name.as_bytes().first() { Some(b'a') | Some(b'A') => name.to_ascii_lowercase() == "after", Some(b'b') | Some(b'B') => name.to_ascii_lowercase() == "before", Some(b'f') | Some(b'F') => matches!( name.to_ascii_lowercase().as_str(), "first-line" | "first-letter" ), _ => false, } } grass_compiler-0.13.4/src/selector/simple.rs000064400000000000000000000536661046102023000172100ustar 00000000000000use std::{ fmt::{self, Write}, hash::{Hash, Hasher}, }; use codemap::Span; use crate::{common::unvendor, error::SassResult}; use super::{ Attribute, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, QualifiedName, SelectorList, Specificity, }; const SUBSELECTOR_PSEUDOS: [&str; 6] = [ "matches", "where", "is", "any", "nth-child", "nth-last-child", ]; const BASE_SPECIFICITY: i32 = 1000; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) enum SimpleSelector { /// * Universal(Namespace), /// A pseudo-class or pseudo-element selector. /// /// The semantics of a specific pseudo selector depends on its name. Some /// selectors take arguments, including other selectors. Sass manually encodes /// logic for each pseudo selector that takes a selector as an argument, to /// ensure that extension and other selector operations work properly. Pseudo(Pseudo), /// A type selector. /// /// This selects elements whose name equals the given name. Type(QualifiedName), /// A placeholder selector. /// /// This doesn't match any elements. It's intended to be extended using /// `@extend`. It's not a plain CSS selector—it should be removed before /// emitting a CSS document. Placeholder(String), /// A selector that matches the parent in the Sass stylesheet. /// `&` /// /// This is not a plain CSS selector—it should be removed before emitting a CSS /// document. /// /// The parameter is the suffix that will be added to the parent selector after /// it's been resolved. /// /// This is assumed to be a valid identifier suffix. It may be `None`, /// indicating that the parent selector will not be modified. Parent(Option), Id(String), /// A class selector. /// /// This selects elements whose `class` attribute contains an identifier with /// the given name. Class(String), Attribute(Box), } impl fmt::Display for SimpleSelector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Id(name) => write!(f, "#{}", name), Self::Class(name) => write!(f, ".{}", name), Self::Placeholder(name) => write!(f, "%{}", name), Self::Universal(namespace) => write!(f, "{}*", namespace), Self::Pseudo(pseudo) => write!(f, "{}", pseudo), Self::Type(name) => write!(f, "{}", name), Self::Attribute(attr) => write!(f, "{}", attr), Self::Parent(..) => unreachable!("It should not be possible to format `&`."), } } } impl SimpleSelector { /// The minimum possible specificity that this selector can have. /// /// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// can have a range of possible specificities. /// /// Specifity is represented in base 1000. The spec says this should be /// "sufficiently high"; it's extremely unlikely that any single selector /// sequence will contain 1000 simple selectors. pub fn min_specificity(&self) -> i32 { match self { Self::Universal(..) => 0, Self::Type(..) => 1, Self::Pseudo(pseudo) => pseudo.min_specificity(), Self::Id(..) => BASE_SPECIFICITY.pow(2_u32), _ => BASE_SPECIFICITY, } } /// The maximum possible specificity that this selector can have. /// /// Pseudo selectors that contain selectors, like `:not()` and `:matches()`, /// can have a range of possible specificities. pub fn max_specificity(&self) -> i32 { match self { Self::Universal(..) => 0, Self::Pseudo(pseudo) => pseudo.max_specificity(), _ => self.min_specificity(), } } pub fn is_invisible(&self) -> bool { match self { Self::Universal(..) | Self::Type(..) | Self::Id(..) | Self::Class(..) | Self::Attribute(..) => false, Self::Pseudo(Pseudo { name, selector, .. }) => { name != "not" && selector.as_ref().map_or(false, |sel| sel.is_invisible()) } Self::Placeholder(..) => true, Self::Parent(..) => unreachable!("parent selectors should be resolved at this point"), } } pub fn add_suffix(&mut self, suffix: &str, span: Span) -> SassResult<()> { match self { Self::Type(name) => name.ident.push_str(suffix), Self::Placeholder(name) | Self::Id(name) | Self::Class(name) | Self::Pseudo(Pseudo { name, argument: None, selector: None, .. }) => name.push_str(suffix), // todo: add test for this? _ => return Err((format!("Invalid parent selector \"{}\"", self), span).into()), }; Ok(()) } pub fn is_universal(&self) -> bool { matches!(self, Self::Universal(..)) } pub fn is_pseudo(&self) -> bool { matches!(self, Self::Pseudo { .. }) } pub fn is_parent(&self) -> bool { matches!(self, Self::Parent(..)) } pub fn is_id(&self) -> bool { matches!(self, Self::Id(..)) } pub fn is_type(&self) -> bool { matches!(self, Self::Type(..)) } pub fn unify(self, compound: Vec) -> Option> { match self { Self::Type(..) => self.unify_type(compound), Self::Universal(..) => self.unify_universal(compound), Self::Pseudo { .. } => self.unify_pseudo(compound), Self::Id(..) => { if compound .iter() .any(|simple| simple.is_id() && simple != &self) { return None; } self.unify_default(compound) } _ => self.unify_default(compound), } } /// Returns the components of a `CompoundSelector` that matches only elements /// matched by both this and `compound`. /// /// By default, this just returns a copy of `compound` with this selector /// added to the end, or returns the original array if this selector already /// exists in it. /// /// Returns `None` if unification is impossible—for example, if there are /// multiple ID selectors. fn unify_default(self, mut compound: Vec) -> Option> { if compound.len() == 1 && compound[0].is_universal() { return compound.swap_remove(0).unify(vec![self]); } if compound.contains(&self) { return Some(compound); } let mut result: Vec = Vec::new(); let mut added_this = false; for simple in compound { if !added_this && simple.is_pseudo() { result.push(self.clone()); added_this = true; } result.push(simple); } if !added_this { result.push(self); } Some(result) } fn unify_universal(self, mut compound: Vec) -> Option> { if let Self::Universal(..) | Self::Type(..) = compound[0] { let mut unified = vec![self.unify_universal_and_element(&compound[0])?]; unified.extend(compound.into_iter().skip(1)); return Some(unified); } if self != Self::Universal(Namespace::Asterisk) && self != Self::Universal(Namespace::None) { let mut v = vec![self]; v.append(&mut compound); return Some(v); } if !compound.is_empty() { return Some(compound); } Some(vec![self]) } /// Returns a `SimpleSelector` that matches only elements that are matched by /// both `selector1` and `selector2`, which must both be either /// `SimpleSelector::Universal`s or `SimpleSelector::Type`s. /// /// If no such selector can be produced, returns `None`. fn unify_universal_and_element(&self, other: &Self) -> Option { let namespace1; let name1; if let SimpleSelector::Type(name) = self.clone() { namespace1 = name.namespace; name1 = name.ident; } else if let SimpleSelector::Universal(namespace) = self.clone() { namespace1 = namespace; name1 = String::new(); } else { unreachable!("{:?} must be a universal selector or a type selector", self); } let namespace2; let mut name2 = String::new(); if let SimpleSelector::Universal(namespace) = other { namespace2 = namespace.clone(); } else if let SimpleSelector::Type(name) = other { namespace2 = name.namespace.clone(); name2 = name.ident.clone(); } else { unreachable!( "{:?} must be a universal selector or a type selector", other ); } let namespace = if namespace1 == namespace2 || namespace2 == Namespace::Asterisk { namespace1 } else if namespace1 == Namespace::Asterisk { namespace2 } else { return None; }; let name = if name1 == name2 || name2.is_empty() { name1 } else if name1.is_empty() || name1 == "*" { name2 } else { return None; }; Some(if name.is_empty() { SimpleSelector::Universal(namespace) } else { SimpleSelector::Type(QualifiedName { namespace, ident: name, }) }) } fn unify_type(self, mut compound: Vec) -> Option> { if let Self::Universal(..) | Self::Type(..) = compound[0] { let mut unified = vec![self.unify_universal_and_element(&compound[0])?]; unified.extend(compound.into_iter().skip(1)); Some(unified) } else { let mut unified = vec![self]; unified.append(&mut compound); Some(unified) } } fn unify_pseudo(self, mut compound: Vec) -> Option> { if compound.len() == 1 && compound[0].is_universal() { return compound.remove(0).unify(vec![self]); } if compound.contains(&self) { return Some(compound); } let mut result = Vec::new(); let mut added_self = false; for simple in compound { if let Self::Pseudo(Pseudo { is_class: false, .. }) = simple { // A given compound selector may only contain one pseudo element. If // `compound` has a different one than `self`, unification fails. if let Self::Pseudo(Pseudo { is_class: false, .. }) = self { return None; } // Otherwise, this is a pseudo selector and should come before pseduo // elements. result.push(self.clone()); added_self = true; } result.push(simple); } if !added_self { result.push(self); } Some(result) } pub fn is_super_selector_of_compound(&self, compound: &CompoundSelector) -> bool { compound.components.iter().any(|their_simple| { if self == their_simple { return true; } if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), name, .. }) = their_simple { if SUBSELECTOR_PSEUDOS.contains(&unvendor(name)) { return sel.components.iter().all(|complex| { if complex.components.len() != 1 { return false; }; complex .components .first() .unwrap() .as_compound() .components .contains(self) }); } false } else { false } }) } } #[derive(Clone, Debug)] pub(crate) struct Pseudo { /// The name of this selector. pub name: String, /// Whether this is a pseudo-class selector. /// /// If this is false, this is a pseudo-element selector pub is_class: bool, /// Whether this is syntactically a pseudo-class selector. /// /// This is the same as `is_class` unless this selector is a pseudo-element /// that was written syntactically as a pseudo-class (`:before`, `:after`, /// `:first-line`, or `:first-letter`). /// /// If this is false, it is syntactically a psuedo-element pub is_syntactic_class: bool, /// The non-selector argument passed to this selector. /// /// This is `None` if there's no argument. If `argument` and `selector` are /// both non-`None`, the selector follows the argument. pub argument: Option>, /// The selector argument passed to this selector. /// /// This is `None` if there's no selector. If `argument` and `selector` are /// both non-`None`, the selector follows the argument. pub selector: Option>, pub span: Span, } impl PartialEq for Pseudo { fn eq(&self, other: &Pseudo) -> bool { self.name == other.name && self.is_class == other.is_class && self.argument == other.argument && self.selector == other.selector } } impl Eq for Pseudo {} impl Hash for Pseudo { fn hash(&self, state: &mut H) { self.name.hash(state); self.is_class.hash(state); self.argument.hash(state); self.selector.hash(state); } } impl fmt::Display for Pseudo { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if let Some(sel) = &self.selector { if self.name == "not" && sel.is_invisible() { return Ok(()); } } f.write_char(':')?; if !self.is_syntactic_class { f.write_char(':')?; } f.write_str(&self.name)?; if self.argument.is_none() && self.selector.is_none() { return Ok(()); } f.write_char('(')?; if let Some(arg) = &self.argument { f.write_str(arg)?; if self.selector.is_some() { f.write_char(' ')?; } } if let Some(sel) = &self.selector { write!(f, "{}", sel)?; } f.write_char(')') } } impl Pseudo { /// Returns whether `pseudo1` is a superselector of `compound2`. /// /// That is, whether `pseudo1` matches every element that `compound2` matches, as well /// as possibly additional elements. /// /// This assumes that `pseudo1`'s `selector` argument is not `None`. /// /// If `parents` is passed, it represents the parents of `compound`. This is /// relevant for pseudo selectors with selector arguments, where we may need to /// know if the parent selectors in the selector argument match `parents`. pub fn is_super_selector( &self, compound: &CompoundSelector, parents: Option>, ) -> bool { debug_assert!(self.selector.is_some()); match self.normalized_name() { "matches" | "is" | "any" | "where" => { selector_pseudos_named(compound.clone(), &self.name, true).any(move |pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(&pseudo2.selector.unwrap()) }) || self .selector .as_ref() .unwrap() .components .iter() .any(move |complex1| { let mut components = parents.clone().unwrap_or_default(); components.push(ComplexSelectorComponent::Compound(compound.clone())); complex1.is_super_selector(&ComplexSelector::new(components, false)) }) } "has" | "host" | "host-context" => { selector_pseudos_named(compound.clone(), &self.name, true).any(|pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(&pseudo2.selector.unwrap()) }) } "slotted" => { selector_pseudos_named(compound.clone(), &self.name, false).any(|pseudo2| { self.selector .as_ref() .unwrap() .is_superselector(pseudo2.selector.as_ref().unwrap()) }) } "not" => self .selector .as_ref() .unwrap() .components .iter() .all(|complex| { compound.components.iter().any(|simple2| { if let SimpleSelector::Type(..) = simple2 { let compound1 = complex.components.last(); if let Some(ComplexSelectorComponent::Compound(c)) = compound1 { c.components .iter() .any(|simple1| simple1.is_type() && simple1 != simple2) } else { false } } else if let SimpleSelector::Id(..) = simple2 { let compound1 = complex.components.last(); if let Some(ComplexSelectorComponent::Compound(c)) = compound1 { c.components .iter() .any(|simple1| simple1.is_id() && simple1 != simple2) } else { false } } else if let SimpleSelector::Pseudo(Pseudo { selector: Some(sel), name, .. }) = simple2 { if name != &self.name { return false; } sel.is_superselector(&SelectorList { components: vec![complex.clone()], span: self.span, }) } else { false } }) }), "current" => selector_pseudos_named(compound.clone(), &self.name, self.is_class) .any(|pseudo2| self.selector == pseudo2.selector), "nth-child" | "nth-last-child" => compound.components.iter().any(|pseudo2| { if let SimpleSelector::Pseudo( pseudo @ Pseudo { selector: Some(..), .. }, ) = pseudo2 { pseudo.name == self.name && pseudo.argument == self.argument && self .selector .as_ref() .unwrap() .is_superselector(pseudo.selector.as_ref().unwrap()) } else { false } }), _ => unreachable!(), } } #[allow(clippy::missing_const_for_fn)] pub fn with_selector(self, selector: Option>) -> Self { Self { selector, ..self } } pub fn max_specificity(&self) -> i32 { self.specificity().max } pub fn min_specificity(&self) -> i32 { self.specificity().min } pub fn specificity(&self) -> Specificity { if !self.is_class { return Specificity { min: 1, max: 1 }; } let selector = match &self.selector { Some(sel) => sel, None => { return Specificity { min: BASE_SPECIFICITY, max: BASE_SPECIFICITY, } } }; if self.name == "not" { let mut min = 0; let mut max = 0; for complex in &selector.components { min = min.max(complex.min_specificity()); max = max.max(complex.max_specificity()); } Specificity { min, max } } else { // This is higher than any selector's specificity can actually be. let mut min = BASE_SPECIFICITY.pow(3_u32); let mut max = 0; for complex in &selector.components { min = min.min(complex.min_specificity()); max = max.max(complex.max_specificity()); } Specificity { min, max } } } /// Like `name`, but without any vendor prefixes. pub fn normalized_name(&self) -> &str { unvendor(&self.name) } } /// Returns all pseudo selectors in `compound` that have a selector argument, /// and that have the given `name`. fn selector_pseudos_named( compound: CompoundSelector, name: &str, is_class: bool, ) -> impl Iterator + '_ { compound .components .into_iter() .filter_map(|c| { if let SimpleSelector::Pseudo(p) = c { Some(p) } else { None } }) .filter(move |p| p.is_class == is_class && p.selector.is_some() && p.name == name) } grass_compiler-0.13.4/src/serializer.rs000064400000000000000000001113461046102023000162360ustar 00000000000000use std::io::Write; use codemap::{CodeMap, Span}; use crate::{ ast::{CssStmt, MediaQuery, Style, SupportsRule}, color::{Color, ColorFormat, NAMED_COLORS}, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, error::SassResult, selector::{ Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, Pseudo, SelectorList, SimpleSelector, }, utils::hex_char_for, value::{ fuzzy_equals, ArgList, CalculationArg, CalculationName, SassCalculation, SassFunction, SassMap, SassNumber, Value, }, Options, }; pub(crate) fn serialize_selector_list( list: &SelectorList, options: &Options, span: Span, ) -> String { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.write_selector_list(list); serializer.finish_for_expr() } pub(crate) fn serialize_calculation_arg( arg: &CalculationArg, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.write_calculation_arg(arg)?; Ok(serializer.finish_for_expr()) } pub(crate) fn serialize_number( number: &SassNumber, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.visit_number(number)?; Ok(serializer.finish_for_expr()) } pub(crate) fn serialize_value(val: &Value, options: &Options, span: Span) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, false, span); serializer.visit_value(val, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_value(val: &Value, options: &Options, span: Span) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.visit_value(val, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_float(number: f64, options: &Options, span: Span) -> String { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.write_float(number); serializer.finish_for_expr() } pub(crate) fn inspect_map(map: &SassMap, options: &Options, span: Span) -> SassResult { let code_map = CodeMap::new(); let mut serializer = Serializer::new(options, &code_map, true, span); serializer.visit_map(map, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_function_ref( func: &SassFunction, options: &Options, span: Span, ) -> SassResult { let code_map = CodeMap::new(); let mut serializer = Serializer::new(options, &code_map, true, span); serializer.visit_function_ref(func, span)?; Ok(serializer.finish_for_expr()) } pub(crate) fn inspect_number( number: &SassNumber, options: &Options, span: Span, ) -> SassResult { let map = CodeMap::new(); let mut serializer = Serializer::new(options, &map, true, span); serializer.visit_number(number)?; Ok(serializer.finish_for_expr()) } pub(crate) struct Serializer<'a> { indentation: usize, options: &'a Options<'a>, inspect: bool, indent_width: usize, // todo: use this field _quote: bool, buffer: Vec, map: &'a CodeMap, span: Span, } impl<'a> Serializer<'a> { pub fn new(options: &'a Options<'a>, map: &'a CodeMap, inspect: bool, span: Span) -> Self { Self { inspect, _quote: true, indentation: 0, indent_width: 2, options, buffer: Vec::new(), map, span, } } fn omit_spaces_around_complex_component(&self, component: &ComplexSelectorComponent) -> bool { self.options.is_compressed() && matches!(component, ComplexSelectorComponent::Combinator(..)) } fn write_pseudo_selector(&mut self, pseudo: &Pseudo) { if let Some(sel) = &pseudo.selector { if pseudo.name == "not" && sel.is_invisible() { return; } } self.buffer.push(b':'); if !pseudo.is_syntactic_class { self.buffer.push(b':'); } self.buffer.extend_from_slice(pseudo.name.as_bytes()); if pseudo.argument.is_none() && pseudo.selector.is_none() { return; } self.buffer.push(b'('); if let Some(arg) = &pseudo.argument { self.buffer.extend_from_slice(arg.as_bytes()); if pseudo.selector.is_some() { self.buffer.push(b' '); } } if let Some(sel) = &pseudo.selector { self.write_selector_list(sel); } self.buffer.push(b')'); } fn write_namespace(&mut self, namespace: &Namespace) { match namespace { Namespace::Empty => self.buffer.push(b'|'), Namespace::Asterisk => self.buffer.extend_from_slice(b"*|"), Namespace::Other(namespace) => { self.buffer.extend_from_slice(namespace.as_bytes()); self.buffer.push(b'|'); } Namespace::None => {} } } fn write_simple_selector(&mut self, simple: &SimpleSelector) { match simple { SimpleSelector::Id(name) => { self.buffer.push(b'#'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Class(name) => { self.buffer.push(b'.'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Placeholder(name) => { self.buffer.push(b'%'); self.buffer.extend_from_slice(name.as_bytes()); } SimpleSelector::Universal(namespace) => { self.write_namespace(namespace); self.buffer.push(b'*'); } SimpleSelector::Pseudo(pseudo) => self.write_pseudo_selector(pseudo), SimpleSelector::Type(name) => { self.write_namespace(&name.namespace); self.buffer.extend_from_slice(name.ident.as_bytes()); } SimpleSelector::Attribute(attr) => write!(&mut self.buffer, "{}", attr).unwrap(), SimpleSelector::Parent(..) => unreachable!("It should not be possible to format `&`."), } } fn write_compound_selector(&mut self, compound: &CompoundSelector) { let mut did_write = false; for simple in &compound.components { if did_write { self.write_simple_selector(simple); } else { let len = self.buffer.len(); self.write_simple_selector(simple); if self.buffer.len() != len { did_write = true; } } } // If we emit an empty compound, it's because all of the components got // optimized out because they match all selectors, so we just emit the // universal selector. if !did_write { self.buffer.push(b'*'); } } fn write_complex_selector_component(&mut self, component: &ComplexSelectorComponent) { match component { ComplexSelectorComponent::Combinator(Combinator::NextSibling) => self.buffer.push(b'+'), ComplexSelectorComponent::Combinator(Combinator::Child) => self.buffer.push(b'>'), ComplexSelectorComponent::Combinator(Combinator::FollowingSibling) => { self.buffer.push(b'~') } ComplexSelectorComponent::Compound(compound) => self.write_compound_selector(compound), } } fn write_complex_selector(&mut self, complex: &ComplexSelector) { let mut last_component = None; for component in &complex.components { if let Some(c) = last_component { if !self.omit_spaces_around_complex_component(c) && !self.omit_spaces_around_complex_component(component) { self.buffer.push(b' '); } } self.write_complex_selector_component(component); last_component = Some(component); } } fn write_selector_list(&mut self, list: &SelectorList) { let complexes = list.components.iter().filter(|c| !c.is_invisible()); let mut first = true; for complex in complexes { if first { first = false; } else { self.buffer.push(b','); if complex.line_break { self.write_newline(); } else { self.write_optional_space(); } } self.write_complex_selector(complex); } } fn write_newline(&mut self) { if !self.options.is_compressed() { self.buffer.push(b'\n'); } } fn write_comma_separator(&mut self) { self.buffer.push(b','); self.write_optional_space(); } fn write_calculation_name(&mut self, name: CalculationName) { match name { CalculationName::Calc => self.buffer.extend_from_slice(b"calc"), CalculationName::Min => self.buffer.extend_from_slice(b"min"), CalculationName::Max => self.buffer.extend_from_slice(b"max"), CalculationName::Clamp => self.buffer.extend_from_slice(b"clamp"), } } fn visit_calculation(&mut self, calculation: &SassCalculation) -> SassResult<()> { self.write_calculation_name(calculation.name); self.buffer.push(b'('); if let Some((last, slice)) = calculation.args.split_last() { for arg in slice { self.write_calculation_arg(arg)?; self.write_comma_separator(); } self.write_calculation_arg(last)?; } self.buffer.push(b')'); Ok(()) } fn write_calculation_arg(&mut self, arg: &CalculationArg) -> SassResult<()> { match arg { CalculationArg::Number(num) => self.visit_number(num)?, CalculationArg::Calculation(calc) => { self.visit_calculation(calc)?; } CalculationArg::String(s) | CalculationArg::Interpolation(s) => { self.buffer.extend_from_slice(s.as_bytes()); } CalculationArg::Operation { lhs, op, rhs } => { let paren_left = match &**lhs { CalculationArg::Interpolation(..) => true, CalculationArg::Operation { op: op2, .. } => op2.precedence() < op.precedence(), _ => false, }; if paren_left { self.buffer.push(b'('); } self.write_calculation_arg(lhs)?; if paren_left { self.buffer.push(b')'); } let operator_whitespace = !self.options.is_compressed() || matches!(op, BinaryOp::Plus | BinaryOp::Minus); if operator_whitespace { self.buffer.push(b' '); } // todo: avoid allocation with `write_binary_operator` method self.buffer.extend_from_slice(op.to_string().as_bytes()); if operator_whitespace { self.buffer.push(b' '); } let paren_right = match &**rhs { CalculationArg::Interpolation(..) => true, CalculationArg::Operation { op: op2, .. } => { CalculationArg::parenthesize_calculation_rhs(*op, *op2) } _ => false, }; if paren_right { self.buffer.push(b'('); } self.write_calculation_arg(rhs)?; if paren_right { self.buffer.push(b')'); } } } Ok(()) } fn write_rgb(&mut self, color: &Color) { let is_opaque = fuzzy_equals(color.alpha().0, 1.0); if is_opaque { self.buffer.extend_from_slice(b"rgb("); } else { self.buffer.extend_from_slice(b"rgba("); } self.write_float(color.red().0); self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.green().0); self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.blue().0); if !is_opaque { self.buffer.extend_from_slice(b","); self.write_optional_space(); self.write_float(color.alpha().0); } self.buffer.push(b')'); } fn write_hsl(&mut self, color: &Color) { let is_opaque = fuzzy_equals(color.alpha().0, 1.0); if is_opaque { self.buffer.extend_from_slice(b"hsl("); } else { self.buffer.extend_from_slice(b"hsla("); } self.write_float(color.hue().0); self.buffer.extend_from_slice(b"deg, "); self.write_float(color.saturation().0); self.buffer.extend_from_slice(b"%, "); self.write_float(color.lightness().0); self.buffer.extend_from_slice(b"%"); if !is_opaque { self.buffer.extend_from_slice(b", "); self.write_float(color.alpha().0); } self.buffer.push(b')'); } fn write_hex_component(&mut self, channel: u32) { debug_assert!(channel < 256); self.buffer.push(hex_char_for(channel >> 4) as u8); self.buffer.push(hex_char_for(channel & 0xF) as u8); } fn is_symmetrical_hex(channel: u32) -> bool { channel & 0xF == channel >> 4 } fn can_use_short_hex(color: &Color) -> bool { Self::is_symmetrical_hex(color.red().0.round() as u32) && Self::is_symmetrical_hex(color.green().0.round() as u32) && Self::is_symmetrical_hex(color.blue().0.round() as u32) } pub fn visit_color(&mut self, color: &Color) { let red = color.red().0.round() as u8; let green = color.green().0.round() as u8; let blue = color.blue().0.round() as u8; let name = if fuzzy_equals(color.alpha().0, 1.0) { NAMED_COLORS.get_by_rgba([red, green, blue]) } else { None }; #[allow(clippy::unnecessary_unwrap)] if self.options.is_compressed() { if fuzzy_equals(color.alpha().0, 1.0) { let hex_length = if Self::can_use_short_hex(color) { 4 } else { 7 }; if name.is_some() && name.unwrap().len() <= hex_length { self.buffer.extend_from_slice(name.unwrap().as_bytes()); } else if Self::can_use_short_hex(color) { self.buffer.push(b'#'); self.buffer.push(hex_char_for(red as u32 & 0xF) as u8); self.buffer.push(hex_char_for(green as u32 & 0xF) as u8); self.buffer.push(hex_char_for(blue as u32 & 0xF) as u8); } else { self.buffer.push(b'#'); self.write_hex_component(red as u32); self.write_hex_component(green as u32); self.write_hex_component(blue as u32); } } else { self.write_rgb(color); } } else if color.format != ColorFormat::Infer { match &color.format { ColorFormat::Rgb => self.write_rgb(color), ColorFormat::Hsl => self.write_hsl(color), ColorFormat::Literal(text) => self.buffer.extend_from_slice(text.as_bytes()), ColorFormat::Infer => unreachable!(), } // Always emit generated transparent colors in rgba format. This works // around an IE bug. See sass/sass#1782. } else if name.is_some() && !fuzzy_equals(color.alpha().0, 0.0) { self.buffer.extend_from_slice(name.unwrap().as_bytes()); } else if fuzzy_equals(color.alpha().0, 1.0) { self.buffer.push(b'#'); self.write_hex_component(red as u32); self.write_hex_component(green as u32); self.write_hex_component(blue as u32); } else { self.write_rgb(color); } } fn write_media_query(&mut self, query: &MediaQuery) { if let Some(modifier) = &query.modifier { self.buffer.extend_from_slice(modifier.as_bytes()); self.buffer.push(b' '); } if let Some(media_type) = &query.media_type { self.buffer.extend_from_slice(media_type.as_bytes()); if !query.conditions.is_empty() { self.buffer.extend_from_slice(b" and "); } } if query.conditions.len() == 1 && query.conditions.first().unwrap().starts_with("(not ") { self.buffer.extend_from_slice(b"not "); let condition = query.conditions.first().unwrap(); self.buffer .extend_from_slice(condition["(not ".len()..condition.len() - 1].as_bytes()); } else { let operator = if query.conjunction { " and " } else { " or " }; self.buffer .extend_from_slice(query.conditions.join(operator).as_bytes()); } } pub fn visit_number(&mut self, number: &SassNumber) -> SassResult<()> { if let Some(as_slash) = &number.as_slash { self.visit_number(&as_slash.0)?; self.buffer.push(b'/'); self.visit_number(&as_slash.1)?; return Ok(()); } if !self.inspect && number.unit.is_complex() { return Err(( format!( "{} isn't a valid CSS value.", inspect_number(number, self.options, self.span)? ), self.span, ) .into()); } self.write_float(number.num.0); write!(&mut self.buffer, "{}", number.unit)?; Ok(()) } fn write_float(&mut self, float: f64) { if float.is_infinite() && float.is_sign_negative() { self.buffer.extend_from_slice(b"-Infinity"); return; } else if float.is_infinite() { self.buffer.extend_from_slice(b"Infinity"); return; } // todo: can optimize away intermediate buffer let mut buffer = String::with_capacity(3); if float < 0.0 { buffer.push('-'); } let num = float.abs(); if self.options.is_compressed() && num < 1.0 { buffer.push_str( format!("{:.10}", num)[1..] .trim_end_matches('0') .trim_end_matches('.'), ); } else { buffer.push_str( format!("{:.10}", num) .trim_end_matches('0') .trim_end_matches('.'), ); } if buffer.is_empty() || buffer == "-" || buffer == "-0" { buffer = "0".to_owned(); } self.buffer.append(&mut buffer.into_bytes()); } pub fn visit_group( &mut self, stmt: CssStmt, prev_was_group_end: bool, prev_requires_semicolon: bool, ) -> SassResult<()> { if prev_requires_semicolon { self.buffer.push(b';'); } if !self.buffer.is_empty() { self.write_optional_newline(); } if prev_was_group_end && !self.buffer.is_empty() { self.write_optional_newline(); } self.visit_stmt(stmt)?; Ok(()) } fn finish_for_expr(self) -> String { // SAFETY: todo unsafe { String::from_utf8_unchecked(self.buffer) } } pub fn finish(mut self, prev_requires_semicolon: bool) -> String { let is_not_ascii = self.buffer.iter().any(|&c| !c.is_ascii()); if prev_requires_semicolon { self.buffer.push(b';'); } if !self.buffer.is_empty() { self.write_optional_newline(); } // SAFETY: todo let mut as_string = unsafe { String::from_utf8_unchecked(self.buffer) }; if is_not_ascii && self.options.is_compressed() && self.options.allows_charset { as_string.insert(0, '\u{FEFF}'); } else if is_not_ascii && self.options.allows_charset { as_string.insert_str(0, "@charset \"UTF-8\";\n"); } as_string } fn write_indentation(&mut self) { if self.options.is_compressed() { return; } self.buffer.reserve(self.indentation); for _ in 0..self.indentation { self.buffer.push(b' '); } } fn write_list_separator(&mut self, sep: ListSeparator) { match (sep, self.options.is_compressed()) { (ListSeparator::Space | ListSeparator::Undecided, _) => self.buffer.push(b' '), (ListSeparator::Comma, true) => self.buffer.push(b','), (ListSeparator::Comma, false) => self.buffer.extend_from_slice(b", "), (ListSeparator::Slash, true) => self.buffer.push(b'/'), (ListSeparator::Slash, false) => self.buffer.extend_from_slice(b" / "), } } fn elem_needs_parens(sep: ListSeparator, elem: &Value) -> bool { match elem { Value::List(elems, sep2, brackets) => { if elems.len() < 2 { return false; } if *brackets == Brackets::Bracketed { return false; } match sep { ListSeparator::Comma => *sep2 == ListSeparator::Comma, ListSeparator::Slash => { *sep2 == ListSeparator::Comma || *sep2 == ListSeparator::Slash } _ => *sep2 != ListSeparator::Undecided, } } _ => false, } } fn visit_list( &mut self, list_elems: &[Value], sep: ListSeparator, brackets: Brackets, span: Span, ) -> SassResult<()> { if brackets == Brackets::Bracketed { self.buffer.push(b'['); } else if list_elems.is_empty() { if !self.inspect { return Err(("() isn't a valid CSS value.", span).into()); } self.buffer.extend_from_slice(b"()"); return Ok(()); } let is_singleton = self.inspect && list_elems.len() == 1 && (sep == ListSeparator::Comma || sep == ListSeparator::Slash); if is_singleton && brackets != Brackets::Bracketed { self.buffer.push(b'('); } let (mut x, mut y); let elems: &mut dyn Iterator = if self.inspect { x = list_elems.iter(); &mut x } else { y = list_elems.iter().filter(|elem| !elem.is_blank()); &mut y }; let mut elems = elems.peekable(); while let Some(elem) = elems.next() { if self.inspect { let needs_parens = Self::elem_needs_parens(sep, elem); if needs_parens { self.buffer.push(b'('); } self.visit_value(elem, span)?; if needs_parens { self.buffer.push(b')'); } } else { self.visit_value(elem, span)?; } if elems.peek().is_some() { self.write_list_separator(sep); } } if is_singleton { match sep { ListSeparator::Comma => self.buffer.push(b','), ListSeparator::Slash => self.buffer.push(b'/'), _ => unreachable!(), } if brackets != Brackets::Bracketed { self.buffer.push(b')'); } } if brackets == Brackets::Bracketed { self.buffer.push(b']'); } Ok(()) } fn write_map_element(&mut self, value: &Value, span: Span) -> SassResult<()> { let needs_parens = matches!(value, Value::List(_, ListSeparator::Comma, Brackets::None)); if needs_parens { self.buffer.push(b'('); } self.visit_value(value, span)?; if needs_parens { self.buffer.push(b')'); } Ok(()) } fn visit_map(&mut self, map: &SassMap, span: Span) -> SassResult<()> { if !self.inspect { return Err(( format!( "{} isn't a valid CSS value.", inspect_map(map, self.options, span)? ), span, ) .into()); } self.buffer.push(b'('); let mut elems = map.iter().peekable(); while let Some((k, v)) = elems.next() { self.write_map_element(&k.node, k.span)?; self.buffer.extend_from_slice(b": "); self.write_map_element(v, k.span)?; if elems.peek().is_some() { self.buffer.extend_from_slice(b", "); } } self.buffer.push(b')'); Ok(()) } fn visit_unquoted_string(&mut self, string: &str) { let mut after_newline = false; self.buffer.reserve(string.len()); for c in string.bytes() { match c { b'\n' => { self.buffer.push(b' '); after_newline = true; } b' ' => { if !after_newline { self.buffer.push(b' '); } } _ => { self.buffer.push(c); after_newline = false; } } } } fn visit_quoted_string(&mut self, force_double_quote: bool, string: &str) { let mut has_single_quote = false; let mut has_double_quote = false; let mut buffer = Vec::new(); if force_double_quote { buffer.push(b'"'); } let mut iter = string.as_bytes().iter().copied().peekable(); while let Some(c) = iter.next() { match c { b'\'' => { if force_double_quote { buffer.push(b'\''); } else if has_double_quote { self.visit_quoted_string(true, string); return; } else { has_single_quote = true; buffer.push(b'\''); } } b'"' => { if force_double_quote { buffer.push(b'\\'); buffer.push(b'"'); } else if has_single_quote { self.visit_quoted_string(true, string); return; } else { has_double_quote = true; buffer.push(b'"'); } } b'\x00'..=b'\x08' | b'\x0A'..=b'\x1F' => { buffer.push(b'\\'); if c as u32 > 0xF { buffer.push(hex_char_for(c as u32 >> 4) as u8); } buffer.push(hex_char_for(c as u32 & 0xF) as u8); let next = match iter.peek() { Some(v) => *v, None => break, }; if next.is_ascii_hexdigit() || next == b' ' || next == b'\t' { buffer.push(b' '); } } b'\\' => { buffer.push(b'\\'); buffer.push(b'\\'); } _ => buffer.push(c), } } if force_double_quote { buffer.push(b'"'); self.buffer.extend_from_slice(&buffer); } else { let quote = if has_double_quote { b'\'' } else { b'"' }; self.buffer.push(quote); self.buffer.extend_from_slice(&buffer); self.buffer.push(quote); } } fn visit_function_ref(&mut self, func: &SassFunction, span: Span) -> SassResult<()> { if !self.inspect { return Err(( format!( "{} isn't a valid CSS value.", inspect_function_ref(func, self.options, span)? ), span, ) .into()); } self.buffer.extend_from_slice(b"get-function("); self.visit_quoted_string(false, func.name().as_str()); self.buffer.push(b')'); Ok(()) } fn visit_arglist(&mut self, arglist: &ArgList, span: Span) -> SassResult<()> { self.visit_list(&arglist.elems, ListSeparator::Comma, Brackets::None, span) } fn visit_value(&mut self, value: &Value, span: Span) -> SassResult<()> { match value { Value::Dimension(num) => self.visit_number(num)?, Value::Color(color) => self.visit_color(color), Value::Calculation(calc) => self.visit_calculation(calc)?, Value::List(elems, sep, brackets) => self.visit_list(elems, *sep, *brackets, span)?, Value::True => self.buffer.extend_from_slice(b"true"), Value::False => self.buffer.extend_from_slice(b"false"), Value::Null => { if self.inspect { self.buffer.extend_from_slice(b"null") } } Value::Map(map) => self.visit_map(map, span)?, Value::FunctionRef(func) => self.visit_function_ref(func, span)?, Value::String(s, QuoteKind::Quoted) => self.visit_quoted_string(false, s), Value::String(s, QuoteKind::None) => self.visit_unquoted_string(s), Value::ArgList(arglist) => self.visit_arglist(arglist, span)?, } Ok(()) } fn write_style(&mut self, style: Style) -> SassResult<()> { if !self.options.is_compressed() { self.write_indentation(); } self.buffer .extend_from_slice(style.property.resolve_ref().as_bytes()); self.buffer.push(b':'); // todo: _writeFoldedValue and _writeReindentedValue if !style.declared_as_custom_property && !self.options.is_compressed() { self.buffer.push(b' '); } self.visit_value(&style.value.node, style.value.span)?; Ok(()) } fn write_import(&mut self, import: &str, modifiers: Option) -> SassResult<()> { self.write_indentation(); self.buffer.extend_from_slice(b"@import "); write!(&mut self.buffer, "{}", import)?; if let Some(modifiers) = modifiers { self.buffer.push(b' '); self.buffer.extend_from_slice(modifiers.as_bytes()); } Ok(()) } fn write_comment(&mut self, comment: &str, span: Span) -> SassResult<()> { if self.options.is_compressed() && !comment.starts_with("/*!") { return Ok(()); } self.write_indentation(); let col = self.map.look_up_pos(span.low()).position.column; let mut lines = comment.lines(); if let Some(line) = lines.next() { self.buffer.extend_from_slice(line.trim_start().as_bytes()); } let lines = lines .map(|line| { let diff = (line.len() - line.trim_start().len()).saturating_sub(col); format!("{}{}", " ".repeat(diff), line.trim_start()) }) .collect::>() .join("\n"); if !lines.is_empty() { write!(&mut self.buffer, "\n{}", lines)?; } Ok(()) } pub fn requires_semicolon(stmt: &CssStmt) -> bool { match stmt { CssStmt::Style(_) | CssStmt::Import(_, _) => true, CssStmt::UnknownAtRule(rule, _) => !rule.has_body, _ => false, } } fn write_children(&mut self, mut children: Vec) -> SassResult<()> { if self.options.is_compressed() { self.buffer.push(b'{'); } else { self.buffer.extend_from_slice(b" {\n"); } self.indentation += self.indent_width; let last = children.pop(); for child in children { let needs_semicolon = Self::requires_semicolon(&child); let did_write = self.visit_stmt(child)?; if !did_write { continue; } if needs_semicolon { self.buffer.push(b';'); } self.write_optional_newline(); } if let Some(last) = last { let needs_semicolon = Self::requires_semicolon(&last); let did_write = self.visit_stmt(last)?; if did_write { if needs_semicolon && !self.options.is_compressed() { self.buffer.push(b';'); } self.write_optional_newline(); } } self.indentation -= self.indent_width; if self.options.is_compressed() { self.buffer.push(b'}'); } else { self.write_indentation(); self.buffer.extend_from_slice(b"}"); } Ok(()) } fn write_optional_space(&mut self) { if !self.options.is_compressed() { self.buffer.push(b' '); } } fn write_optional_newline(&mut self) { if !self.options.is_compressed() { self.buffer.push(b'\n'); } } fn write_supports_rule(&mut self, supports_rule: SupportsRule) -> SassResult<()> { self.write_indentation(); self.buffer.extend_from_slice(b"@supports"); if !supports_rule.params.is_empty() { self.buffer.push(b' '); self.buffer .extend_from_slice(supports_rule.params.as_bytes()); } self.write_children(supports_rule.body)?; Ok(()) } /// Returns whether or not text was written fn visit_stmt(&mut self, stmt: CssStmt) -> SassResult { if stmt.is_invisible() { return Ok(false); } match stmt { CssStmt::RuleSet { selector, body, .. } => { self.write_indentation(); self.write_selector_list(&selector.as_selector_list()); self.write_children(body)?; } CssStmt::Media(media_rule, ..) => { self.write_indentation(); self.buffer.extend_from_slice(b"@media "); if let Some((last, rest)) = media_rule.query.split_last() { for query in rest { self.write_media_query(query); self.buffer.push(b','); self.write_optional_space(); } self.write_media_query(last); } self.write_children(media_rule.body)?; } CssStmt::UnknownAtRule(unknown_at_rule, ..) => { self.write_indentation(); self.buffer.push(b'@'); self.buffer .extend_from_slice(unknown_at_rule.name.as_bytes()); if !unknown_at_rule.params.is_empty() { write!(&mut self.buffer, " {}", unknown_at_rule.params)?; } if !unknown_at_rule.has_body { debug_assert!(unknown_at_rule.body.is_empty()); return Ok(true); } else if unknown_at_rule.body.iter().all(CssStmt::is_invisible) { self.buffer.extend_from_slice(b" {}"); return Ok(true); } self.write_children(unknown_at_rule.body)?; } CssStmt::Style(style) => self.write_style(style)?, CssStmt::Comment(comment, span) => self.write_comment(&comment, span)?, CssStmt::KeyframesRuleSet(keyframes_rule_set) => { self.write_indentation(); // todo: i bet we can do something like write_with_separator to avoid extra allocation let selector = keyframes_rule_set .selector .into_iter() .map(|s| s.to_string()) .collect::>() .join(", "); self.buffer.extend_from_slice(selector.as_bytes()); self.write_children(keyframes_rule_set.body)?; } CssStmt::Import(import, modifier) => self.write_import(&import, modifier)?, CssStmt::Supports(supports_rule, _) => self.write_supports_rule(supports_rule)?, } Ok(true) } } grass_compiler-0.13.4/src/unit/conversion.rs000064400000000000000000000155571046102023000172400ustar 00000000000000//! A big dictionary of units and their conversion ratios. //! //! Arbitrary precision is retained. use std::{ collections::{HashMap, HashSet}, f64::consts::PI, iter::FromIterator, }; use once_cell::sync::Lazy; use crate::unit::Unit; pub(crate) static UNIT_CONVERSION_TABLE: Lazy>> = Lazy::new(|| { let mut from_in = HashMap::new(); from_in.insert(Unit::In, 1.0); from_in.insert(Unit::Cm, 1.0 / 2.54); from_in.insert(Unit::Pc, 1.0 / 6.0); from_in.insert(Unit::Mm, 1.0 / 25.4); from_in.insert(Unit::Q, 1.0 / 101.6); from_in.insert(Unit::Pt, 1.0 / 72.0); from_in.insert(Unit::Px, 1.0 / 96.0); let mut from_cm = HashMap::new(); from_cm.insert(Unit::In, 2.54); from_cm.insert(Unit::Cm, 1.0); from_cm.insert(Unit::Pc, 2.54 / 6.0); from_cm.insert(Unit::Mm, 1.0 / 10.0); from_cm.insert(Unit::Q, 1.0 / 40.0); from_cm.insert(Unit::Pt, 2.54 / 72.0); from_cm.insert(Unit::Px, 2.54 / 96.0); let mut from_pc = HashMap::new(); from_pc.insert(Unit::In, 6.0); from_pc.insert(Unit::Cm, 6.0 / 2.54); from_pc.insert(Unit::Pc, 1.0); from_pc.insert(Unit::Mm, 6.0 / 25.4); from_pc.insert(Unit::Q, 6.0 / 101.6); from_pc.insert(Unit::Pt, 1.0 / 12.0); from_pc.insert(Unit::Px, 1.0 / 16.0); let mut from_mm = HashMap::new(); from_mm.insert(Unit::In, 25.4); from_mm.insert(Unit::Cm, 10.0); from_mm.insert(Unit::Pc, 25.4 / 6.0); from_mm.insert(Unit::Mm, 1.0); from_mm.insert(Unit::Q, 1.0 / 4.0); from_mm.insert(Unit::Pt, 25.4 / 72.0); from_mm.insert(Unit::Px, 25.4 / 96.0); let mut from_q = HashMap::new(); from_q.insert(Unit::In, 101.6); from_q.insert(Unit::Cm, 40.0); from_q.insert(Unit::Pc, 101.6 / 6.0); from_q.insert(Unit::Mm, 4.0); from_q.insert(Unit::Q, 1.0); from_q.insert(Unit::Pt, 101.6 / 72.0); from_q.insert(Unit::Px, 101.6 / 96.0); let mut from_pt = HashMap::new(); from_pt.insert(Unit::In, 72.0); from_pt.insert(Unit::Cm, 72.0 / 2.54); from_pt.insert(Unit::Pc, 12.0); from_pt.insert(Unit::Mm, 72.0 / 25.4); from_pt.insert(Unit::Q, 72.0 / 101.6); from_pt.insert(Unit::Pt, 1.0); from_pt.insert(Unit::Px, 3.0 / 4.0); let mut from_px = HashMap::new(); from_px.insert(Unit::In, 96.0); from_px.insert(Unit::Cm, 96.0 / 2.54); from_px.insert(Unit::Pc, 16.0); from_px.insert(Unit::Mm, 96.0 / 25.4); from_px.insert(Unit::Q, 96.0 / 101.6); from_px.insert(Unit::Pt, 4.0 / 3.0); from_px.insert(Unit::Px, 1.0); let mut from_deg = HashMap::new(); from_deg.insert(Unit::Deg, 1.0); from_deg.insert(Unit::Grad, 9.0 / 10.0); from_deg.insert(Unit::Rad, 180.0 / PI); from_deg.insert(Unit::Turn, 360.0); let mut from_grad = HashMap::new(); from_grad.insert(Unit::Deg, 10.0 / 9.0); from_grad.insert(Unit::Grad, 1.0); from_grad.insert(Unit::Rad, 200.0 / PI); from_grad.insert(Unit::Turn, 400.0); let mut from_rad = HashMap::new(); from_rad.insert(Unit::Deg, PI / 180.0); from_rad.insert(Unit::Grad, PI / 200.0); from_rad.insert(Unit::Rad, 1.0); from_rad.insert(Unit::Turn, 2.0 * PI); let mut from_turn = HashMap::new(); from_turn.insert(Unit::Deg, 1.0 / 360.0); from_turn.insert(Unit::Grad, 1.0 / 400.0); from_turn.insert(Unit::Rad, 1.0 / (2.0 * PI)); from_turn.insert(Unit::Turn, 1.0); let mut from_s = HashMap::new(); from_s.insert(Unit::S, 1.0); from_s.insert(Unit::Ms, 1.0 / 1000.0); let mut from_ms = HashMap::new(); from_ms.insert(Unit::S, 1000.0); from_ms.insert(Unit::Ms, 1.0); let mut from_hz = HashMap::new(); from_hz.insert(Unit::Hz, 1.0); from_hz.insert(Unit::Khz, 1000.0); let mut from_khz = HashMap::new(); from_khz.insert(Unit::Hz, 1.0 / 1000.0); from_khz.insert(Unit::Khz, 1.0); let mut from_dpi = HashMap::new(); from_dpi.insert(Unit::Dpi, 1.0); from_dpi.insert(Unit::Dpcm, 2.54); from_dpi.insert(Unit::Dppx, 96.0); let mut from_dpcm = HashMap::new(); from_dpcm.insert(Unit::Dpi, 1.0 / 2.54); from_dpcm.insert(Unit::Dpcm, 1.0); from_dpcm.insert(Unit::Dppx, 96.0 / 2.54); let mut from_dppx = HashMap::new(); from_dppx.insert(Unit::Dpi, 1.0 / 96.0); from_dppx.insert(Unit::Dpcm, 2.54 / 96.0); from_dppx.insert(Unit::Dppx, 1.0); let mut m = HashMap::new(); m.insert(Unit::In, from_in); m.insert(Unit::Cm, from_cm); m.insert(Unit::Pc, from_pc); m.insert(Unit::Mm, from_mm); m.insert(Unit::Q, from_q); m.insert(Unit::Pt, from_pt); m.insert(Unit::Px, from_px); m.insert(Unit::Deg, from_deg); m.insert(Unit::Grad, from_grad); m.insert(Unit::Rad, from_rad); m.insert(Unit::Turn, from_turn); m.insert(Unit::S, from_s); m.insert(Unit::Ms, from_ms); m.insert(Unit::Hz, from_hz); m.insert(Unit::Khz, from_khz); m.insert(Unit::Dpi, from_dpi); m.insert(Unit::Dpcm, from_dpcm); m.insert(Unit::Dppx, from_dppx); m }); pub(crate) static KNOWN_COMPATIBILITIES: Lazy<[HashSet; 5]> = Lazy::new(|| { let dimensions = HashSet::from_iter([ Unit::Em, Unit::Ex, Unit::Ch, Unit::Rem, Unit::Vw, Unit::Vh, Unit::Vmin, Unit::Vmax, Unit::Cm, Unit::Mm, Unit::Q, Unit::In, Unit::Pt, Unit::Pc, Unit::Px, ]); let angles = HashSet::from_iter([Unit::Deg, Unit::Grad, Unit::Rad, Unit::Turn]); let time = HashSet::from_iter([Unit::S, Unit::Ms]); let frequency = HashSet::from_iter([Unit::Hz, Unit::Khz]); let resolution = HashSet::from_iter([Unit::Dpi, Unit::Dpcm, Unit::Dppx]); [dimensions, angles, time, frequency, resolution] }); pub(crate) fn known_compatibilities_by_unit(unit: &Unit) -> Option<&HashSet> { match unit { Unit::Em | Unit::Ex | Unit::Ch | Unit::Rem | Unit::Vw | Unit::Vh | Unit::Vmin | Unit::Vmax | Unit::Cm | Unit::Mm | Unit::Q | Unit::In | Unit::Pt | Unit::Pc | Unit::Px => Some(&KNOWN_COMPATIBILITIES[0]), Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => Some(&KNOWN_COMPATIBILITIES[1]), Unit::S | Unit::Ms => Some(&KNOWN_COMPATIBILITIES[2]), Unit::Hz | Unit::Khz => Some(&KNOWN_COMPATIBILITIES[3]), Unit::Dpi | Unit::Dpcm | Unit::Dppx => Some(&KNOWN_COMPATIBILITIES[4]), _ => None, } } grass_compiler-0.13.4/src/unit/mod.rs000064400000000000000000000225541046102023000156250ustar 00000000000000use std::{fmt, sync::Arc}; use crate::interner::InternedString; pub(crate) use conversion::{known_compatibilities_by_unit, UNIT_CONVERSION_TABLE}; mod conversion; #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub enum Unit { // Absolute units /// Pixels Px, /// Millimeters Mm, /// Inches In, /// Centimeters Cm, /// Quarter-millimeters Q, /// Points Pt, /// Picas Pc, // Font relative units /// Font size of the parent element Em, /// Font size of the root element Rem, /// Line height of the element Lh, /// x-height of the element's font Ex, /// The advance measure (width) of the glyph "0" of the element's font Ch, /// Represents the "cap height" (nominal height of capital letters) of the element's font Cap, /// Equal to the used advance measure of the "水" (CJK water ideograph, U+6C34) glyph /// found in the font used to render it Ic, /// Equal to the computed value of the line-height property on the root element /// (typically \), converted to an absolute length Rlh, // Viewport relative units /// 1% of the viewport's width Vw, /// 1% of the viewport's height Vh, /// 1% of the viewport's smaller dimension Vmin, /// 1% of the viewport's larger dimension Vmax, /// Equal to 1% of the size of the initial containing block, in the direction of the root /// element's inline axis Vi, /// Equal to 1% of the size of the initial containing block, in the direction of the root /// element's block axis Vb, // Angle units /// Represents an angle in degrees. One full circle is 360deg Deg, /// Represents an angle in gradians. One full circle is 400grad Grad, /// Represents an angle in radians. One full circle is 2π radians which approximates to 6.283rad Rad, /// Represents an angle in a number of turns. One full circle is 1turn Turn, // Time units /// Represents a time in seconds S, /// Represents a time in milliseconds Ms, // Frequency units /// Represents a frequency in hertz Hz, /// Represents a frequency in kilohertz Khz, // Resolution units /// Represents the number of dots per inch Dpi, /// Represents the number of dots per centimeter Dpcm, /// Represents the number of dots per px unit Dppx, // Other units /// Represents a fraction of the available space in the grid container Fr, Percent, /// Unknown unit Unknown(InternedString), /// Unspecified unit None, Complex(Arc), } #[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct ComplexUnit { pub numer: Vec, pub denom: Vec, } pub(crate) fn are_any_convertible(units1: &[Unit], units2: &[Unit]) -> bool { for unit1 in units1 { for unit2 in units2 { if unit1.comparable(unit2) { return true; } } } false } #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub(crate) enum UnitKind { Absolute, FontRelative, ViewportRelative, Angle, Time, Frequency, Resolution, Other, None, } impl Unit { pub(crate) fn new(mut numer: Vec, denom: Vec) -> Self { if denom.is_empty() && numer.is_empty() { Unit::None } else if denom.is_empty() && numer.len() == 1 { numer.pop().unwrap() } else { Unit::Complex(Arc::new(ComplexUnit { numer, denom })) } } pub(crate) fn numer_and_denom(self) -> (Vec, Vec) { match self { Self::Complex(complex) => (complex.numer.clone(), complex.denom.clone()), Self::None => (Vec::new(), Vec::new()), v => (vec![v], Vec::new()), } } pub(crate) fn invert(self) -> Self { let (numer, denom) = self.numer_and_denom(); Self::new(denom, numer) } pub(crate) fn is_complex(&self) -> bool { matches!(self, Unit::Complex(complex) if complex.numer.len() != 1 || !complex.denom.is_empty()) } pub(crate) fn comparable(&self, other: &Unit) -> bool { if other == &Unit::None { return true; } match self.kind() { UnitKind::FontRelative | UnitKind::ViewportRelative | UnitKind::Other => self == other, UnitKind::None => true, u => other.kind() == u, } } /// Used internally to determine if two units are comparable or not fn kind(&self) -> UnitKind { match self { Unit::Px | Unit::Mm | Unit::In | Unit::Cm | Unit::Q | Unit::Pt | Unit::Pc => { UnitKind::Absolute } Unit::Em | Unit::Rem | Unit::Lh | Unit::Ex | Unit::Ch | Unit::Cap | Unit::Ic | Unit::Rlh => UnitKind::FontRelative, Unit::Vw | Unit::Vh | Unit::Vmin | Unit::Vmax | Unit::Vi | Unit::Vb => { UnitKind::ViewportRelative } Unit::Deg | Unit::Grad | Unit::Rad | Unit::Turn => UnitKind::Angle, Unit::S | Unit::Ms => UnitKind::Time, Unit::Hz | Unit::Khz => UnitKind::Frequency, Unit::Dpi | Unit::Dpcm | Unit::Dppx => UnitKind::Resolution, Unit::None => UnitKind::None, Unit::Fr | Unit::Percent | Unit::Unknown(..) | Unit::Complex { .. } => UnitKind::Other, } } } impl From for Unit { fn from(unit: String) -> Self { match unit.to_ascii_lowercase().as_str() { "px" => Unit::Px, "mm" => Unit::Mm, "in" => Unit::In, "cm" => Unit::Cm, "q" => Unit::Q, "pt" => Unit::Pt, "pc" => Unit::Pc, "em" => Unit::Em, "rem" => Unit::Rem, "lh" => Unit::Lh, "%" => Unit::Percent, "ex" => Unit::Ex, "ch" => Unit::Ch, "cap" => Unit::Cap, "ic" => Unit::Ic, "rlh" => Unit::Rlh, "vw" => Unit::Vw, "vh" => Unit::Vh, "vmin" => Unit::Vmin, "vmax" => Unit::Vmax, "vi" => Unit::Vi, "vb" => Unit::Vb, "deg" => Unit::Deg, "grad" => Unit::Grad, "rad" => Unit::Rad, "turn" => Unit::Turn, "s" => Unit::S, "ms" => Unit::Ms, "hz" => Unit::Hz, "khz" => Unit::Khz, "dpi" => Unit::Dpi, "dpcm" => Unit::Dpcm, "dppx" => Unit::Dppx, "fr" => Unit::Fr, _ => Unit::Unknown(InternedString::get_or_intern(unit)), } } } impl fmt::Display for Unit { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Unit::Px => write!(f, "px"), Unit::Mm => write!(f, "mm"), Unit::In => write!(f, "in"), Unit::Cm => write!(f, "cm"), Unit::Q => write!(f, "q"), Unit::Pt => write!(f, "pt"), Unit::Pc => write!(f, "pc"), Unit::Em => write!(f, "em"), Unit::Rem => write!(f, "rem"), Unit::Lh => write!(f, "lh"), Unit::Percent => write!(f, "%"), Unit::Ex => write!(f, "ex"), Unit::Ch => write!(f, "ch"), Unit::Cap => write!(f, "cap"), Unit::Ic => write!(f, "ic"), Unit::Rlh => write!(f, "rlh"), Unit::Vw => write!(f, "vw"), Unit::Vh => write!(f, "vh"), Unit::Vmin => write!(f, "vmin"), Unit::Vmax => write!(f, "vmax"), Unit::Vi => write!(f, "vi"), Unit::Vb => write!(f, "vb"), Unit::Deg => write!(f, "deg"), Unit::Grad => write!(f, "grad"), Unit::Rad => write!(f, "rad"), Unit::Turn => write!(f, "turn"), Unit::S => write!(f, "s"), Unit::Ms => write!(f, "ms"), Unit::Hz => write!(f, "Hz"), Unit::Khz => write!(f, "kHz"), Unit::Dpi => write!(f, "dpi"), Unit::Dpcm => write!(f, "dpcm"), Unit::Dppx => write!(f, "dppx"), Unit::Fr => write!(f, "fr"), Unit::Unknown(s) => write!(f, "{}", s), Unit::None => Ok(()), Unit::Complex(complex) => { let numer = &complex.numer; let denom = &complex.denom; debug_assert!( numer.len() > 1 || !denom.is_empty(), "unsimplified complex unit" ); let numer_rendered = numer .iter() .map(ToString::to_string) .collect::>() .join("*"); let denom_rendered = denom .iter() .map(ToString::to_string) .collect::>() .join("*"); if denom.is_empty() { write!(f, "{}", numer_rendered) } else if numer.is_empty() && denom.len() == 1 { write!(f, "{}^-1", denom_rendered) } else if numer.is_empty() { write!(f, "({})^-1", denom_rendered) } else { write!(f, "{}/{}", numer_rendered, denom_rendered) } } } } } grass_compiler-0.13.4/src/utils/chars.rs000064400000000000000000000012121046102023000163130ustar 00000000000000pub(crate) fn hex_char_for(number: u32) -> char { debug_assert!(number < 0x10); std::char::from_u32(if number < 0xA { 0x30 + number } else { 0x61 - 0xA + number }) .unwrap() } pub(crate) fn is_name(c: char) -> bool { is_name_start(c) || c.is_ascii_digit() || c == '-' } pub(crate) fn is_name_start(c: char) -> bool { c == '_' || c.is_alphabetic() || c as u32 >= 0x0080 } pub(crate) fn as_hex(c: char) -> u32 { match c { '0'..='9' => c as u32 - '0' as u32, 'A'..='F' => 10 + c as u32 - 'A' as u32, 'a'..='f' => 10 + c as u32 - 'a' as u32, _ => unreachable!(), } } grass_compiler-0.13.4/src/utils/map_view.rs000064400000000000000000000230111046102023000170230ustar 00000000000000use std::{ cell::RefCell, collections::{BTreeMap, HashSet}, fmt, sync::Arc, }; use crate::common::Identifier; pub(crate) trait MapView: fmt::Debug { type Value; fn get(&self, name: Identifier) -> Option; fn remove(&self, name: Identifier) -> Option; fn insert(&self, name: Identifier, value: Self::Value) -> Option; fn len(&self) -> usize; fn is_empty(&self) -> bool { self.len() == 0 } fn contains_key(&self, k: Identifier) -> bool { self.get(k).is_some() } // todo: wildly ineffecient to return vec here, because of the arbitrary nesting of Self fn keys(&self) -> Vec; fn iter(&self) -> Vec<(Identifier, Self::Value)>; } impl MapView for Arc> { type Value = T; fn get(&self, name: Identifier) -> Option { (**self).get(name) } fn remove(&self, name: Identifier) -> Option { (**self).remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { (**self).insert(name, value) } fn len(&self) -> usize { (**self).len() } fn keys(&self) -> Vec { (**self).keys() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { (**self).iter() } } #[derive(Debug)] pub(crate) struct BaseMapView(pub Arc>>); impl Clone for BaseMapView { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } } #[derive(Debug, Clone)] pub(crate) struct UnprefixedMapView + Clone>( pub T, pub String, ); #[derive(Debug, Clone)] pub(crate) struct PrefixedMapView + Clone>( pub T, pub String, ); impl MapView for BaseMapView { type Value = T; fn get(&self, name: Identifier) -> Option { (*self.0).borrow().get(&name).cloned() } fn len(&self) -> usize { (*self.0).borrow().len() } fn remove(&self, name: Identifier) -> Option { (*self.0).borrow_mut().remove(&name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { (*self.0).borrow_mut().insert(name, value) } fn keys(&self) -> Vec { (*self.0).borrow().keys().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { (*self.0).borrow().clone().into_iter().collect() } } impl + Clone> MapView for UnprefixedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.get(name) } fn remove(&self, name: Identifier) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { let name = Identifier::from(format!("{}{}", self.1, name)); self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .into_iter() .filter(|key| key.as_str().starts_with(&self.1)) .map(|key| Identifier::from(key.as_str().strip_prefix(&self.1).unwrap())) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } impl + Clone> MapView for PrefixedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !name.as_str().starts_with(&self.1) { return None; } let name = Identifier::from(name.as_str().strip_prefix(&self.1).unwrap()); self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .into_iter() .filter(|key| key.as_str().starts_with(&self.1)) .map(|key| Identifier::from(format!("{}{}", self.1, key))) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } /// A mostly-unmodifiable view of a map that only allows certain keys to be /// accessed. /// /// Whether or not the underlying map contains keys that aren't allowed, this /// view will behave as though it doesn't contain them. /// /// The underlying map's values may change independently of this view, but its /// set of keys may not. /// /// This is unmodifiable *except for the [remove] method*, which is used for /// `@used with` to mark configured variables as used. #[derive(Debug, Clone)] pub(crate) struct LimitedMapView + Clone>( pub T, pub HashSet, ); impl + Clone> LimitedMapView { pub fn safelist(map: T, keys: &HashSet) -> Self { let keys = keys .iter() .copied() .filter(|key| map.contains_key(*key)) .collect(); Self(map, keys) } pub fn blocklist(map: T, blocklist: &HashSet) -> Self { let keys = map .keys() .into_iter() .filter(|key| !blocklist.contains(key)) .collect(); Self(map, keys) } } impl + Clone> MapView for LimitedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !self.1.contains(&name) { return None; } self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !self.1.contains(&name) { return None; } self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !self.1.contains(&name) { return None; } self.0.insert(name, value) } fn len(&self) -> usize { self.1.len() } fn keys(&self) -> Vec { self.1.iter().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { unimplemented!() } } #[derive(Debug)] pub(crate) struct MergedMapView( pub Vec>>, HashSet, ); impl MergedMapView { pub fn new(maps: Vec>>) -> Self { let unique_keys: HashSet = maps.iter().fold(HashSet::new(), |mut keys, map| { keys.extend(&map.keys()); keys }); Self(maps, unique_keys) } } impl MapView for MergedMapView { type Value = V; fn get(&self, name: Identifier) -> Option { self.0.iter().rev().find_map(|map| (*map).get(name)) } fn remove(&self, _name: Identifier) -> Option { unimplemented!() } fn len(&self) -> usize { self.1.len() } fn insert(&self, name: Identifier, value: Self::Value) -> Option { for map in self.0.iter().rev() { if map.contains_key(name) { return map.insert(name, value); } } unreachable!("New entries may not be added to MergedMapView") } fn keys(&self) -> Vec { self.1.iter().copied().collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { self.1 .iter() .copied() .map(|name| (name, self.get(name).unwrap())) .collect() } } #[derive(Debug, Clone)] pub(crate) struct PublicMemberMapView + Clone>(pub T); impl + Clone> MapView for PublicMemberMapView { type Value = V; fn get(&self, name: Identifier) -> Option { if !name.is_public() { return None; } self.0.get(name) } fn remove(&self, name: Identifier) -> Option { if !name.is_public() { return None; } self.0.remove(name) } fn insert(&self, name: Identifier, value: Self::Value) -> Option { if !name.is_public() { return None; } self.0.insert(name, value) } fn len(&self) -> usize { self.0.len() } fn keys(&self) -> Vec { self.0 .keys() .iter() .copied() .filter(Identifier::is_public) .collect() } fn iter(&self) -> Vec<(Identifier, Self::Value)> { self.0 .iter() .into_iter() .filter(|(name, _)| Identifier::is_public(name)) .collect() } } grass_compiler-0.13.4/src/utils/mod.rs000064400000000000000000000045501046102023000160020ustar 00000000000000pub(crate) use chars::*; pub(crate) use map_view::*; pub(crate) use strings::*; mod chars; mod map_view; mod strings; #[allow(clippy::case_sensitive_file_extension_comparisons)] pub(crate) fn is_plain_css_import(url: &str) -> bool { if url.len() < 5 { return false; } let lower = url.to_ascii_lowercase(); lower.ends_with(".css") || lower.starts_with("http://") || lower.starts_with("https://") || lower.starts_with("//") } pub(crate) fn opposite_bracket(b: char) -> char { debug_assert!(matches!(b, '(' | '{' | '[' | ')' | '}' | ']')); match b { '(' => ')', '{' => '}', '[' => ']', ')' => '(', '}' => '{', ']' => '[', _ => unreachable!(), } } pub(crate) fn is_special_function(s: &str) -> bool { s.starts_with("calc(") || s.starts_with("var(") || s.starts_with("env(") || s.starts_with("min(") || s.starts_with("max(") || s.starts_with("clamp(") } /// Trim ASCII whitespace from both sides of string. /// /// If [excludeEscape] is `true`, this doesn't trim whitespace included in a CSS /// escape. pub(crate) fn trim_ascii( s: &str, // default=false exclude_escape: bool, ) -> &str { match s.chars().position(|c| !c.is_ascii_whitespace()) { Some(start) => &s[start..=last_non_whitespace(s, exclude_escape).unwrap()], None => "", } } fn last_non_whitespace(s: &str, exclude_escape: bool) -> Option { let mut idx = s.len() - 1; for c in s.chars().rev() { if !c.is_ascii_whitespace() { if exclude_escape && idx != 0 && idx != s.len() - 1 && c == '\\' { return Some(idx + 1); } else { return Some(idx); } } idx -= 1; } None } pub(crate) fn to_sentence>(mut elems: Vec, conjunction: &'static str) -> String { debug_assert!( !elems.is_empty(), "expected sentence to contain at least one element" ); if elems.len() == 1 { return elems.pop().unwrap().into(); } let last = elems.pop().unwrap(); format!( "{} {conjunction} {}", elems .into_iter() .map(Into::into) .collect::>() .join(", "), last.into(), conjunction = conjunction, ) } grass_compiler-0.13.4/src/utils/strings.rs000064400000000000000000000015551046102023000167160ustar 00000000000000use super::{is_name, is_name_start}; pub(crate) fn is_ident(s: &str) -> bool { let mut chars = s.chars().peekable(); match chars.next() { Some(c) if is_name_start(c) && !c.is_numeric() => {} Some(..) | None => return false, } while let Some(c) = chars.next() { if c == '\\' { for _ in 0..6 { let next = match chars.next() { Some(t) => t, None => return true, }; if !next.is_ascii_hexdigit() { break; } } match chars.peek() { Some(c) if c.is_whitespace() => { chars.next(); } _ => {} }; continue; } if !is_name(c) { return false; } } true } grass_compiler-0.13.4/src/value/arglist.rs000064400000000000000000000031611046102023000166410ustar 00000000000000use std::{cell::Cell, collections::BTreeMap, rc::Rc}; use crate::common::{Identifier, ListSeparator}; use super::Value; #[derive(Debug, Clone)] pub struct ArgList { pub elems: Vec, were_keywords_accessed: Rc>, // todo: special wrapper around this field to avoid having to make it private? keywords: BTreeMap, pub separator: ListSeparator, } impl PartialEq for ArgList { fn eq(&self, other: &Self) -> bool { self.elems == other.elems && self.keywords == other.keywords && self.separator == other.separator } } impl Eq for ArgList {} impl ArgList { pub fn new( elems: Vec, were_keywords_accessed: Rc>, keywords: BTreeMap, separator: ListSeparator, ) -> Self { debug_assert!( !(*were_keywords_accessed).get(), "expected args to initialize with unaccessed keywords" ); Self { elems, were_keywords_accessed, keywords, separator, } } pub fn len(&self) -> usize { self.elems.len() } pub fn is_empty(&self) -> bool { self.len() == 0 } pub fn is_blank(&self) -> bool { !self.is_empty() && (self.elems.iter().all(Value::is_blank)) } pub fn keywords(&self) -> &BTreeMap { (*self.were_keywords_accessed).set(true); &self.keywords } pub fn into_keywords(self) -> BTreeMap { (*self.were_keywords_accessed).set(true); self.keywords } } grass_compiler-0.13.4/src/value/calculation.rs000064400000000000000000000307201046102023000174730ustar 00000000000000use core::fmt; use std::iter::Iterator; use codemap::Span; use crate::{ common::BinaryOp, error::SassResult, serializer::inspect_number, unit::Unit, value::{SassNumber, Value}, Options, }; #[derive(Debug, Clone, PartialEq, Eq)] pub enum CalculationArg { Number(SassNumber), Calculation(SassCalculation), String(String), Operation { lhs: Box, op: BinaryOp, rhs: Box, }, Interpolation(String), } impl CalculationArg { pub fn parenthesize_calculation_rhs(outer: BinaryOp, right: BinaryOp) -> bool { if outer == BinaryOp::Div { true } else if outer == BinaryOp::Plus { false } else { right == BinaryOp::Plus || right == BinaryOp::Minus } } } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum CalculationName { Calc, Min, Max, Clamp, } impl fmt::Display for CalculationName { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { CalculationName::Calc => f.write_str("calc"), CalculationName::Min => f.write_str("min"), CalculationName::Max => f.write_str("max"), CalculationName::Clamp => f.write_str("clamp"), } } } impl CalculationName { pub(crate) fn in_min_or_max(self) -> bool { self == CalculationName::Min || self == CalculationName::Max } } #[derive(Debug, Clone, PartialEq, Eq)] pub struct SassCalculation { pub name: CalculationName, pub args: Vec, } impl SassCalculation { pub fn unsimplified(name: CalculationName, args: Vec) -> Self { Self { name, args } } pub fn calc(arg: CalculationArg) -> Value { let arg = Self::simplify(arg); match arg { CalculationArg::Number(n) => Value::Dimension(n), CalculationArg::Calculation(c) => Value::Calculation(c), _ => Value::Calculation(SassCalculation { name: CalculationName::Calc, args: vec![arg], }), } } pub fn min(args: Vec, options: &Options, span: Span) -> SassResult { let args = Self::simplify_arguments(args); debug_assert!(!args.is_empty(), "min() must have at least one argument."); let mut minimum: Option = None; for arg in &args { match arg { CalculationArg::Number(n) if minimum.is_some() && !minimum.as_ref().unwrap().is_comparable_to(n) => { minimum = None; break; } CalculationArg::Number(n) if minimum.is_none() || minimum.as_ref().unwrap().num > n.num.convert(&n.unit, &minimum.as_ref().unwrap().unit) => { minimum = Some(n.clone()); } CalculationArg::Number(..) => continue, _ => { minimum = None; break; } } } Ok(match minimum { Some(min) => Value::Dimension(min), None => { Self::verify_compatible_numbers(&args, options, span)?; Value::Calculation(SassCalculation { name: CalculationName::Min, args, }) } }) } pub fn max(args: Vec, options: &Options, span: Span) -> SassResult { let args = Self::simplify_arguments(args); if args.is_empty() { return Err(("max() must have at least one argument.", span).into()); } let mut maximum: Option = None; for arg in &args { match arg { CalculationArg::Number(n) if maximum.is_some() && !maximum.as_ref().unwrap().is_comparable_to(n) => { maximum = None; break; } CalculationArg::Number(n) if maximum.is_none() || maximum.as_ref().unwrap().num < n.num.convert(&n.unit, &maximum.as_ref().unwrap().unit) => { maximum = Some(n.clone()); } CalculationArg::Number(..) => continue, _ => { maximum = None; break; } } } Ok(match maximum { Some(max) => Value::Dimension(max), None => { Self::verify_compatible_numbers(&args, options, span)?; Value::Calculation(SassCalculation { name: CalculationName::Max, args, }) } }) } pub fn clamp( min: CalculationArg, value: Option, max: Option, options: &Options, span: Span, ) -> SassResult { if value.is_none() && max.is_some() { return Err(("If value is null, max must also be null.", span).into()); } let min = Self::simplify(min); let value = value.map(Self::simplify); let max = max.map(Self::simplify); match (min.clone(), value.clone(), max.clone()) { ( CalculationArg::Number(min), Some(CalculationArg::Number(value)), Some(CalculationArg::Number(max)), ) => { if min.is_comparable_to(&value) && min.is_comparable_to(&max) { if value.num <= min.num.convert(min.unit(), value.unit()) { return Ok(Value::Dimension(min)); } if value.num >= max.num.convert(max.unit(), value.unit()) { return Ok(Value::Dimension(max)); } return Ok(Value::Dimension(value)); } } _ => {} } let mut args = vec![min]; if let Some(value) = value { args.push(value); } if let Some(max) = max { args.push(max); } Self::verify_length(&args, 3, span)?; Self::verify_compatible_numbers(&args, options, span)?; Ok(Value::Calculation(SassCalculation { name: CalculationName::Clamp, args, })) } fn verify_length(args: &[CalculationArg], len: usize, span: Span) -> SassResult<()> { if args.len() == len { return Ok(()); } if args.iter().any(|arg| { matches!( arg, CalculationArg::String(..) | CalculationArg::Interpolation(..) ) }) { return Ok(()); } let was_or_were = if args.len() == 1 { "was" } else { "were" }; Err(( format!( "{len} arguments required, but only {} {was_or_were} passed.", args.len(), len = len, was_or_were = was_or_were, ), span, ) .into()) } #[allow(clippy::needless_range_loop)] fn verify_compatible_numbers( args: &[CalculationArg], options: &Options, span: Span, ) -> SassResult<()> { for arg in args { match arg { CalculationArg::Number(num) => match &num.unit { Unit::Complex(complex) => { if complex.numer.len() > 1 || !complex.denom.is_empty() { let num = num.clone(); let value = Value::Dimension(num); return Err(( format!( "Number {} isn't compatible with CSS calculations.", value.inspect(span)? ), span, ) .into()); } } _ => continue, }, _ => continue, } } for i in 0..args.len() { let number1 = match &args[i] { CalculationArg::Number(num) => num, _ => continue, }; for j in (i + 1)..args.len() { let number2 = match &args[j] { CalculationArg::Number(num) => num, _ => continue, }; if number1.has_possibly_compatible_units(number2) { continue; } return Err(( format!( "{} and {} are incompatible.", inspect_number(number1, options, span)?, inspect_number(number2, options, span)? ), span, ) .into()); } } Ok(()) } pub fn operate_internal( mut op: BinaryOp, left: CalculationArg, right: CalculationArg, in_min_or_max: bool, simplify: bool, options: &Options, span: Span, ) -> SassResult { if !simplify { return Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }); } let left = Self::simplify(left); let mut right = Self::simplify(right); if op == BinaryOp::Plus || op == BinaryOp::Minus { match (&left, &right) { (CalculationArg::Number(left), CalculationArg::Number(right)) if if in_min_or_max { left.is_comparable_to(right) } else { left.has_compatible_units(&right.unit) } => { if op == BinaryOp::Plus { return Ok(CalculationArg::Number(left.clone() + right.clone())); } else { return Ok(CalculationArg::Number(left.clone() - right.clone())); } } _ => {} } Self::verify_compatible_numbers(&[left.clone(), right.clone()], options, span)?; if let CalculationArg::Number(mut n) = right { if n.num.is_negative() { n.num.0 *= -1.0; op = if op == BinaryOp::Plus { BinaryOp::Minus } else { BinaryOp::Plus } } else { // todo: do we need this branch? } right = CalculationArg::Number(n); } return Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }); } match (left, right) { (CalculationArg::Number(num1), CalculationArg::Number(num2)) => { if op == BinaryOp::Mul { Ok(CalculationArg::Number(num1 * num2)) } else { Ok(CalculationArg::Number(num1 / num2)) } } (left, right) => Ok(CalculationArg::Operation { lhs: Box::new(left), op, rhs: Box::new(right), }), } // _verifyCompatibleNumbers([left, right]); // Ok(CalculationArg::Operation { // lhs: Box::new(left), // op, // rhs: Box::new(right), // }) } fn simplify(arg: CalculationArg) -> CalculationArg { match arg { CalculationArg::Number(..) | CalculationArg::Operation { .. } | CalculationArg::Interpolation(..) | CalculationArg::String(..) => arg, CalculationArg::Calculation(mut calc) => { if calc.name == CalculationName::Calc { calc.args.remove(0) } else { CalculationArg::Calculation(calc) } } } } fn simplify_arguments(args: Vec) -> Vec { args.into_iter().map(Self::simplify).collect() } } grass_compiler-0.13.4/src/value/map.rs000064400000000000000000000052331046102023000157530ustar 00000000000000use std::{slice::Iter, vec::IntoIter}; use codemap::Spanned; use crate::{ common::{Brackets, ListSeparator}, value::Value, }; #[derive(Debug, Clone, Default)] pub struct SassMap(Vec<(Spanned, Value)>); impl PartialEq for SassMap { fn eq(&self, other: &Self) -> bool { if self.0.len() != other.0.len() { return false; } for (key, value) in &self.0 { if !other .0 .iter() .any(|(key2, value2)| key.node == key2.node && value == value2) { return false; } } true } } impl Eq for SassMap {} impl SassMap { pub const fn new() -> SassMap { SassMap(Vec::new()) } pub const fn new_with(elements: Vec<(Spanned, Value)>) -> SassMap { SassMap(elements) } pub fn get(self, key: &Value) -> Option { for (k, v) in self.0 { if &k.node == key { return Some(v); } } None } pub fn get_ref(&self, key: &Value) -> Option<&Value> { for (k, v) in &self.0 { if &k.node == key { return Some(v); } } None } pub fn remove(&mut self, key: &Value) { self.0.retain(|(ref k, ..)| k.not_equals(key)); } pub fn merge(&mut self, other: SassMap) { for (key, value) in other { self.insert(key, value); } } pub fn iter(&self) -> Iter<(Spanned, Value)> { self.0.iter() } pub fn keys(self) -> Vec { self.0.into_iter().map(|(k, ..)| k.node).collect() } pub fn values(self) -> Vec { self.0.into_iter().map(|(.., v)| v).collect() } pub fn contains(&self, key: &Value) -> bool { self.0.iter().any(|(k, ..)| &k.node == key) } pub fn as_list(self) -> Vec { self.0 .into_iter() .map(|(k, v)| Value::List(vec![k.node, v], ListSeparator::Space, Brackets::None)) .collect() } /// Returns true if the key already exists pub fn insert(&mut self, key: Spanned, value: Value) -> bool { for (ref k, ref mut v) in &mut self.0 { if k.node == key.node { *v = value; return true; } } self.0.push((key, value)); false } pub fn is_empty(&self) -> bool { self.0.is_empty() } } impl IntoIterator for SassMap { type Item = (Spanned, Value); type IntoIter = IntoIter; fn into_iter(self) -> Self::IntoIter { self.0.into_iter() } } grass_compiler-0.13.4/src/value/mod.rs000064400000000000000000000434501046102023000157600ustar 00000000000000use std::{cmp::Ordering, sync::Arc}; use codemap::{Span, Spanned}; use crate::{ color::Color, common::{BinaryOp, Brackets, ListSeparator, QuoteKind}, error::SassResult, evaluate::Visitor, selector::Selector, serializer::{inspect_value, serialize_value}, unit::Unit, utils::is_special_function, Options, OutputStyle, }; pub use arglist::ArgList; pub use calculation::*; pub use map::SassMap; pub use number::*; pub use sass_function::{SassFunction, UserDefinedFunction}; pub(crate) use sass_number::conversion_factor; pub use sass_number::SassNumber; mod arglist; mod calculation; mod map; mod number; mod sass_function; mod sass_number; #[derive(Debug, Clone)] pub enum Value { True, False, Null, Dimension(SassNumber), List(Vec, ListSeparator, Brackets), Color(Arc), String(String, QuoteKind), Map(SassMap), ArgList(ArgList), /// Returned by `get-function()` FunctionRef(Box), Calculation(SassCalculation), } impl PartialEq for Value { fn eq(&self, other: &Self) -> bool { match self { Value::Calculation(calc1) => match other { Value::Calculation(calc2) => calc1 == calc2, _ => false, }, Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 == s2, _ => false, }, Value::Dimension(n1) => match other { Value::Dimension(n2) => n1 == n2, _ => false, }, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { false } else { for (a, b) in list1.iter().zip(list2) { if a != b { return false; } } true } } _ => false, }, Value::Null => matches!(other, Value::Null), Value::True => matches!(other, Value::True), Value::False => matches!(other, Value::False), Value::FunctionRef(fn1) => { if let Value::FunctionRef(fn2) = other { fn1 == fn2 } else { false } } Value::Map(map1) => { if let Value::Map(map2) = other { map1 == map2 } else { false } } Value::Color(color1) => { if let Value::Color(color2) = other { color1 == color2 } else { false } } Value::ArgList(list1) => match other { Value::ArgList(list2) => list1 == list2, Value::List(list2, ListSeparator::Comma, ..) => { if list1.len() != list2.len() { return false; } for (el1, el2) in list1.elems.iter().zip(list2) { if el1 != el2 { return false; } } true } _ => false, }, } } } impl Eq for Value {} impl Value { pub fn with_slash( self, numerator: SassNumber, denom: SassNumber, span: Span, ) -> SassResult { let mut number = self.assert_number(span)?; number.as_slash = Some(Arc::new((numerator, denom))); Ok(Value::Dimension(number)) } pub fn assert_number(self, span: Span) -> SassResult { match self { Value::Dimension(n) => Ok(n), _ => Err((format!("{} is not a number.", self.inspect(span)?), span).into()), } } pub fn assert_number_with_name(self, name: &str, span: Span) -> SassResult { match self { Value::Dimension(n) => Ok(n), _ => Err(( format!( "${name}: {} is not a number.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_color_with_name(self, name: &str, span: Span) -> SassResult> { match self { Value::Color(c) => Ok(c), _ => Err(( format!( "${name}: {} is not a color.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_map_with_name(self, name: &str, span: Span) -> SassResult { match self { Value::Map(m) => Ok(m), Value::List(v, ..) if v.is_empty() => Ok(SassMap::new()), Value::ArgList(v) if v.is_empty() => Ok(SassMap::new()), _ => Err(( format!( "${name}: {} is not a map.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn assert_string_with_name( self, name: &str, span: Span, ) -> SassResult<(String, QuoteKind)> { match self { Value::String(s, quotes) => Ok((s, quotes)), _ => Err(( format!( "${name}: {} is not a string.", self.inspect(span)?, name = name, ), span, ) .into()), } } pub fn is_blank(&self) -> bool { match self { Value::Null => true, Value::String(i, QuoteKind::None) if i.is_empty() => true, Value::List(_, _, Brackets::Bracketed) => false, Value::List(v, ..) => v.iter().all(Value::is_blank), Value::ArgList(v, ..) => v.is_blank(), _ => false, } } pub fn is_empty_list(&self) -> bool { match self { Value::List(v, ..) => v.is_empty(), Value::Map(m) => m.is_empty(), Value::ArgList(v) => v.elems.is_empty(), _ => false, } } pub fn to_css_string(&self, span: Span, is_compressed: bool) -> SassResult { serialize_value( self, &Options::default().style(if is_compressed { OutputStyle::Compressed } else { OutputStyle::Expanded }), span, ) } pub fn inspect(&self, span: Span) -> SassResult { inspect_value(self, &Options::default(), span) } pub fn is_truthy(&self) -> bool { !matches!(self, Value::Null | Value::False) } pub fn unquote(self) -> Self { match self { Value::String(s1, _) => Value::String(s1, QuoteKind::None), Value::List(v, sep, bracket) => { Value::List(v.into_iter().map(Value::unquote).collect(), sep, bracket) } v => v, } } pub const fn span(self, span: Span) -> Spanned { Spanned { node: self, span } } pub fn kind(&self) -> &'static str { match self { Value::Color(..) => "color", Value::String(..) => "string", Value::Calculation(..) => "calculation", Value::Dimension(..) => "number", Value::List(..) => "list", Value::FunctionRef(..) => "function", Value::ArgList(..) => "arglist", Value::True | Value::False => "bool", Value::Null => "null", Value::Map(..) => "map", } } pub fn as_slash(&self) -> Option> { match self { Value::Dimension(SassNumber { as_slash, .. }) => as_slash.clone(), _ => None, } } pub fn without_slash(self) -> Self { match self { Value::Dimension(SassNumber { num, unit, as_slash: _, }) => Value::Dimension(SassNumber { num, unit, as_slash: None, }), _ => self, } } pub fn is_special_function(&self) -> bool { match self { Value::String(s, QuoteKind::None) => is_special_function(s), Value::Calculation(..) => true, _ => false, } } pub fn is_var(&self) -> bool { match self { Value::String(s, QuoteKind::None) => { if s.len() < "var(--_)".len() { return false; } s.starts_with("var(") } Value::Calculation(..) => true, _ => false, } } pub fn try_map(&self) -> Option { match &self { Value::Map(m) => Some(m.clone()), Value::List(v, ..) if v.is_empty() => Some(SassMap::new()), Value::ArgList(v) if v.is_empty() => Some(SassMap::new()), _ => None, } } pub fn bool(b: bool) -> Self { if b { Value::True } else { Value::False } } pub fn cmp(&self, other: &Self, span: Span, op: BinaryOp) -> SassResult> { Ok(match self { Value::Dimension(SassNumber { num, unit, .. }) => match &other { Value::Dimension(SassNumber { num: num2, unit: unit2, .. }) => { if !unit.comparable(unit2) { return Err( (format!("Incompatible units {} and {}.", unit2, unit), span).into(), ); } if unit == unit2 || unit == &Unit::None || unit2 == &Unit::None { num.partial_cmp(num2) } else { num.partial_cmp(&num2.convert(unit2, unit)) } } _ => { return Err(( format!( "Undefined operation \"{} {} {}\".", self.inspect(span)?, op, other.inspect(span)? ), span, ) .into()) } }, _ => { return Err(( format!( "Undefined operation \"{} {} {}\".", self.inspect(span)?, op, other.inspect(span)? ), span, ) .into()); } }) } pub fn not_equals(&self, other: &Self) -> bool { match self { Value::String(s1, ..) => match other { Value::String(s2, ..) => s1 != s2, _ => true, }, Value::Dimension(SassNumber { num: n, unit, as_slash: _, }) if !n.is_nan() => match other { Value::Dimension(SassNumber { num: n2, unit: unit2, as_slash: _, }) if !n2.is_nan() => { if !unit.comparable(unit2) { true } else if unit == unit2 { n != n2 } else if unit == &Unit::None || unit2 == &Unit::None { true } else { n != &n2.convert(unit2, unit) } } _ => true, }, Value::List(list1, sep1, brackets1) => match other { Value::List(list2, sep2, brackets2) => { if sep1 != sep2 || brackets1 != brackets2 || list1.len() != list2.len() { true } else { for (a, b) in list1.iter().zip(list2) { if a.not_equals(b) { return true; } } false } } _ => true, }, s => s != other, } } pub fn as_list(self) -> Vec { match self { Value::List(v, ..) => v, Value::Map(m) => m.as_list(), Value::ArgList(v) => v.elems, v => vec![v], } } pub fn separator(&self) -> ListSeparator { match self { Value::List(_, list_separator, _) => *list_separator, Value::Map(..) | Value::ArgList(..) => ListSeparator::Comma, _ => ListSeparator::Space, } } /// Parses `self` as a selector list, in the same manner as the /// `selector-parse()` function. /// /// Returns a `SassError` if `self` isn't a type that can be parsed as a /// selector, or if parsing fails. If `allow_parent` is `true`, this allows /// parent selectors. Otherwise, they're considered parse errors. /// /// `name` is the argument name. It's used for error reporting. pub fn to_selector( self, visitor: &mut Visitor, name: &str, allows_parent: bool, span: Span, ) -> SassResult { let string = match self.clone().selector_string()? { Some(v) => v, None => return Err((format!("${}: {} is not a valid selector: it must be a string,\n a list of strings, or a list of lists of strings.", name, self.inspect(span)?), span).into()), }; Ok(Selector(visitor.parse_selector_from_string( &string, allows_parent, true, span, )?)) } fn selector_string(self) -> SassResult> { Ok(Some(match self { Value::String(text, ..) => text, Value::List(list, sep, ..) if !list.is_empty() => { let mut result = Vec::new(); match sep { ListSeparator::Comma => { for complex in list { if let Value::String(text, ..) = complex { result.push(text); } else if let Value::List( _, ListSeparator::Space | ListSeparator::Undecided, .., ) = complex { result.push(match complex.selector_string()? { Some(v) => v, None => return Ok(None), }); } else { return Ok(None); } } } ListSeparator::Slash => return Ok(None), ListSeparator::Space | ListSeparator::Undecided => { for compound in list { if let Value::String(text, ..) = compound { result.push(text); } else { return Ok(None); } } } } result.join(sep.as_str()) } _ => return Ok(None), })) } pub fn unary_plus(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(match self { Self::Dimension(SassNumber { .. }) => self, Self::Calculation(..) => { return Err(( format!("Undefined operation \"+{}\".", self.inspect(span)?), span, ) .into()) } _ => Self::String( format!( "+{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, ), }) } pub fn unary_neg(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(match self { Self::Calculation(..) => { return Err(( format!("Undefined operation \"-{}\".", self.inspect(span)?), span, ) .into()) } Self::Dimension(SassNumber { num, unit, as_slash, }) => Self::Dimension(SassNumber { num: -num, unit, as_slash, }), _ => Self::String( format!( "-{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, ), }) } pub fn unary_div(self, visitor: &mut Visitor, span: Span) -> SassResult { Ok(Self::String( format!( "/{}", &self.to_css_string(span, visitor.options.is_compressed())? ), QuoteKind::None, )) } pub fn unary_not(self) -> Self { match self { Self::False | Self::Null => Self::True, _ => Self::False, } } } grass_compiler-0.13.4/src/value/number.rs000064400000000000000000000205761046102023000164750ustar 00000000000000use std::{ convert::From, fmt, mem, ops::{ Add, AddAssign, Deref, Div, DivAssign, Mul, MulAssign, Neg, Rem, RemAssign, Sub, SubAssign, }, }; use crate::{ error::SassResult, unit::{Unit, UNIT_CONVERSION_TABLE}, }; use codemap::Span; const PRECISION: i32 = 10; fn epsilon() -> f64 { 10.0_f64.powi(-PRECISION - 1) } fn inverse_epsilon() -> f64 { 10.0_f64.powi(PRECISION + 1) } /// Thin wrapper around `f64` providing utility functions and more accurate /// operations -- namely a Sass-compatible modulo #[derive(Clone, Copy, PartialOrd)] #[repr(transparent)] pub struct Number(pub f64); impl PartialEq for Number { fn eq(&self, other: &Self) -> bool { fuzzy_equals(self.0, other.0) } } impl Eq for Number {} pub(crate) fn fuzzy_equals(a: f64, b: f64) -> bool { if a == b { return true; } (a - b).abs() <= epsilon() && (a * inverse_epsilon()).round() == (b * inverse_epsilon()).round() } pub(crate) fn fuzzy_as_int(num: f64) -> Option { if !num.is_finite() { return None; } let rounded = num.round(); if fuzzy_equals(num, rounded) { // todo: this can oveflow Some(rounded as i64) } else { None } } pub(crate) fn fuzzy_round(number: f64) -> f64 { // If the number is within epsilon of X.5, round up (or down for negative // numbers). if number > 0.0 { if fuzzy_less_than(number % 1.0, 0.5) { number.floor() } else { number.ceil() } } else if fuzzy_less_than_or_equals(number % 1.0, 0.5) { number.floor() } else { number.ceil() } } pub(crate) fn fuzzy_less_than(number1: f64, number2: f64) -> bool { number1 < number2 && !fuzzy_equals(number1, number2) } pub(crate) fn fuzzy_less_than_or_equals(number1: f64, number2: f64) -> bool { number1 < number2 || fuzzy_equals(number1, number2) } impl Number { /// This differs from `std::cmp::min` when either value is NaN pub fn min(self, other: Self) -> Self { if self < other { self } else { other } } /// This differs from `std::cmp::max` when either value is NaN pub fn max(self, other: Self) -> Self { if self > other { self } else { other } } pub fn is_positive(self) -> bool { self.0.is_sign_positive() && !self.is_zero() } pub fn is_negative(self) -> bool { self.0.is_sign_negative() && !self.is_zero() } pub fn assert_int(self, span: Span) -> SassResult { match fuzzy_as_int(self.0) { Some(i) => Ok(i), None => Err((format!("{} is not an int.", self.0), span).into()), } } pub fn round(self) -> Self { Self(self.0.round()) } pub fn ceil(self) -> Self { Self(self.0.ceil()) } pub fn floor(self) -> Self { Self(self.0.floor()) } pub fn abs(self) -> Self { Self(self.0.abs()) } pub fn clamp(self, min: f64, max: f64) -> Self { Number(min.max(self.0.min(max))) } pub fn sqrt(self) -> Self { Self(self.0.sqrt()) } pub fn ln(self) -> Self { Self(self.0.ln()) } pub fn log(self, base: Number) -> Self { Self(self.0.log(base.0)) } pub fn pow(self, exponent: Self) -> Self { Self(self.0.powf(exponent.0)) } /// Invariants: `from.comparable(&to)` must be true pub fn convert(self, from: &Unit, to: &Unit) -> Self { if from == &Unit::None || to == &Unit::None || from == to { return self; } debug_assert!(from.comparable(to), "from: {:?}, to: {:?}", from, to); Number(self.0 * UNIT_CONVERSION_TABLE[to][from]) } } macro_rules! inverse_trig_fn( ($name:ident) => { pub fn $name(self) -> Self { Self(self.0.$name().to_degrees()) } } ); /// Trigonometry methods impl Number { inverse_trig_fn!(acos); inverse_trig_fn!(asin); inverse_trig_fn!(atan); } impl Default for Number { fn default() -> Self { Self::zero() } } impl Number { pub const fn one() -> Self { Self(1.0) } pub fn is_one(self) -> bool { fuzzy_equals(self.0, 1.0) } pub const fn zero() -> Self { Self(0.0) } pub fn is_zero(self) -> bool { fuzzy_equals(self.0, 0.0) } } impl Deref for Number { type Target = f64; fn deref(&self) -> &Self::Target { &self.0 } } macro_rules! from_integer { ($ty:ty) => { impl From<$ty> for Number { fn from(b: $ty) -> Self { Number(b as f64) } } }; } macro_rules! from_smaller_integer { ($ty:ty) => { impl From<$ty> for Number { fn from(val: $ty) -> Self { Self(f64::from(val)) } } }; } impl From for Number { fn from(val: i64) -> Self { Self(val as f64) } } impl From for Number { fn from(b: f64) -> Self { Self(b) } } from_integer!(usize); from_integer!(isize); from_smaller_integer!(i32); from_smaller_integer!(u32); from_smaller_integer!(u8); impl fmt::Debug for Number { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "Number( {} )", self.to_string(false)) } } impl Number { pub(crate) fn inspect(self) -> String { self.to_string(false) } pub(crate) fn to_string(self, is_compressed: bool) -> String { if self.0.is_infinite() && self.0.is_sign_negative() { return "-Infinity".to_owned(); } else if self.0.is_infinite() { return "Infinity".to_owned(); } let mut buffer = String::with_capacity(3); if self.0 < 0.0 { buffer.push('-'); } let num = self.0.abs(); if is_compressed && num < 1.0 { buffer.push_str( format!("{:.10}", num)[1..] .trim_end_matches('0') .trim_end_matches('.'), ); } else { buffer.push_str( format!("{:.10}", num) .trim_end_matches('0') .trim_end_matches('.'), ); } if buffer.is_empty() || buffer == "-" || buffer == "-0" { return "0".to_owned(); } buffer } } impl Add for Number { type Output = Self; fn add(self, other: Self) -> Self { Self(self.0 + other.0) } } impl AddAssign for Number { fn add_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp + other; } } impl Sub for Number { type Output = Self; fn sub(self, other: Self) -> Self { Self(self.0 - other.0) } } impl SubAssign for Number { fn sub_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp - other; } } impl Mul for Number { type Output = Self; fn mul(self, other: Self) -> Self { Self(self.0 * other.0) } } impl Mul for Number { type Output = Self; fn mul(self, other: i64) -> Self { Self(self.0 * other as f64) } } impl MulAssign for Number { fn mul_assign(&mut self, other: i64) { let tmp = mem::take(self); *self = tmp * other; } } impl MulAssign for Number { fn mul_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp * other; } } impl Div for Number { type Output = Self; fn div(self, other: Self) -> Self { Self(self.0 / other.0) } } impl DivAssign for Number { fn div_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp / other; } } fn real_mod(n1: f64, n2: f64) -> f64 { n1.rem_euclid(n2) } fn modulo(n1: f64, n2: f64) -> f64 { if n2 > 0.0 { return real_mod(n1, n2); } if n2 == 0.0 { return f64::NAN; } let result = real_mod(n1, n2); if result == 0.0 { 0.0 } else { result + n2 } } impl Rem for Number { type Output = Self; fn rem(self, other: Self) -> Self { Self(modulo(self.0, other.0)) } } impl RemAssign for Number { fn rem_assign(&mut self, other: Self) { let tmp = mem::take(self); *self = tmp % other; } } impl Neg for Number { type Output = Self; fn neg(self) -> Self { Self(-self.0) } } grass_compiler-0.13.4/src/value/sass_function.rs000064400000000000000000000037161046102023000200600ustar 00000000000000use std::{fmt, sync::Arc}; use crate::{ast::AstFunctionDecl, builtin::Builtin, common::Identifier, evaluate::Environment}; /// A Sass function /// /// The function name is stored in addition to the body /// for use in the builtin function `inspect()` #[derive(Clone, Eq, PartialEq)] pub enum SassFunction { // todo: Cow<'static>? /// Builtin functions are those that have been implemented in Rust and are /// in the global scope. Builtin(Builtin, Identifier), // todo: maybe arc? /// User-defined functions are those that have been implemented in Sass using /// the @function rule. UserDefined(UserDefinedFunction), Plain { name: Identifier, }, } #[derive(Debug, Clone)] pub struct UserDefinedFunction { pub(crate) function: Arc, pub name: Identifier, pub(crate) env: Environment, } impl PartialEq for UserDefinedFunction { fn eq(&self, other: &Self) -> bool { self.function == other.function && self.name == other.name } } impl Eq for UserDefinedFunction {} impl SassFunction { /// Get the name of the function referenced /// /// Used mainly in debugging and `inspect()` pub fn name(&self) -> Identifier { match self { Self::Builtin(_, name) | Self::UserDefined(UserDefinedFunction { name, .. }) | Self::Plain { name } => *name, } } /// Whether the function is builtin or user-defined /// /// Used only in `std::fmt::Debug` for `SassFunction` fn kind(&self) -> &'static str { match &self { Self::Plain { .. } => "Plain", Self::Builtin(..) => "Builtin", Self::UserDefined { .. } => "UserDefined", } } } impl fmt::Debug for SassFunction { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.debug_struct("SassFunction") .field("name", &self.name()) .field("kind", &self.kind()) .finish() } } grass_compiler-0.13.4/src/value/sass_number.rs000064400000000000000000000234221046102023000175170ustar 00000000000000use std::{ ops::{Add, Div, Mul, Sub}, sync::Arc, }; use codemap::Span; use crate::{ error::SassResult, serializer::{inspect_float, inspect_number}, unit::{are_any_convertible, known_compatibilities_by_unit, Unit, UNIT_CONVERSION_TABLE}, Options, }; use super::{fuzzy_as_int, Number}; #[derive(Debug, Clone)] pub struct SassNumber { pub num: Number, pub unit: Unit, pub as_slash: Option>, } pub(crate) fn conversion_factor(from: &Unit, to: &Unit) -> Option { if from == to { return Some(1.0); } UNIT_CONVERSION_TABLE.get(to)?.get(from).copied() } impl SassNumber { pub fn new_unitless>(n: N) -> Self { Self { num: n.into(), unit: Unit::None, as_slash: None, } } pub fn has_comparable_units(&self, other_unit: &Unit) -> bool { self.unit.comparable(other_unit) } /// Unlike [`SassNumber::has_comparable_units`], this considers `Unit::None` /// to be compatible only with itself pub fn has_compatible_units(&self, other_unit: &Unit) -> bool { if (self.unit == Unit::None || *other_unit == Unit::None) && self.unit != *other_unit { return false; } self.has_comparable_units(other_unit) } #[allow(clippy::collapsible_if)] pub(crate) fn multiply_units(&self, mut num: f64, other_unit: Unit) -> SassNumber { let (numer_units, denom_units) = self.unit.clone().numer_and_denom(); let (other_numer, other_denom) = other_unit.numer_and_denom(); if numer_units.is_empty() { if other_denom.is_empty() && !are_any_convertible(&denom_units, &other_numer) { return SassNumber { num: Number(num), unit: Unit::new(other_numer, denom_units), as_slash: None, }; } else if denom_units.is_empty() { return SassNumber { num: Number(num), unit: Unit::new(other_numer, other_denom), as_slash: None, }; } } else if other_numer.is_empty() { if other_denom.is_empty() || (denom_units.is_empty() && !are_any_convertible(&numer_units, &other_denom)) { return SassNumber { num: Number(num), unit: Unit::new(numer_units, other_denom), as_slash: None, }; } } let mut new_numer = Vec::new(); let mut mutable_other_denom = other_denom; for numer in numer_units { let mut has_removed = false; mutable_other_denom.retain(|denom| { if has_removed { return true; } if let Some(factor) = conversion_factor(denom, &numer) { num /= factor; has_removed = true; return false; } true }); if !has_removed { new_numer.push(numer); } } let mut mutable_denom = denom_units; for numer in other_numer { let mut has_removed = false; mutable_denom.retain(|denom| { if has_removed { return true; } if let Some(factor) = conversion_factor(denom, &numer) { num /= factor; has_removed = true; return false; } true }); if !has_removed { new_numer.push(numer); } } mutable_denom.append(&mut mutable_other_denom); SassNumber { num: Number(num), unit: Unit::new(new_numer, mutable_denom), as_slash: None, } } pub fn assert_no_units(&self, name: &str, span: Span) -> SassResult<()> { if self.unit == Unit::None { Ok(()) } else { Err(( format!( "${name}: Expected {} to have no units.", inspect_number(self, &Options::default(), span)?, name = name, ), span, ) .into()) } } pub fn assert_unit(&self, unit: &Unit, name: &str, span: Span) -> SassResult<()> { if self.unit == *unit { Ok(()) } else { Err(( format!( "${name}: Expected {} to have unit \"{unit}\".", inspect_number(self, &Options::default(), span)?, name = name, unit = unit, ), span, ) .into()) } } pub fn assert_bounds(&self, name: &str, min: f64, max: f64, span: Span) -> SassResult<()> { self.assert_bounds_with_unit(name, min, max, &self.unit, span) } pub fn assert_int_with_name(&self, name: &'static str, span: Span) -> SassResult { match fuzzy_as_int(self.num.0) { Some(i) => Ok(i), None => Err(( format!( "${name}: {} is not an int.", inspect_number(self, &Options::default(), span)?, name = name, ), span, ) .into()), } } pub fn assert_bounds_with_unit( &self, name: &str, min: f64, max: f64, unit: &Unit, span: Span, ) -> SassResult<()> { if !(self.num <= Number(max) && self.num >= Number(min)) { return Err(( format!( "${}: Expected {} to be within {}{} and {}{}.", name, inspect_number(self, &Options::default(), span)?, inspect_float(min, &Options::default(), span), unit, inspect_float(max, &Options::default(), span), unit, ), span, ) .into()); } Ok(()) } pub fn is_comparable_to(&self, other: &Self) -> bool { self.unit.comparable(&other.unit) } /// For use in calculations pub fn has_possibly_compatible_units(&self, other: &Self) -> bool { if self.unit.is_complex() || other.unit.is_complex() { return false; } let known_compatibilities = match known_compatibilities_by_unit(&self.unit) { Some(known_compatibilities) => known_compatibilities, None => return true, }; known_compatibilities.contains(&other.unit) || known_compatibilities_by_unit(&other.unit).is_none() } pub fn unit(&self) -> &Unit { &self.unit } } impl PartialEq for SassNumber { fn eq(&self, other: &Self) -> bool { if !self.unit.comparable(&other.unit) { return false; } if (other.unit == Unit::None || self.unit == Unit::None) && self.unit != other.unit { return false; } self.num == other.num.convert(&other.unit, &self.unit) } } impl Eq for SassNumber {} impl Add for SassNumber { type Output = SassNumber; fn add(self, rhs: SassNumber) -> Self::Output { if self.unit == rhs.unit { SassNumber { num: self.num + rhs.num, unit: self.unit, as_slash: None, } } else if self.unit == Unit::None { SassNumber { num: self.num + rhs.num, unit: rhs.unit, as_slash: None, } } else if rhs.unit == Unit::None { SassNumber { num: self.num + rhs.num, unit: self.unit, as_slash: None, } } else { SassNumber { num: self.num + rhs.num.convert(&rhs.unit, &self.unit), unit: self.unit, as_slash: None, } } } } impl Sub for SassNumber { type Output = SassNumber; fn sub(self, rhs: SassNumber) -> Self::Output { if self.unit == rhs.unit { SassNumber { num: self.num - rhs.num, unit: self.unit, as_slash: None, } } else if self.unit == Unit::None { SassNumber { num: self.num - rhs.num, unit: rhs.unit, as_slash: None, } } else if rhs.unit == Unit::None { SassNumber { num: self.num - rhs.num, unit: self.unit, as_slash: None, } } else { SassNumber { num: self.num - rhs.num.convert(&rhs.unit, &self.unit), unit: self.unit, as_slash: None, } } } } impl Mul for SassNumber { type Output = SassNumber; fn mul(self, rhs: SassNumber) -> Self::Output { if rhs.unit == Unit::None { return SassNumber { num: self.num * rhs.num, unit: self.unit, as_slash: None, }; } self.multiply_units(self.num.0 * rhs.num.0, rhs.unit) } } impl Div for SassNumber { type Output = SassNumber; fn div(self, rhs: SassNumber) -> Self::Output { if rhs.unit == Unit::None { return SassNumber { num: self.num / rhs.num, unit: self.unit, as_slash: None, }; } self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert()) } }