diff --git a/Cargo.toml b/Cargo.toml index 7d39116..5a5e8ed 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ beef = "0.4.4" # criterion is not a dev-dependency because it makes tests take too # long to compile, and you cannot make dev-dependencies optional criterion = { version = "0.3.2", optional = true } +indexmap = "1.4.0" [features] default = ["commandline", "random"] diff --git a/src/atrule/mixin.rs b/src/atrule/mixin.rs index 5a82ab5..d8df725 100644 --- a/src/atrule/mixin.rs +++ b/src/atrule/mixin.rs @@ -155,7 +155,10 @@ impl Mixin { return Err(("Mixins may not contain mixin declarations.", span).into()) } Expr::Selector(selector) => { - let rules = self.eval(&super_selector.zip(&selector), content)?; + let rules = self.eval( + &selector.resolve_parent_selectors(super_selector, true), + content, + )?; stmts.push(Spanned { node: Stmt::RuleSet(RuleSet { super_selector: super_selector.clone(), diff --git a/src/atrule/mod.rs b/src/atrule/mod.rs index 1a3b251..88cb3df 100644 --- a/src/atrule/mod.rs +++ b/src/atrule/mod.rs @@ -153,14 +153,13 @@ impl AtRule { } } AtRuleKind::AtRoot => { - let mut selector = &Selector::replace( + let mut selector = &Selector::from_tokens( + &mut read_until_open_curly_brace(toks)?.into_iter().peekmore(), + scope, super_selector, - Selector::from_tokens( - &mut read_until_open_curly_brace(toks)?.into_iter().peekmore(), - scope, - super_selector, - )?, - ); + true, + )? + .resolve_parent_selectors(super_selector, false); let mut is_some = true; if selector.is_empty() { is_some = false; diff --git a/src/atrule/parse.rs b/src/atrule/parse.rs index 609b95d..af54c93 100644 --- a/src/atrule/parse.rs +++ b/src/atrule/parse.rs @@ -33,7 +33,7 @@ pub(crate) fn eat_stmts>( let rules = eat_stmts( toks, scope, - &super_selector.zip(&selector), + &selector.resolve_parent_selectors(super_selector, true), at_root, content, )?; @@ -81,11 +81,8 @@ pub(crate) fn eat_stmts_at_root>( ), Expr::MixinDecl(..) | Expr::FunctionDecl(..) => todo!(), Expr::Selector(mut selector) => { - if nesting > 1 || is_some { - selector = super_selector.zip(&selector); - } else { - selector = Selector::replace(super_selector, selector); - } + selector = + selector.resolve_parent_selectors(super_selector, nesting > 1 || is_some); nesting += 1; let rules = eat_stmts_at_root(toks, scope, &selector, nesting, true, content)?; nesting -= 1; diff --git a/src/builtin/list.rs b/src/builtin/list.rs index b245256..c9e17de 100644 --- a/src/builtin/list.rs +++ b/src/builtin/list.rs @@ -22,11 +22,7 @@ fn length(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassR fn nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { args.max_args(2)?; - let mut list = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, ..) => v, - Value::Map(m) => m.entries(), - v => vec![v], - }; + let mut list = arg!(args, scope, super_selector, 0, "list").as_list(); let n = match arg!(args, scope, super_selector, 1, "n") { Value::Dimension(num, _) => num, v => { @@ -260,11 +256,7 @@ fn is_bracketed(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> fn index(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { args.max_args(2)?; - let list = match arg!(args, scope, super_selector, 0, "list") { - Value::List(v, ..) => v, - Value::Map(m) => m.entries(), - v => vec![v], - }; + let list = arg!(args, scope, super_selector, 0, "list").as_list(); let value = arg!(args, scope, super_selector, 1, "value"); // TODO: find a way around this unwrap. // It should be impossible to hit as the arg is @@ -288,13 +280,7 @@ fn zip(args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult v, - Value::Map(m) => m.entries(), - v => vec![v], - }) - }) + .map(|x| Ok(x.node.eval(span)?.node.as_list())) .collect::>>>()?; let len = lists.iter().map(Vec::len).min().unwrap_or(0); diff --git a/src/builtin/mod.rs b/src/builtin/mod.rs index d1898ac..8e37009 100644 --- a/src/builtin/mod.rs +++ b/src/builtin/mod.rs @@ -50,6 +50,7 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy = Lazy::new(|| { map::declare(&mut m); math::declare(&mut m); meta::declare(&mut m); + selector::declare(&mut m); string::declare(&mut m); m }); diff --git a/src/builtin/selector.rs b/src/builtin/selector.rs index 8b13789..6f99801 100644 --- a/src/builtin/selector.rs +++ b/src/builtin/selector.rs @@ -1 +1,281 @@ +use super::{Builtin, GlobalFunctionMap}; +use crate::args::CallArgs; +use crate::common::{Brackets, ListSeparator, QuoteKind}; +use crate::error::SassResult; +use crate::scope::Scope; +use crate::selector::{ + ComplexSelector, ComplexSelectorComponent, Extender, Selector, SelectorList, +}; +use crate::value::Value; + +fn is_superselector( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(2)?; + let parent_selector = arg!(args, scope, super_selector, 0, "super").to_selector( + args.span(), + scope, + super_selector, + "super", + false, + )?; + let child_selector = arg!(args, scope, super_selector, 1, "sub").to_selector( + args.span(), + scope, + super_selector, + "sub", + false, + )?; + + Ok(Value::bool( + parent_selector.is_super_selector(&child_selector), + )) +} + +fn simple_selectors( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + // todo: Value::to_compound_selector + let selector = arg!(args, scope, super_selector, 0, "selector").to_selector( + args.span(), + scope, + super_selector, + "selector", + false, + )?; + + 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.get(0).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, + )) +} + +fn selector_parse( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(1)?; + Ok(arg!(args, scope, super_selector, 0, "selector") + .to_selector(args.span(), scope, super_selector, "selector", false)? + .into_value()) +} + +fn selector_nest(args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let span = args.span(); + let selectors = args.get_variadic(scope, super_selector)?; + 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(span, scope, super_selector, "selectors", true) + }) + .collect::>>()? + .into_iter() + .fold(Selector::new(), |parent, child| { + child.resolve_parent_selectors(&parent, true) + }) + .into_value()) +} + +fn selector_append(args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult { + let span = args.span(); + let selectors = args.get_variadic(scope, super_selector)?; + 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| { + let tmp = s + .node + .to_selector(span, scope, super_selector, "selectors", false)?; + if tmp.contains_parent_selector() { + Err(("Parent selectors aren't allowed here.", span).into()) + } else { + Ok(tmp) + } + }) + .collect::>>()?; + + let first = parsed_selectors.remove(0); + Ok(parsed_selectors + .into_iter() + .try_fold(first, |parent, child| -> SassResult { + Ok(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, parent), + span, + ) + .into()) + } + }]; + components.extend(complex.components.into_iter().skip(1)); + Ok(ComplexSelector { + components, + line_break: false, + }) + } else { + Err((format!("Can't append {} to {}.", complex, parent), span).into()) + } + }) + .collect::>>()?, + }) + .resolve_parent_selectors(&parent, false)) + })? + .into_value()) +} + +fn selector_extend( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(3)?; + let selector = arg!(args, scope, super_selector, 0, "selector").to_selector( + args.span(), + scope, + super_selector, + "selector", + false, + )?; + let target = arg!(args, scope, super_selector, 1, "extendee").to_selector( + args.span(), + scope, + super_selector, + "extendee", + false, + )?; + let source = arg!(args, scope, super_selector, 2, "extender").to_selector( + args.span(), + scope, + super_selector, + "extender", + false, + )?; + + Ok(Extender::extend(selector.0, source.0, target.0).to_sass_list()) +} + +fn selector_replace( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(3)?; + let selector = arg!(args, scope, super_selector, 0, "selector").to_selector( + args.span(), + scope, + super_selector, + "selector", + false, + )?; + let target = arg!(args, scope, super_selector, 1, "original").to_selector( + args.span(), + scope, + super_selector, + "original", + false, + )?; + let source = arg!(args, scope, super_selector, 2, "replacement").to_selector( + args.span(), + scope, + super_selector, + "replacement", + false, + )?; + Ok(Extender::replace(selector.0, source.0, target.0).to_sass_list()) +} + +fn selector_unify( + mut args: CallArgs, + scope: &Scope, + super_selector: &Selector, +) -> SassResult { + args.max_args(2)?; + let selector1 = arg!(args, scope, super_selector, 0, "selector1").to_selector( + args.span(), + scope, + super_selector, + "selector1", + false, + )?; + + if selector1.contains_parent_selector() { + return Err(( + "$selector1: Parent selectors aren't allowed here.", + args.span(), + ) + .into()); + } + + let selector2 = arg!(args, scope, super_selector, 1, "selector2").to_selector( + args.span(), + scope, + super_selector, + "selector2", + false, + )?; + + 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)); +} diff --git a/src/color/mod.rs b/src/color/mod.rs index 2162383..81b8806 100644 --- a/src/color/mod.rs +++ b/src/color/mod.rs @@ -47,7 +47,7 @@ impl Color { } } - fn new_hsla( + const fn new_hsla( red: Number, green: Number, blue: Number, diff --git a/src/common.rs b/src/common.rs index add4e2a..09a21fb 100644 --- a/src/common.rs +++ b/src/common.rs @@ -105,21 +105,6 @@ impl ListSeparator { } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct QualifiedName { - pub ident: String, - pub namespace: Option, -} - -impl Display for QualifiedName { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(namespace) = &self.namespace { - write!(f, "{}|", namespace)?; - } - f.write_str(&self.ident) - } -} - #[derive(Debug, Clone, Eq, PartialEq, Hash)] pub(crate) struct Identifier(String); diff --git a/src/error.rs b/src/error.rs index bc6db85..f1b9908 100644 --- a/src/error.rs +++ b/src/error.rs @@ -20,7 +20,7 @@ impl SassError { } } - pub(crate) fn from_loc(message: String, loc: SpanLoc) -> Self { + pub(crate) const fn from_loc(message: String, loc: SpanLoc) -> Self { SassError { kind: SassErrorKind::ParseError { message, loc }, } diff --git a/src/lib.rs b/src/lib.rs index f2fa4ad..92aa6c7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -59,6 +59,9 @@ grass input.scss clippy::filter_map, clippy::else_if_without_else, clippy::new_ret_no_self, + renamed_and_removed_lints, + clippy::unknown_clippy_lints, + clippy::replace_consts, // temporarily allowed while under heavy development. // eventually these allows should be refactored away @@ -67,6 +70,7 @@ grass input.scss clippy::todo, clippy::too_many_lines, clippy::panic, + clippy::unwrap_used, clippy::option_unwrap_used, clippy::result_unwrap_used, clippy::cast_possible_truncation, @@ -282,6 +286,7 @@ pub(crate) fn eat_expr>( &mut values.into_iter().peekmore(), scope, super_selector, + true, )?), span, })); diff --git a/src/output.rs b/src/output.rs index 95bdc7c..0ba38a3 100644 --- a/src/output.rs +++ b/src/output.rs @@ -75,7 +75,9 @@ impl Css { super_selector, rules, }) => { - let selector = super_selector.zip(&selector).remove_placeholders(); + let selector = selector + .resolve_parent_selectors(&super_selector, true) + .remove_placeholders(); if selector.is_empty() { return Ok(Vec::new()); } diff --git a/src/selector/attribute.rs b/src/selector/attribute.rs index 38736b9..7abf0b5 100644 --- a/src/selector/attribute.rs +++ b/src/selector/attribute.rs @@ -4,15 +4,15 @@ use peekmore::PeekMoreIterator; use codemap::Span; -use super::{Selector, SelectorKind}; -use crate::common::{QualifiedName, QuoteKind}; +use super::{Namespace, QualifiedName, Selector}; +use crate::common::QuoteKind; use crate::error::SassResult; use crate::scope::Scope; use crate::utils::{devour_whitespace, eat_ident, is_ident, parse_quoted_string}; use crate::value::Value; use crate::Token; -#[derive(Clone, Debug, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct Attribute { attr: QualifiedName, value: String, @@ -40,7 +40,7 @@ fn attribute_name>( let ident = eat_ident(toks, scope, super_selector, span_before)?.node; return Ok(QualifiedName { ident, - namespace: Some('*'.to_string()), + namespace: Namespace::Asterisk, }); } let span_before = next.pos; @@ -49,7 +49,7 @@ fn attribute_name>( Some(v) if v.kind != '|' => { return Ok(QualifiedName { ident: name_or_namespace.node, - namespace: None, + namespace: Namespace::None, }); } Some(..) => {} @@ -57,14 +57,14 @@ fn attribute_name>( } match toks.peek_forward(1) { Some(v) if v.kind == '=' => { - toks.peek_backward(1).unwrap(); + toks.reset_view(); return Ok(QualifiedName { ident: name_or_namespace.node, - namespace: None, + namespace: Namespace::None, }); } Some(..) => { - toks.peek_backward(1).unwrap(); + toks.reset_view(); } None => return Err(("expected more input.", name_or_namespace.span).into()), } @@ -72,7 +72,7 @@ fn attribute_name>( let ident = eat_ident(toks, scope, super_selector, span_before)?.node; Ok(QualifiedName { ident, - namespace: Some(name_or_namespace.node), + namespace: Namespace::Other(name_or_namespace.node), }) } @@ -100,19 +100,19 @@ impl Attribute { scope: &Scope, super_selector: &Selector, start: Span, - ) -> SassResult { + ) -> SassResult { devour_whitespace(toks); let attr = attribute_name(toks, scope, super_selector, start)?; devour_whitespace(toks); if toks.peek().ok_or(("expected more input.", start))?.kind == ']' { toks.next(); - return Ok(SelectorKind::Attribute(Attribute { + return Ok(Attribute { attr, value: String::new(), modifier: None, op: AttributeOp::Any, span: start, - })); + }); } let op = attribute_operator(toks, start)?; @@ -152,13 +152,13 @@ impl Attribute { toks.next(); - Ok(SelectorKind::Attribute(Attribute { + Ok(Attribute { op, attr, value, modifier, span: start, - })) + }) } } @@ -201,7 +201,7 @@ impl Display for Attribute { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq)] +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] enum AttributeOp { /// \[attr\] /// diff --git a/src/selector/common.rs b/src/selector/common.rs new file mode 100644 index 0000000..908ef42 --- /dev/null +++ b/src/selector/common.rs @@ -0,0 +1,50 @@ +use std::fmt::{self, Display}; + +/// 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(String), + None, +} + +impl 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 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 } + } +} diff --git a/src/selector/complex.rs b/src/selector/complex.rs new file mode 100644 index 0000000..4655918 --- /dev/null +++ b/src/selector/complex.rs @@ -0,0 +1,288 @@ +use super::{CompoundSelector, Pseudo, SelectorList, SimpleSelector, Specificity}; +use std::fmt::{self, Display, Write}; + +/// 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, Eq, PartialEq, Hash)] +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, +} + +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 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, parent: SelectorList) -> Option> { + match self { + Self::Compound(c) => c.resolve_parent_selectors(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), + } + } +} diff --git a/src/selector/compound.rs b/src/selector/compound.rs new file mode 100644 index 0000000..c3f3612 --- /dev/null +++ b/src/selector/compound.rs @@ -0,0 +1,224 @@ +use std::fmt::{self, Write}; + +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, parent: SelectorList) -> Option> { + 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 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 SimpleSelector::Pseudo(pseudo); + } + pseudo.selector = + Some(sel.resolve_parent_selectors(Some(parent.clone()), false)); + SimpleSelector::Pseudo(pseudo) + } else { + SimpleSelector::Pseudo(pseudo) + } + } else { + simple + } + }) + .collect() + } else { + self.components.clone() + }; + + if let Some(SimpleSelector::Parent(suffix)) = self.components.first() { + if self.components.len() == 1 && suffix.is_none() { + return Some(parent.components); + } + } else { + return Some(vec![ComplexSelector { + components: vec![ComplexSelectorComponent::Compound(CompoundSelector { + components: resolved_members, + })], + line_break: false, + }]); + } + + 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 { + todo!("throw SassScriptException('Parent \"$complex\" is incompatible with this selector.');") + }; + + let last = if let Some(SimpleSelector::Parent(Some(suffix))) = self.components.first() { + let mut components = last.components; + let mut end = components.pop().unwrap(); + end.add_suffix(suffix); + components.push(end); + components.extend(resolved_members.clone().into_iter().skip(1)); + CompoundSelector { components } + } else { + let mut components = last.components; + components.extend(resolved_members.clone().into_iter().skip(1)); + CompoundSelector { components } + }; + + complex.components.pop(); + + let mut components = complex.components; + components.push(ComplexSelectorComponent::Compound(last)); + + ComplexSelector { components, line_break: 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 } + } + }) + } +} diff --git a/src/selector/extend/extension.rs b/src/selector/extend/extension.rs new file mode 100644 index 0000000..6560da3 --- /dev/null +++ b/src/selector/extend/extension.rs @@ -0,0 +1,59 @@ +use codemap::Span; + +use super::{ComplexSelector, CssMediaQuery, SimpleSelector}; + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub(super) 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. + // todo: Option + pub media_context: Vec, + + /// The span in which `extender` was defined. + pub span: Option, +} + +impl Extension { + pub fn one_off(extender: ComplexSelector, specificity: Option, is_original: bool) -> Self { + Self { + specificity: specificity.unwrap_or_else(|| extender.max_specificity()), + extender, + target: None, + span: None, + is_optional: true, + is_original, + media_context: Vec::new(), + } + } + + /// Asserts that the `media_context` for a selector is compatible with the + /// query context for this extender. + pub fn assert_compatible_media_context(&self, media_context: &Option>) { + if let Some(media_context) = media_context { + if &self.media_context == media_context { + return; + } + } + + // todo!("throw SassException(\"You may not @extend selectors across media queries.\", span);") + } +} diff --git a/src/selector/extend/functions.rs b/src/selector/extend/functions.rs new file mode 100644 index 0000000..5527dc5 --- /dev/null +++ b/src/selector/extend/functions.rs @@ -0,0 +1,788 @@ +#![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( + complexes: Vec>, +) -> Vec> { + let mut prefixes: Vec> = vec![complexes.first().unwrap().clone()]; + + for complex in complexes.into_iter().skip(1) { + if complex.is_empty() { + continue; + } + + let target = complex.last().unwrap().clone(); + + if complex.len() == 1 { + for prefix in &mut prefixes { + prefix.push(target.clone()); + } + continue; + } + + let complex_len = complex.len(); + + let parents: Vec = + complex.into_iter().take(complex_len - 1).collect(); + let mut new_prefixes: Vec> = Vec::new(); + + for prefix in prefixes { + let parent_prefixes = weave_parents(prefix, parents.clone()); + + if let Some(parent_prefixes) = parent_prefixes { + 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(sequence.get(0).unwrap().clone(), 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.get(0) { + combinators_one.push(*c); + components_one.pop_front(); + } + + let mut combinators_two = Vec::new(); + + while let Some(ComplexSelectorComponent::Combinator(c)) = components_two.get(0) { + 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. +#[allow(clippy::cast_sign_loss, clippy::cast_possible_wrap)] +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_else(Vec::new); + + 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 + .get(components_one.len() - 1) + .unwrap() + .is_combinator()) + && (components_two.is_empty() + || !components_two + .get(components_two.len() - 1) + .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 = if combinators_one.is_empty() { + None + } else { + combinators_one.first() + }; + + let combinator_two = if combinators_two.is_empty() { + None + } else { + 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.get(components_one.len() - 1) + { + if let Some(ComplexSelectorComponent::Compound(c2)) = + components_two.get(components_two.len() - 1) + { + 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.get(components_one.len() - 1) + { + if let Some(ComplexSelectorComponent::Compound(c2)) = + components_two.get(components_two.len() - 1) + { + 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) => todo!("the above, but we dont have access to combinator_two"), + } +} + +/// 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.get(0) { + 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(); + + let mut group = if let Some(c) = iter.next() { + vec![c] + } else { + return groups; + }; + + groups.push_back(group.clone()); + + for c in iter { + if group + .last() + .map_or(false, ComplexSelectorComponent::is_combinator) + || c.is_combinator() + { + group.push(c); + } else { + group = vec![c]; + groups.push_back(group.clone()); + } + } + + 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 { + components: complex_one, + line_break: false, + } + .is_super_selector(&ComplexSelector { + components: complex_two, + line_break: 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, .. })) +} diff --git a/src/selector/extend/mod.rs b/src/selector/extend/mod.rs new file mode 100644 index 0000000..b2afa00 --- /dev/null +++ b/src/selector/extend/mod.rs @@ -0,0 +1,800 @@ +use std::collections::{HashMap, HashSet, VecDeque}; + +use indexmap::IndexMap; + +use super::{ + ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SelectorList, + SimpleSelector, +}; + +use extension::Extension; +pub(crate) use functions::unify_complex; +use functions::{paths, weave}; + +mod extension; +mod functions; + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) struct CssMediaQuery; + +#[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, Eq, PartialEq, Default)] +pub(crate) struct Extender { + /// 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: HashSet, + + /// The mode that controls this extender's behavior. + mode: ExtendMode, +} + +impl Extender { + /// 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, + ) -> SelectorList { + Self::extend_or_replace(selector, source, targets, ExtendMode::AllTargets) + } + + pub fn replace( + selector: SelectorList, + source: SelectorList, + targets: SelectorList, + ) -> SelectorList { + Self::extend_or_replace(selector, source, targets, ExtendMode::Replace) + } + + fn extend_or_replace( + mut selector: SelectorList, + source: SelectorList, + targets: SelectorList, + mode: ExtendMode, + ) -> SelectorList { + let extenders: IndexMap = source + .components + .clone() + .into_iter() + .zip( + source + .components + .into_iter() + .map(|complex| Extension::one_off(complex, None, false)), + ) + .collect(); + + for complex in targets.components { + if complex.components.len() != 1 { + todo!("throw SassScriptException(\"Can't extend complex selector $complex.\");") + } + + let compound = match complex.components.first() { + Some(ComplexSelectorComponent::Compound(c)) => c, + Some(..) | None => todo!(), + }; + + let extensions: HashMap> = + compound + .components + .clone() + .into_iter() + .map(|simple| (simple, extenders.clone())) + .collect(); + + let mut extender = Extender::with_mode(mode); + if !selector.is_invisible() { + extender + .originals + .extend(selector.components.clone().into_iter()); + } + + selector = extender.extend_list(selector, &extensions, &None); + } + + selector + } + + fn with_mode(mode: ExtendMode) -> Self { + Self { + mode, + ..Extender::default() + } + } + + /// Extends `list` using `extensions`. + fn extend_list( + &mut self, + list: SelectorList, + extensions: &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: Vec = Vec::new(); + for i in 0..list.components.len() { + let complex = list.components.get(i).unwrap().clone(); + + if let Some(result) = + self.extend_complex(complex.clone(), extensions, media_query_context) + { + if extended.is_empty() && i != 0 { + extended = list.components[0..i].to_vec(); + } + extended.extend(result.into_iter()); + } else if !extended.is_empty() { + extended.push(complex); + } + } + + if extended.is_empty() { + return list; + } + + SelectorList { + components: self.trim(extended, |complex| self.originals.contains(&complex)), + } + } + + /// Extends `complex` using `extensions`, and returns the contents of a + /// `SelectorList`. + fn extend_complex( + &mut self, + complex: ComplexSelector, + extensions: &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: Vec> = Vec::new(); + + let complex_has_line_break = complex.line_break; + + for i in 0..complex.components.len() { + if let Some(ComplexSelectorComponent::Compound(component)) = complex.components.get(i) { + if let Some(extended) = + self.extend_compound(component, extensions, media_query_context) + { + if extended_not_expanded.is_empty() { + extended_not_expanded = complex + .components + .clone() + .into_iter() + .take(i) + .map(|component| { + vec![ComplexSelector { + components: vec![component], + line_break: complex.line_break, + }] + }) + .collect(); + } + extended_not_expanded.push(extended); + } else { + extended_not_expanded.push(vec![ComplexSelector { + components: vec![ComplexSelectorComponent::Compound(component.clone())], + line_break: false, + }]) + } + } + } + + if extended_not_expanded.is_empty() { + return None; + } + + let mut first = true; + + let mut originals: Vec = Vec::new(); + + 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 { + components, + line_break: complex_has_line_break + || path.iter().any(|input_complex| input_complex.line_break), + }; + + if first && originals.contains(&complex.clone()) { + originals.push(output_complex.clone()); + } + first = false; + + output_complex + }) + .collect::>() + }) + .collect(), + ) + } + + /// Extends `compound` using `extensions`, and returns the contents of a + /// `SelectorList`. + fn extend_compound( + &mut self, + compound: &CompoundSelector, + extensions: &HashMap>, + media_query_context: &Option>, + ) -> 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: Vec> = Vec::new(); + + for i in 0..compound.components.len() { + let simple = compound.components.get(i).cloned().unwrap(); + + if let Some(extended) = self.extend_simple( + simple.clone(), + extensions, + media_query_context, + &mut targets_used, + ) { + if options.is_empty() && i != 0 { + options.push(vec![self.extension_for_compound( + compound.components.clone().into_iter().take(i).collect(), + )]); + } + + options.extend(extended.into_iter()); + } else { + options.push(vec![self.extension_for_simple(simple)]); + } + } + + if options.is_empty() { + return None; + } + + // If `self.mode` isn't `ExtendMode::Normal` and we didn't use all the targets in + // `extensions`, extension fails for `compound`. + if !targets_used.is_empty() && targets_used.len() != extensions.len() { + 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: Vec>> = 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| { + 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 { + components, + line_break, + }) + .collect(), + ) + }) + .collect(); + + Some( + unified_paths + .into_iter() + .filter_map(|complexes| complexes) + .flatten() + .collect(), + ) + } + + fn extend_simple( + &mut self, + simple: SimpleSelector, + extensions: &HashMap>, + media_query_context: &Option>, + targets_used: &mut HashSet, + ) -> Option>> { + if let SimpleSelector::Pseudo( + simple @ Pseudo { + selector: Some(..), .. + }, + ) = simple.clone() + { + 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: &HashMap>, + media_query_context: &Option>, + ) -> Option> { + let extended = self.extend_list( + pseudo.selector.clone().unwrap_or_else(SelectorList::new), + extensions, + media_query_context, + ); + /*todo: identical(extended, pseudo.selector)*/ + if Some(&extended) == pseudo.selector.as_ref() { + 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.as_str() { + "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. + if inner_pseudo.normalized_name == "matches" { + inner_pseudo.selector.clone().unwrap().components + } else { + Vec::new() + } + } + "matches" | "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(SelectorList { + components: vec![complex], + })) + }) + .collect::>(); + if result.is_empty() { + None + } else { + Some(result) + } + } else { + Some(vec![pseudo.with_selector(Some(SelectorList { + components: complexes, + }))]) + } + } + + // Extends `simple` without extending the contents of any selector pseudos + // it contains. + fn without_pseudo( + &self, + simple: SimpleSelector, + extensions: &HashMap>, + targets_used: &mut HashSet, + mode: ExtendMode, + ) -> Option> { + let extenders = 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.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 { + components: vec![ComplexSelectorComponent::Compound(CompoundSelector { + components: vec![simple], + })], + line_break: false, + }, + specificity, + true, + ) + } + + /// 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 { + components: vec![ComplexSelectorComponent::Compound(compound)], + line_break: false, + }, + specificity, + true, + ) + } + + /// 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: impl 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 + loop { + let mut should_break_to_outer = false; + for i in (0..=(selectors.len().saturating_sub(1))).rev() { + let complex1 = selectors.get(i).unwrap(); + if is_original(complex1.clone()) { + // 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).unwrap() == complex1 { + rotate_slice(&mut result, 0, j + 1); + should_break_to_outer = true; + break; + } + } + if should_break_to_outer { + break; + } + 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()); + } + if should_break_to_outer { + continue; + } + break; + } + + Vec::from(result) + } +} + +/// 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; + } +} diff --git a/src/selector/list.rs b/src/selector/list.rs new file mode 100644 index 0000000..7710ac3 --- /dev/null +++ b/src/selector/list.rs @@ -0,0 +1,259 @@ +use std::collections::VecDeque; +use std::{ + fmt::{self, Write}, + mem, +}; + +use super::{unify_complex, ComplexSelector, ComplexSelectorComponent}; + +use crate::common::{Brackets, ListSeparator, QuoteKind}; +use crate::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, Eq, PartialEq, Hash)] +pub(crate) struct SelectorList { + /// The components of this selector. + /// + /// This is never empty. + pub components: Vec, +} + +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 { + // todo: not emitted in compressed + 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() -> Self { + Self { + components: Vec::new(), + } + } + + 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 { + components: c, + line_break: false, + }) + .collect() + } else { + Vec::new() + } + }) + }) + .collect(); + + if contents.is_empty() { + return None; + } + + Some(Self { + components: contents, + }) + } + + /// 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`. + // todo: return SassResult (the issue is figuring out the span) + pub fn resolve_parent_selectors(self, parent: Option, implicit_parent: bool) -> Self { + let parent = match parent { + Some(p) => p, + None => { + if !self.contains_parent_selector() { + return self; + } + todo!("Top-level selectors may not contain the parent selector \"&\".") + } + }; + + Self { + components: flatten_vertically( + self.components + .into_iter() + .map(|complex| { + if !complex.contains_parent_selector() { + if !implicit_parent { + return vec![complex]; + } + return parent + .clone() + .components + .into_iter() + .map(move |parent_complex| { + let mut components = parent_complex.components; + components.append(&mut complex.components.clone()); + ComplexSelector { + components, + line_break: 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(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; + new_complexes + .into_iter() + .map(|new_complex| { + i += 1; + ComplexSelector { + components: new_complex, + line_break: line_breaks[i - 1], + } + }) + .collect() + }) + .collect(), + ), + } + } + + 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 = queues + .into_iter() + .filter(|queue| !queue.is_empty()) + .collect(); + } + + result +} diff --git a/src/selector/mod.rs b/src/selector/mod.rs index 06461b3..94becaa 100644 --- a/src/selector/mod.rs +++ b/src/selector/mod.rs @@ -1,234 +1,55 @@ -use std::fmt::{self, Display, Write}; - -use codemap::Span; +use std::fmt::{self, Display}; use peekmore::{PeekMore, PeekMoreIterator}; -use crate::common::{Brackets, ListSeparator, QuoteKind}; use crate::error::SassResult; use crate::scope::Scope; -use crate::utils::{ - devour_whitespace, eat_comment, eat_ident_no_interpolation, parse_interpolation, - read_until_closing_paren, read_until_newline, IsWhitespace, -}; +use crate::utils::{devour_whitespace, eat_comment, parse_interpolation, read_until_newline}; use crate::value::Value; use crate::Token; -use attribute::Attribute; +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; #[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) struct Selector(Vec); - -#[derive(Clone, Debug, Eq, PartialEq)] -struct SelectorPart { - pub inner: Vec, - pub is_invisible: bool, - pub has_newline: bool, - pub contains_super_selector: bool, -} - -impl SelectorPart { - pub fn into_value(&self) -> Value { - let mut kinds = Vec::new(); - let mut this_kind = Vec::new(); - for kind in &self.inner { - match kind { - SelectorKind::Whitespace => { - if !this_kind.is_empty() { - kinds.push(SelectorPart { - inner: std::mem::take(&mut this_kind), - is_invisible: false, - has_newline: false, - contains_super_selector: false, - }); - } - } - v => this_kind.push(v.clone()), - } - } - if !this_kind.is_empty() { - kinds.push(SelectorPart { - inner: std::mem::take(&mut this_kind), - is_invisible: false, - has_newline: false, - contains_super_selector: false, - }); - } - Value::List( - kinds - .iter() - .map(|s| Value::String(s.to_string(), QuoteKind::None)) - .collect(), - ListSeparator::Space, - Brackets::None, - ) - } -} - -impl Display for SelectorPart { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let mut iter = self.inner.iter().peekmore(); - devour_whitespace(&mut iter); - while let Some(s) = iter.next() { - write!(f, "{}", s)?; - if devour_whitespace(&mut iter) { - match iter.peek() { - Some(SelectorKind::Universal) - | Some(SelectorKind::Following) - | Some(SelectorKind::ImmediateChild) - | Some(SelectorKind::Preceding) => { - f.write_char(' ')?; - write!(f, "{}", iter.next().unwrap())?; - devour_whitespace(&mut iter); - if iter.peek().is_some() { - f.write_char(' ')?; - } - } - Some(..) => { - f.write_char(' ')?; - } - None => break, - } - } - } - Ok(()) - } -} +pub(crate) struct Selector(pub SelectorList); impl Display for Selector { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - for (idx, part) in self.0.iter().enumerate() { - write!(f, "{}", part)?; - if idx + 1 < self.0.len() { - f.write_char(',')?; - if part.has_newline { - f.write_char('\n')?; - } else { - f.write_char(' ')?; - } - } - } - Ok(()) + write!(f, "{}", self.0) } } -#[derive(Clone, Debug, Eq, PartialEq)] -pub(crate) enum SelectorKind { - /// Any string - /// - /// `button` - Element(String), - - /// An id selector - /// - /// `#` - Id(String), - - /// A class selector - /// - /// `.` - Class(String), - - /// A universal selector - /// - /// `*` - Universal, - - /// Select all immediate children - /// - /// `>` - ImmediateChild, - - /// Select all elements immediately following - /// - /// `+` - Following, - - /// Select elements preceeded by - /// - /// `~` - Preceding, - - /// Select elements with attribute - /// - /// `[lang|=en]` - Attribute(Attribute), - - Super, - - /// Pseudo selector - /// - /// `:hover` - Pseudo(String), - - /// Pseudo element selector - /// - /// `::before` - PseudoElement(String), - - /// Pseudo selector with additional parens - /// - /// `:any(h1, h2, h3, h4, h5, h6)` - PseudoParen(String, Selector), - - /// Placeholder selector - /// - /// `%` - Placeholder(String), - - /// Denotes whitespace between two selectors - Whitespace, -} - -impl IsWhitespace for &SelectorKind { - fn is_whitespace(&self) -> bool { - self == &&SelectorKind::Whitespace - } -} - -impl Display for SelectorKind { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - SelectorKind::Element(s) => write!(f, "{}", s), - SelectorKind::Id(s) => write!(f, "#{}", s), - SelectorKind::Class(s) => write!(f, ".{}", s), - SelectorKind::Universal => write!(f, "*"), - SelectorKind::ImmediateChild => write!(f, ">"), - SelectorKind::Following => write!(f, "+"), - SelectorKind::Preceding => write!(f, "~"), - SelectorKind::Attribute(attr) => write!(f, "{}", attr), - SelectorKind::Pseudo(s) => write!(f, ":{}", s), - SelectorKind::PseudoElement(s) => write!(f, "::{}", s), - SelectorKind::PseudoParen(s, val) => write!(f, ":{}({})", s, val), - SelectorKind::Placeholder(s) => write!(f, "%{}", s), - SelectorKind::Super => unreachable!("& selector should not be emitted"), - SelectorKind::Whitespace => f.write_char(' '), - } - } -} - -fn is_selector_name_char(c: char) -> bool { - c.is_ascii_alphanumeric() - || c == '_' - || c == '\\' - || (!c.is_ascii() && !c.is_control()) - || c == '-' -} - impl Selector { pub fn from_tokens>( toks: &mut PeekMoreIterator, scope: &Scope, super_selector: &Selector, + allows_parent: bool, ) -> SassResult { let mut string = String::new(); + let mut span = if let Some(tok) = toks.peek() { tok.pos() } else { return Ok(Selector::new()); }; + while let Some(tok) = toks.next() { span = span.merge(tok.pos()); match tok.kind { @@ -279,325 +100,47 @@ impl Selector { break; } - let mut inner = Vec::new(); - let mut is_invisible = false; - let mut has_newline = false; - let mut contains_super_selector = false; - let mut parts = Vec::new(); - - let mut sel_toks = Vec::new(); - - sel_toks.extend(string.chars().map(|x| Token::new(span, x))); + let sel_toks: Vec = string.chars().map(|x| Token::new(span, x)).collect(); let mut iter = sel_toks.into_iter().peekmore(); - while let Some(tok) = iter.peek() { - let Token { kind, pos } = *tok; - inner.push(match kind { - _ if is_selector_name_char(kind) => { - inner.push(SelectorKind::Element( - eat_ident_no_interpolation(&mut iter, false, pos)?.node, - )); - continue; - } - '&' => { - contains_super_selector = true; - SelectorKind::Super - } - '.' => { - iter.next(); - inner.push(SelectorKind::Class( - eat_ident_no_interpolation(&mut iter, false, pos)?.node, - )); - continue; - } - '#' => { - iter.next(); - inner.push(SelectorKind::Id( - eat_ident_no_interpolation(&mut iter, false, pos)?.node, - )); - continue; - } - '%' => { - iter.next(); - is_invisible = true; - inner.push(SelectorKind::Placeholder( - eat_ident_no_interpolation(&mut iter, false, pos)?.node, - )); - continue; - } - '>' => SelectorKind::ImmediateChild, - '+' => SelectorKind::Following, - '~' => SelectorKind::Preceding, - '*' => SelectorKind::Universal, - ',' => { - iter.next(); - if iter.peek().is_some() && iter.peek().unwrap().kind == '\n' { - has_newline = true; - } - if !inner.is_empty() { - parts.push(SelectorPart { - inner: inner.clone(), - is_invisible, - has_newline, - contains_super_selector, - }); - inner.clear(); - } - is_invisible = false; - has_newline = false; - contains_super_selector = false; - devour_whitespace(&mut iter); - continue; - } - '[' => { - let span = iter.next().unwrap().pos(); - inner.push(Attribute::from_tokens( - &mut iter, - scope, - super_selector, - span, - )?); - continue; - } - ':' => { - iter.next(); - let sel = Self::consume_pseudo_selector(&mut iter, scope, super_selector, pos)?; - match &sel { - SelectorKind::PseudoParen(_, s) => { - if s.contains_super_selector() { - contains_super_selector = true; - } - } - _ => {} - } - inner.push(sel); - continue; - } - c if c.is_whitespace() => { - if devour_whitespace(&mut iter) { - inner.push(SelectorKind::Whitespace); - } - continue; - } - _ => return Err(("expected selector.", tok.pos()).into()), - }); - iter.next(); - } - - if !inner.is_empty() { - parts.push(SelectorPart { - inner, - is_invisible, - has_newline, - contains_super_selector, - }); - } - - Ok(Selector(parts)) + Ok(Selector( + SelectorParser::new(&mut iter, scope, super_selector, allows_parent, true, span) + .parse()?, + )) } - fn consume_pseudo_selector>( - toks: &mut PeekMoreIterator, - scope: &Scope, - super_selector: &Selector, - span_before: Span, - ) -> SassResult { - let is_pseudo_element = if toks - .peek() - .ok_or(("Expected identifier.", span_before))? - .kind - == ':' - { - toks.next(); - true - } else { - false - }; - let t = if let Some(tok) = toks.peek() { - *tok - } else { - todo!() - }; - - if !is_selector_name_char(t.kind) { - return Err(("Expected identifier.", t.pos).into()); - } - - let name = eat_ident_no_interpolation(toks, false, t.pos)?.node; - Ok( - if toks.peek().is_some() && toks.peek().unwrap().kind == '(' { - toks.next(); - let mut inner_toks = read_until_closing_paren(toks)?; - inner_toks.pop(); - let inner = Selector::from_tokens( - &mut inner_toks.into_iter().peekmore(), - scope, - super_selector, - )?; - SelectorKind::PseudoParen(name, inner) - } else if is_pseudo_element { - SelectorKind::PseudoElement(name) + /// 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) -> Self { + Self(self.0.clone().resolve_parent_selectors( + if parent.is_empty() { + None } else { - SelectorKind::Pseudo(name) + Some(parent.0.clone()) }, - ) + implicit_parent, + )) } - pub fn replace(super_selector: &Selector, this: Selector) -> Selector { - if super_selector.0.is_empty() || this.0.is_empty() { - return this; - } - let mut parts = Vec::with_capacity(super_selector.0.len()); - for (idx, part) in super_selector.clone().0.into_iter().enumerate() { - let mut found_inner = false; - for part2 in this.clone().0 { - if !part2.contains_super_selector { - if idx == 0 { - parts.push(part2); - } - continue; - } - let mut kinds = Vec::new(); - for kind in part2.clone().inner { - match kind { - SelectorKind::Super => kinds.extend(part.inner.clone()), - SelectorKind::PseudoParen(name, inner) => { - if inner.contains_super_selector() { - found_inner = true; - kinds.push(SelectorKind::PseudoParen( - name, - Selector::replace(super_selector, inner), - )) - } else { - kinds.push(SelectorKind::PseudoParen(name, inner)); - } - } - _ => kinds.push(kind), - } - } - parts.push(SelectorPart { - inner: kinds, - is_invisible: part2.is_invisible, - has_newline: part2.has_newline, - contains_super_selector: false, - }); - } - if found_inner { - break; - } - } - Selector(parts) + pub fn is_super_selector(&self, other: &Self) -> bool { + self.0.is_superselector(&other.0) } - pub fn zip(&self, other: &Selector) -> Selector { - if self.0.is_empty() { - return Selector(other.0.clone()); - } else if other.0.is_empty() { - return self.clone(); - } - let mut rules = Vec::with_capacity(self.0.len()); - for sel1 in self.clone().0 { - let mut found_inner = false; - for sel2 in other.clone().0 { - let mut this_selector: Vec = Vec::with_capacity(other.0.len()); - let mut found_super = false; - - for sel in sel2.inner { - match sel { - SelectorKind::Super => { - this_selector.extend(sel1.inner.clone()); - found_super = true; - } - SelectorKind::PseudoParen(s, inner_selector) => { - if inner_selector.contains_super_selector() { - found_super = true; - found_inner = true; - this_selector.push(SelectorKind::PseudoParen( - s, - Selector::replace(self, inner_selector), - )) - } else { - this_selector.push(SelectorKind::PseudoParen(s, inner_selector)); - } - } - _ => this_selector.push(sel), - } - } - - if !found_super { - let mut x = std::mem::take(&mut this_selector); - let mut y = sel1.clone().inner; - y.push(SelectorKind::Whitespace); - y.append(&mut x); - this_selector = y; - } - rules.push(SelectorPart { - inner: this_selector, - is_invisible: sel1.is_invisible || sel2.is_invisible, - has_newline: (sel1.has_newline || sel2.has_newline) && !found_inner, - contains_super_selector: false, - }); - } - if found_inner { - break; - } - } - Selector(rules) + pub fn contains_parent_selector(&self) -> bool { + self.0.contains_parent_selector() } pub fn remove_placeholders(self) -> Selector { - Selector( - self.0 + Self(SelectorList { + components: self + .0 + .components .into_iter() - .filter_map(|s| { - if s.is_invisible { - None - } else { - let mut inner = Vec::new(); - let mut last_was_whitespace = false; - let len = s.inner.len(); - for kind in s.inner { - match kind { - SelectorKind::PseudoParen(name, inner_selector) => { - let inner_empty = inner_selector.is_empty(); - let removed_placeholders = inner_selector.remove_placeholders(); - if removed_placeholders.is_empty() && !inner_empty { - if name.to_ascii_lowercase().as_str() == "not" { - if last_was_whitespace || len == 1 { - inner.push(SelectorKind::Universal); - } else { - continue; - } - } else { - return None; - } - } else { - inner.push(SelectorKind::PseudoParen( - name, - removed_placeholders, - )); - } - } - SelectorKind::Whitespace => { - last_was_whitespace = true; - inner.push(kind); - continue; - } - _ => inner.push(kind), - } - last_was_whitespace = false; - } - Some(SelectorPart { - inner, - is_invisible: false, - has_newline: s.has_newline, - contains_super_selector: s.contains_super_selector, - }) - } - }) + .filter(|c| !c.is_invisible()) .collect(), - ) + }) } pub fn is_empty(&self) -> bool { @@ -605,18 +148,14 @@ impl Selector { } pub const fn new() -> Selector { - Selector(Vec::new()) + Selector(SelectorList::new()) } - pub fn contains_super_selector(&self) -> bool { - self.0.iter().any(|s| s.contains_super_selector) + pub fn into_value(self) -> Value { + self.0.to_sass_list() } - pub fn into_value(&self) -> Value { - Value::List( - self.0.iter().map(SelectorPart::into_value).collect(), - ListSeparator::Comma, - Brackets::None, - ) + pub fn unify(self, other: &Self) -> Option { + Some(Selector(self.0.unify(&other.0)?)) } } diff --git a/src/selector/parse.rs b/src/selector/parse.rs new file mode 100644 index 0000000..e0bd091 --- /dev/null +++ b/src/selector/parse.rs @@ -0,0 +1,607 @@ +use peekmore::PeekMoreIterator; + +use codemap::Span; + +use super::{ + Attribute, Combinator, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, + Pseudo, QualifiedName, SelectorList, SimpleSelector, +}; +use crate::error::SassResult; +use crate::scope::Scope; +use crate::selector::Selector; +use crate::utils::{ + devour_whitespace, eat_ident, eat_ident_no_interpolation, is_name, is_name_start, + read_until_closing_paren, +}; +use crate::Token; + +#[derive(PartialEq)] +enum DevouredWhitespace { + /// Some whitespace was found + Whitespace, + /// A newline and potentially other whitespace was found + Newline, + /// No whitespace was found + None, +} + +impl DevouredWhitespace { + fn found_whitespace(&mut self) { + if self == &Self::None { + *self = Self::Whitespace; + } + } + + fn found_newline(&mut self) { + *self = Self::Newline; + } +} + +/// Pseudo-class selectors that take unadorned selectors as arguments. +const SELECTOR_PSEUDO_CLASSES: [&str; 7] = [ + "not", + "matches", + "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<'a, I: Iterator> { + /// Whether this parser allows the parent selector `&`. + allows_parent: bool, + + /// Whether this parser allows placeholder selectors beginning with `%`. + allows_placeholder: bool, + + toks: &'a mut PeekMoreIterator, + + scope: &'a Scope, + + super_selector: &'a Selector, + + span: Span, +} + +impl<'a, I: Iterator> SelectorParser<'a, I> { + pub fn new( + toks: &'a mut PeekMoreIterator, + scope: &'a Scope, + super_selector: &'a Selector, + allows_parent: bool, + allows_placeholder: bool, + span: Span, + ) -> Self { + Self { + toks, + scope, + super_selector, + 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)?]; + + devour_whitespace(self.toks); + + let mut line_break = false; + + while let Some(Token { kind: ',', .. }) = self.toks.peek() { + self.toks.next(); + 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 }) + } + + fn eat_whitespace(&mut self) -> DevouredWhitespace { + let mut whitespace_devoured = DevouredWhitespace::None; + while let Some(tok) = self.toks.peek() { + match tok.kind { + ' ' | '\t' => whitespace_devoured.found_whitespace(), + '\n' => whitespace_devoured.found_newline(), + _ => break, + } + self.toks.next(); + } + + whitespace_devoured + } + + /// 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(); + + // todo: or patterns + loop { + devour_whitespace(self.toks); + + // 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 { + components, + line_break, + }) + } + + fn parse_compound_selector(&mut self) -> SassResult { + let mut components = vec![self.parse_simple_selector(true)?]; + + while let Some(Token { kind, .. }) = self.toks.peek() { + if !is_simple_selector_start(*kind) { + break; + } + + components.push(self.parse_simple_selector(false)?); + } + + Ok(CompoundSelector { components }) + } + + /// Returns whether the scanner is immediately before a plain CSS identifier. + /// + // todo: foward arg + /// If `forward` is passed, this looks that many characters forward instead. + /// + /// 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(&mut 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_forward(1) { + Some(Token { kind, .. }) if is_name_start(*kind) || kind == &'-' || kind == &'\\' => { + self.toks.reset_view(); + true + } + Some(..) | None => { + self.toks.reset_view(); + false + } + } + } + + fn looking_at_identifier_body(&mut self) -> bool { + matches!(self.toks.peek(), Some(t) if is_name(t.kind) || t.kind == '\\') + } + + /// Consumes a simple selector. + fn parse_simple_selector(&mut self, allow_parent: bool) -> 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: '&', .. }) => { + if !allow_parent && !self.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(Attribute::from_tokens( + self.toks, + self.scope, + self.super_selector, + self.span, + )?)) + } + + fn parse_class_selector(&mut self) -> SassResult { + self.toks.next(); + Ok(SimpleSelector::Class( + eat_ident(self.toks, self.scope, self.super_selector, self.span)?.node, + )) + } + + fn parse_id_selector(&mut self) -> SassResult { + self.toks.next(); + Ok(SimpleSelector::Id( + eat_ident(self.toks, self.scope, self.super_selector, self.span)?.node, + )) + } + + fn parse_pseudo_selector(&mut self) -> SassResult { + self.toks.next(); + let element = match self.toks.peek() { + Some(Token { kind: ':', .. }) => { + self.toks.next(); + true + } + _ => false, + }; + + let name = eat_ident(self.toks, self.scope, self.super_selector, self.span)?; + + match self.toks.peek() { + Some(Token { kind: '(', .. }) => self.toks.next(), + _ => { + return Ok(SimpleSelector::Pseudo(Pseudo { + // todo: we can store the reference to this + normalized_name: unvendor(&name.node).to_string(), + is_class: !element && !is_fake_pseudo_element(&name), + name: name.node, + selector: None, + is_syntactic_class: !element, + argument: None, + })); + } + }; + + devour_whitespace(self.toks); + + 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(self.parse_selector_list()?); + devour_whitespace(self.toks); + self.expect_closing_paren()?; + } else { + argument = Some(self.declaration_value()?); + } + } else if SELECTOR_PSEUDO_CLASSES.contains(&unvendored) { + selector = Some(self.parse_selector_list()?); + devour_whitespace(self.toks); + self.expect_closing_paren()?; + } else if unvendored == "nth-child" || unvendored == "nth-last-child" { + let mut this_arg = self.parse_a_n_plus_b()?; + let found_whitespace = devour_whitespace(self.toks); + #[allow(clippy::match_same_arms)] + match (found_whitespace, self.toks.peek()) { + (_, Some(Token { kind: ')', .. })) => {} + (true, _) => { + self.expect_identifier("of")?; + this_arg.push_str(" of"); + devour_whitespace(self.toks); + selector = Some(self.parse_selector_list()?); + } + _ => {} + } + self.expect_closing_paren()?; + argument = Some(this_arg); + } else { + argument = Some(self.declaration_value()?.trim_end().to_string()); + } + + Ok(SimpleSelector::Pseudo(Pseudo { + normalized_name: unvendor(&name.node).to_string(), + is_class: !element && !is_fake_pseudo_element(&name), + name: name.node, + selector, + // todo: we can store the reference to this + is_syntactic_class: !element, + argument, + })) + } + + fn parse_parent_selector(&mut self) -> SassResult { + self.toks.next(); + let suffix = if self.looking_at_identifier_body() { + Some(eat_ident(self.toks, self.scope, self.super_selector, self.span)?.node) + } else { + None + }; + Ok(SimpleSelector::Parent(suffix)) + } + + fn parse_placeholder_selector(&mut self) -> SassResult { + self.toks.next(); + Ok(SimpleSelector::Placeholder( + eat_ident(self.toks, self.scope, self.super_selector, self.span)?.node, + )) + } + + /// 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 { + self.toks.peek(); + + match self.toks.peek().cloned() { + Some(Token { kind: '*', pos }) => { + 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)); + } else { + return Ok(SimpleSelector::Type(QualifiedName { + ident: eat_ident(self.toks, self.scope, self.super_selector, pos)?.node, + namespace: Namespace::Asterisk, + })); + } + } else { + return Ok(SimpleSelector::Universal(Namespace::None)); + } + } + Some(Token { kind: '|', pos }) => { + 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: eat_ident(self.toks, self.scope, self.super_selector, pos)?.node, + namespace: Namespace::Empty, + })); + } + } + } + _ => {} + } + + let name_or_namespace = + eat_ident(self.toks, self.scope, self.super_selector, self.span)?.node; + + 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)) + } else { + SimpleSelector::Type(QualifiedName { + ident: eat_ident(self.toks, self.scope, self.super_selector, self.span)? + .node, + namespace: Namespace::Other(name_or_namespace), + }) + } + } + 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")?; + return Ok("even".to_string()); + } + Some(Token { kind: 'o', .. }) | Some(Token { kind: 'O', .. }) => { + self.expect_identifier("odd")?; + return Ok("odd".to_string()); + } + 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(); + } + devour_whitespace(self.toks); + if let Some(t) = self.toks.peek() { + if t.kind != 'n' && t.kind != 'N' { + return Ok(buf); + } + self.toks.next(); + } + } + Some(t) => { + if t.kind == 'n' || t.kind == 'N' { + self.toks.next(); + } else { + return Err(("Expected \"n\".", self.span).into()); + } + } + None => return Err(("expected more input.", self.span).into()), + } + + buf.push('n'); + + devour_whitespace(self.toks); + + if let Some(t @ Token { kind: '+', .. }) | Some(t @ Token { kind: '-', .. }) = + self.toks.peek() + { + buf.push(t.kind); + self.toks.next(); + devour_whitespace(self.toks); + 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) + } + + fn declaration_value(&mut self) -> SassResult { + // todo: this consumes the closing paren + let mut tmp = read_until_closing_paren(self.toks)?; + if let Some(Token { kind: ')', .. }) = tmp.pop() { + } else { + return Err(("expected \")\".", self.span).into()); + } + Ok(tmp.into_iter().map(|t| t.kind).collect::()) + } + + fn expect_identifier(&mut self, s: &str) -> SassResult<()> { + let mut ident = eat_ident_no_interpolation(self.toks, false, self.span)?.node; + ident.make_ascii_lowercase(); + if ident == s { + Ok(()) + } else { + Err((format!("Expected \"{}\".", s), self.span).into()) + } + } + + fn expect_closing_paren(&mut self) -> SassResult<()> { + if let Some(Token { kind: ')', .. }) = self.toks.next() { + Ok(()) + } else { + Err(("expected \")\".", self.span).into()) + } + } +} + +/// Returns whether `c` can start a simple selector other than a type +/// selector. +fn is_simple_selector_start(c: char) -> bool { + matches!(c, '*' | '[' | '.' | '#' | '%' | ':') +} + +/// Returns `name` without a vendor prefix. +/// +/// If `name` has no vendor prefix, it's returned as-is. +fn unvendor(name: &str) -> &str { + let bytes = name.as_bytes(); + + if bytes.len() < 2 { + return name; + } + + if bytes[0_usize] != b'-' || bytes[1_usize] == b'-' { + return name; + } + + for i in 2..bytes.len() { + if bytes.get(i) == Some(&b'-') { + return &name[i + 1..]; + } + } + + name +} + +/// 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()[0] { + b'a' | b'A' => name.to_ascii_lowercase() == "after", + b'b' | b'B' => name.to_ascii_lowercase() == "before", + b'f' | b'F' => match name.to_ascii_lowercase().as_str() { + "first-line" | "first-letter" => true, + _ => false, + }, + _ => false, + } +} diff --git a/src/selector/simple.rs b/src/selector/simple.rs new file mode 100644 index 0000000..1430de2 --- /dev/null +++ b/src/selector/simple.rs @@ -0,0 +1,635 @@ +use std::fmt::{self, Write}; + +use super::{ + Attribute, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Namespace, + QualifiedName, SelectorList, Specificity, +}; + +const SUBSELECTOR_PSEUDOS: [&str; 4] = ["matches", "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(Attribute), +} + +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(..) => todo!(), + } + } +} + +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, SelectorList::is_invisible) + } + Self::Placeholder(..) => true, + Self::Parent(..) => todo!(), + } + } + + pub fn add_suffix(&mut self, suffix: &str) { + 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!("Invalid parent selector"), //return Err((format!("Invalid parent selector \"{}\"", self), SPAN)), + } + } + + 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 compoments 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 { + todo!("ArgumentError.value(selector1, 'selector1', 'must be a UniversalSelector or a TypeSelector')") + } + + 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 { + todo!("ArgumentError.value(selector2, 'selector2', 'must be a UniversalSelector or a TypeSelector');") + } + + 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 [this], 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), + normalized_name, + .. + }) = their_simple + { + if SUBSELECTOR_PSEUDOS.contains(&normalized_name.as_str()) { + return sel.components.iter().all(|complex| { + if complex.components.len() != 1 { + return false; + }; + complex + .components + .get(0) + .unwrap() + .as_compound() + .components + .contains(self) + }); + } + false + } else { + false + } + }) + } +} + +#[derive(Clone, Debug, Eq, PartialEq, Hash)] +pub(crate) struct Pseudo { + /// The name of this selector. + pub name: String, + + /// Like `name`, but without any vendor prefixes. + pub normalized_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, +} + +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 { + match self.normalized_name.as_str() { + "matches" | "any" => { + let pseudos = selector_pseudos_named(compound.clone(), &self.name, true); + pseudos.iter().any(move |pseudo2| { + self.selector + .as_ref() + .unwrap() + .is_superselector(&pseudo2.selector.clone().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 { + components, + line_break: false, + }) + }) + } + "has" | "host" | "host-context" => { + selector_pseudos_named(compound.clone(), &self.name, true) + .iter() + .any(|pseudo2| { + self.selector + .as_ref() + .unwrap() + .is_superselector(&pseudo2.selector.clone().unwrap()) + }) + } + "slotted" => selector_pseudos_named(compound.clone(), &self.name, false) + .iter() + .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()], + }) + } else { + false + } + }) + }), + "current" => selector_pseudos_named(compound.clone(), &self.name, self.is_class) + .iter() + .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 } + } + } +} + +/// Returns all pseudo selectors in `compound` that have a selector argument, +/// and that have the given `name`. +// todo: return `impl Iterator` +fn selector_pseudos_named(compound: CompoundSelector, name: &str, is_class: bool) -> Vec { + compound + .components + .into_iter() + .filter_map(|c| { + if let SimpleSelector::Pseudo(p) = c { + Some(p) + } else { + None + } + }) + .filter(|p| p.is_class == is_class && p.selector.is_some() && p.name == name) + .collect() +} diff --git a/src/stylesheet.rs b/src/stylesheet.rs index 6ea1ad9..eeb0db0 100644 --- a/src/stylesheet.rs +++ b/src/stylesheet.rs @@ -421,7 +421,8 @@ impl<'a> StyleSheetParser<'a> { } Expr::Selector(s) => { self.nesting += 1; - let rules = self.eat_rules(&super_selector.zip(&s), scope)?; + let rules = + self.eat_rules(&s.resolve_parent_selectors(super_selector, true), scope)?; stmts.push(Spanned { node: Stmt::RuleSet(RuleSet { super_selector: super_selector.clone(), diff --git a/src/value/mod.rs b/src/value/mod.rs index c45f3a4..cefcc02 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -1,13 +1,17 @@ use std::iter::Iterator; +use peekmore::PeekMore; + use codemap::{Span, Spanned}; use crate::color::Color; use crate::common::{Brackets, ListSeparator, Op, QuoteKind}; use crate::error::SassResult; +use crate::scope::Scope; +use crate::selector::Selector; use crate::unit::Unit; use crate::utils::hex_char_for; -use crate::Cow; +use crate::{Cow, Token}; use css_function::is_special_function; pub(crate) use map::SassMap; @@ -309,4 +313,77 @@ impl Value { v => v.to_css_string(span)?, }) } + + pub fn as_list(self) -> Vec { + match self { + Value::List(v, ..) => v, + Value::Map(m) => m.entries(), + v => vec![v], + } + } + + /// 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, + span: Span, + scope: &Scope, + super_selector: &Selector, + name: &str, + allows_parent: bool, + ) -> SassResult { + let string = match self.clone().selector_string(span)? { + Some(v) => v, + None => return Err((format!("${}: {} is not a valid selector: it must be a string, a list of strings, or a list of lists of strings.", name, self.inspect(span)?), span).into()), + }; + Selector::from_tokens( + &mut string.chars().map(|c| Token::new(span, c)).peekmore(), + scope, + super_selector, + allows_parent, + ) + } + + fn selector_string(self, span: Span) -> SassResult> { + Ok(Some(match self.eval(span)?.node { + Self::String(text, ..) => text, + Self::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, ..) = complex { + result.push(match complex.selector_string(span)? { + Some(v) => v, + None => return Ok(None), + }); + } else { + return Ok(None); + } + } + } + ListSeparator::Space => { + 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), + })) + } } diff --git a/src/value/parse.rs b/src/value/parse.rs index 6577ab2..16d995e 100644 --- a/src/value/parse.rs +++ b/src/value/parse.rs @@ -809,7 +809,7 @@ impl Value { } '&' => { let span = toks.next().unwrap().pos(); - IntermediateValue::Value(super_selector.into_value()).span(span) + IntermediateValue::Value(super_selector.clone().into_value()).span(span) } '#' => { if let Some(Token { kind: '{', pos }) = toks.peek_forward(1) { diff --git a/tests/error.rs b/tests/error.rs index 708372c..6f37719 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -187,6 +187,7 @@ error!(toplevel_caret_alone, "^", "Error: expected \"{\"."); test!(toplevel_gt_as_selector, "> {}", ""); test!(toplevel_tilde_as_selector, "~ {}", ""); error!(toplevel_lt_as_selector, "< {}", "Error: expected selector."); +error!(toplevel_pipe, "| {}", "Error: Expected identifier."); error!( toplevel_question_as_selector, "? {}", "Error: expected selector." diff --git a/tests/is-superselector.rs b/tests/is-superselector.rs new file mode 100644 index 0000000..9d03d37 --- /dev/null +++ b/tests/is-superselector.rs @@ -0,0 +1,476 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + more_specific_class_compound, + "a {\n color: is-superselector(\".foo\", \".foo.bar\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + less_specific_class_compound, + "a {\n color: is-superselector(\".foo.bar\", \".foo\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + more_specific_class_complex, + "a {\n color: is-superselector(\".bar\", \".foo .bar\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + less_specific_class_complex, + "a {\n color: is-superselector(\".foo .bar\", \".bar\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + two_in_sub, + "a {\n color: is-superselector(\"c\", \"c, d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + two_in_super, + "a {\n color: is-superselector(\"c, d\", \"c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + two_in_both_equal, + "a {\n color: is-superselector(\"c, d\", \"c, d\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + two_in_both_subset, + "a {\n color: is-superselector(\"c, d\", \"c.e, d.f\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + two_in_both_superset, + "a {\n color: is-superselector(\"c.e, d.f\", \"c, d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + two_in_sub_satisfied_by_one_super, + "a {\n color: is-superselector(\".c\", \"d.c, e.c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + three_in_super_match_one, + "a {\n color: is-superselector(\"c, d, e\", \"d\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + three_in_super_match_two, + "a {\n color: is-superselector(\"c, d, e\", \"e, c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + three_in_super_match_three, + "a {\n color: is-superselector(\"c, d, e\", \"d, c, e\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + three_in_super_miss_one, + "a {\n color: is-superselector(\"c, d, e\", \"c, f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_attribute_equal, + "a {\n color: is-superselector(\"[c=d]\", \"[c=d]\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_attribute_different_attr, + "a {\n color: is-superselector(\"[c=d]\", \"[e=d]\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_attribute_different_val, + "a {\n color: is-superselector(\"[c=d]\", \"[c=e]\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_attribute_different_operator, + "a {\n color: is-superselector(\"[c=d]\", \"[c^=e]\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_class_equal, + "a {\n color: is-superselector(\".c\", \".c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_class_not_equal, + "a {\n color: is-superselector(\".c\", \".d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_id_equal, + "a {\n color: is-superselector(\"#c\", \"#c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_id_not_equal, + "a {\n color: is-superselector(\"#c\", \"#d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_placeholder_equal, + "a {\n color: is-superselector(\"%c\", \"%c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_placeholder_not_equal, + "a {\n color: is-superselector(\"%c\", \"%d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_equal, + "a {\n color: is-superselector(\"c\", \"c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_type_not_equal, + "a {\n color: is-superselector(\"c\", \"d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_and_universal, + "a {\n color: is-superselector(\"c\", \"*\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_explicit_namespace_equal, + "a {\n color: is-superselector(\"c|d\", \"c|d\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_type_different_explicit_namespace, + "a {\n color: is-superselector(\"c|d\", \"e|d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_explicit_namespace_and_implicit_namespace, + "a {\n color: is-superselector(\"c|d\", \"d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_explicit_namespace_and_empty_namespace, + "a {\n color: is-superselector(\"c|d\", \"|d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_explicit_namespace_and_universal_namespace, + "a {\n color: is-superselector(\"c|d\", \"*|d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_empty_namespace_and_explicit_namespace, + "a {\n color: is-superselector(\"|c\", \"d|c\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_type_empty_namespace_and_empty_namespace, + "a {\n color: is-superselector(\"|c\", \"|c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + #[ignore = "https://github.com/sass/dart-sass/issues/789"] + simple_type_universal_namespace_and_explicit_namespace, + "a {\n color: is-superselector(\"*|c\", \"d|c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + #[ignore = "https://github.com/sass/dart-sass/issues/789"] + simple_type_universal_namespace_and_implicit_namespace, + "a {\n color: is-superselector(\"*|c\", \"c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + #[ignore = "https://github.com/sass/dart-sass/issues/789"] + simple_type_universal_namespace_and_empty_namespace, + "a {\n color: is-superselector(\"*|c\", \"|c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_type_universal_namespace_and_universal_namespace, + "a {\n color: is-superselector(\"*|c\", \"*|c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_pseudo_no_args_equal, + "a {\n color: is-superselector(\":c\", \":c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_pseudo_no_args_different, + "a {\n color: is-superselector(\":c\", \":d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_no_args_class_and_element, + "a {\n color: is-superselector(\":c\", \"::c\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_no_args_element_and_element_equal, + "a {\n color: is-superselector(\"::c\", \"::c\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_pseudo_no_args_element_and_element_different, + "a {\n color: is-superselector(\"::c\", \"::d\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_no_args_element_and_class, + "a {\n color: is-superselector(\"::c\", \":c\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_class_equal, + "a {\n color: is-superselector(\":c(@#$)\", \":c(@#$)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_pseudo_arg_class_different_name, + "a {\n color: is-superselector(\":c(@#$)\", \":d(@#$)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_class_different_arg, + "a {\n color: is-superselector(\":c(@#$)\", \":d(*&^)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_class_different_no_arg, + "a {\n color: is-superselector(\":c(@#$)\", \":c\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_class_and_element, + "a {\n color: is-superselector(\":c(@#$)\", \"::c(@#$)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_element_and_element_equal, + "a {\n color: is-superselector(\"::c(@#$)\", \"::c(@#$)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + simple_pseudo_arg_element_and_element_different_name, + "a {\n color: is-superselector(\"::c(@#$)\", \"::d(@#$)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_element_and_element_different_arg, + "a {\n color: is-superselector(\"::c(@#$)\", \"::c(*&^)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_element_and_element_different_no_arg, + "a {\n color: is-superselector(\"::c(@#$)\", \"::c\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + simple_pseudo_arg_element_and_class, + "a {\n color: is-superselector(\"::c(@#$)\", \":c(@#$)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_any_superset, + "a {\n color: is-superselector(\":any(c d, e f, g h)\", \":any(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_any_subset, + "a {\n color: is-superselector(\":any(c d.i, e j f)\", \":any(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_any_prefix_superset, + "a {\n color: is-superselector(\":-pfx-any(c d, e f, g h)\", \":-pfx-any(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_any_prefix_subset, + "a {\n color: is-superselector(\":-pfx-any(c d.i, e j f)\", \":-pfx-any(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_superset, + "a {\n color: is-superselector(\":current(c d, e f, g h)\", \":current(c d.i, e j f)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_subset, + "a {\n color: is-superselector(\":current(c d.i, e j f)\", \":current(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_equal, + "a {\n color: is-superselector(\":current(c d, e f)\", \":current(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_current_bare_sub, + "a {\n color: is-superselector(\":current(c d, e f)\", \"c d, e f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_prefix_superset, + "a {\n color: is-superselector(\":-pfx-current(c d, e f, g h)\", \":-pfx-current(c d.i, e j f)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_prefix_subset, + "a {\n color: is-superselector(\":-pfx-current(c d.i, e j f)\", \":-pfx-current(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_current_prefix_equal, + "a {\n color: is-superselector(\":-pfx-current(c d, e f)\", \":-pfx-current(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_has_superset, + "a {\n color: is-superselector(\":has(c d, e f, g h)\", \":has(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_has_subset, + "a {\n color: is-superselector(\":has(c d.i, e j f)\", \":has(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_has_equal, + "a {\n color: is-superselector(\":has(c d, e f)\", \":has(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_has_bare_sub, + "a {\n color: is-superselector(\":has(c d, e f)\", \"c d, e f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_has_prefix_superset, + "a {\n color: is-superselector(\":-pfx-has(c d, e f, g h)\", \":-pfx-has(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_has_prefix_subset, + "a {\n color: is-superselector(\":-pfx-has(c d.i, e j f)\", \":-pfx-has(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_has_prefix_equal, + "a {\n color: is-superselector(\":-pfx-has(c d, e f)\", \":-pfx-has(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_superset, + "a {\n color: is-superselector(\":host(c d, e f, g h)\", \":host(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_subset, + "a {\n color: is-superselector(\":host(c d.i, e j f)\", \":host(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_equal, + "a {\n color: is-superselector(\":host(c d, e f)\", \":host(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_bare_sub, + "a {\n color: is-superselector(\":host(c d, e f)\", \"c d, e f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_prefix_superset, + "a {\n color: is-superselector(\":-pfx-host(c d, e f, g h)\", \":-pfx-host(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_prefix_subset, + "a {\n color: is-superselector(\":-pfx-host(c d.i, e j f)\", \":-pfx-host(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_prefix_equal, + "a {\n color: is-superselector(\":-pfx-host(c d, e f)\", \":-pfx-host(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_context_superset, + "a {\n color: is-superselector(\":host-context(c d, e f, g h)\", \":host-context(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_context_subset, + "a {\n color: is-superselector(\":host-context(c d.i, e j f)\", \":host-context(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_context_equal, + "a {\n color: is-superselector(\":host-context(c d, e f)\", \":host-context(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_context_bare_sub, + "a {\n color: is-superselector(\":host-context(c d, e f)\", \"c d, e f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_context_prefix_superset, + "a {\n color: is-superselector(\":-pfx-host-context(c d, e f, g h)\", \":-pfx-host-context(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_host_context_prefix_subset, + "a {\n color: is-superselector(\":-pfx-host-context(c d.i, e j f)\", \":-pfx-host-context(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_host_context_prefix_equal, + "a {\n color: is-superselector(\":-pfx-host-context(c d, e f)\", \":-pfx-host-context(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_slotted_superset, + "a {\n color: is-superselector(\"::slotted(c d, e f, g h)\", \"::slotted(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_slotted_subset, + "a {\n color: is-superselector(\"::slotted(c d.i, e j f)\", \"::slotted(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_slotted_equal, + "a {\n color: is-superselector(\"::slotted(c d, e f)\", \"::slotted(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_slotted_bare_sub, + "a {\n color: is-superselector(\"::slotted(c d, e f)\", \"c d, e f\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_slotted_prefix_superset, + "a {\n color: is-superselector(\"::-pfx-slotted(c d, e f, g h)\", \"::-pfx-slotted(c d.i, e j f)\");\n}\n", + "a {\n color: true;\n}\n" +); +test!( + psuedo_slotted_prefix_subset, + "a {\n color: is-superselector(\"::-pfx-slotted(c d.i, e j f)\", \"::-pfx-slotted(c d, e f, g h)\");\n}\n", + "a {\n color: false;\n}\n" +); +test!( + psuedo_slotted_prefix_equal, + "a {\n color: is-superselector(\"::-pfx-slotted(c d, e f)\", \"::-pfx-slotted(c d, e f)\");\n}\n", + "a {\n color: true;\n}\n" +); + +// todo: /spec/core_functions/selector/is_superselector/simple/pseudo/selector_arg/ +// :not, :matches, :nth-child, :nth-last-child diff --git a/tests/selector-append.rs b/tests/selector-append.rs new file mode 100644 index 0000000..28feda7 --- /dev/null +++ b/tests/selector-append.rs @@ -0,0 +1,78 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + classes_single, + "a {\n color: selector-append(\".c\", \".d\");\n}\n", + "a {\n color: .c.d;\n}\n" +); +test!( + classes_multiple, + "a {\n color: selector-append(\".c, .d\", \".e, .f\");\n}\n", + "a {\n color: .c.e, .c.f, .d.e, .d.f;\n}\n" +); +test!( + suffix_single, + "a {\n color: selector-append(\".c\", \"d\");\n}\n", + "a {\n color: .cd;\n}\n" +); +test!( + suffix_multiple, + "a {\n color: selector-append(\".c, .d\", \"e, f\");\n}\n", + "a {\n color: .ce, .cf, .de, .df;\n}\n" +); +test!( + suffix_descendant, + "a {\n color: selector-append(\"c d\", \"e f\");\n}\n", + "a {\n color: c de f;\n}\n" +); +test!( + one_arg, + "a {\n color: selector-append(\".c.d\");\n}\n", + "a {\n color: .c.d;\n}\n" +); +test!( + many_args, + "a {\n color: selector-append(\".c\", \".d\", \".e\");\n}\n", + "a {\n color: .c.d.e;\n}\n" +); +test!( + paren_first_arg, + "a {\n color: selector-append((c, d e), f);\n}\n", + "a {\n color: cf, d ef;\n}\n" +); +test!( + paren_second_arg, + "a {\n color: selector-append(c, (d, e f));\n}\n", + "a {\n color: cd, ce f;\n}\n" +); +test!( + output_structure, + "a {\n color: selector-append(\"c d, e f\", \"g\") == (\"c\" \"dg\", \"e\" \"fg\");\n}\n", + "a {\n color: true;\n}\n" +); +error!( + universal_in_second_arg, + "a {\n color: selector-append(\".c\", \"*\");\n}\n", "Error: Can't append * to .c." +); +error!( + parent_in_second_arg, + "a {\n color: selector-append(\"c\", \"&\");\n}\n", + "Error: Parent selectors aren't allowed here." +); +error!( + malformed_selector_in_first_arg, + "a {\n color: selector-append(\"[c\", \".d\");\n}\n", "Error: expected more input." +); +error!( + invalid_type_in_first_arg, + "a {\n color: selector-append(\"c\", 1);\n}\n", + "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." +); +error!( + no_args, + "a {\n color: selector-append();\n}\n", + "Error: $selectors: At least one selector must be passed." +); diff --git a/tests/selector-extend.rs b/tests/selector-extend.rs new file mode 100644 index 0000000..8e795a9 --- /dev/null +++ b/tests/selector-extend.rs @@ -0,0 +1,236 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + simple_attribute_equal, + "a {\n color: selector-extend(\"[c=d]\", \"[c=d]\", \"e\");\n}\n", + "a {\n color: [c=d], e;\n}\n" +); +test!( + simple_attribute_unequal_name, + "a {\n color: selector-extend(\"[c=d]\", \"[e=d]\", \"f\");\n}\n", + "a {\n color: [c=d];\n}\n" +); +test!( + simple_attribute_unequal_value, + "a {\n color: selector-extend(\"[c=d]\", \"[c=e]\", \"f\");\n}\n", + "a {\n color: [c=d];\n}\n" +); +test!( + simple_attribute_unequal_operator, + "a {\n color: selector-extend(\"[c=d]\", \"[c^=e]\", \"f\");\n}\n", + "a {\n color: [c=d];\n}\n" +); +test!( + simple_class_equal, + "a {\n color: selector-extend(\".c\", \".c\", \"e\");\n}\n", + "a {\n color: .c, e;\n}\n" +); +test!( + simple_class_unequal, + "a {\n color: selector-extend(\".c\", \".d\", \"e\");\n}\n", + "a {\n color: .c;\n}\n" +); +test!( + simple_id_equal, + "a {\n color: selector-extend(\"#c\", \"#c\", \"e\");\n}\n", + "a {\n color: #c, e;\n}\n" +); +test!( + simple_id_unequal, + "a {\n color: selector-extend(\"#c\", \"#d\", \"e\");\n}\n", + "a {\n color: #c;\n}\n" +); +test!( + simple_placeholder_equal, + "a {\n color: selector-extend(\"%c\", \"%c\", \"e\");\n}\n", + "a {\n color: %c, e;\n}\n" +); +test!( + simple_placeholder_unequal, + "a {\n color: selector-extend(\"%c\", \"%d\", \"e\");\n}\n", + "a {\n color: %c;\n}\n" +); +test!( + simple_type_equal, + "a {\n color: selector-extend(\"c\", \"c\", \"e\");\n}\n", + "a {\n color: c, e;\n}\n" +); +test!( + simple_type_unequal, + "a {\n color: selector-extend(\"c\", \"d\", \"e\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + simple_type_and_universal, + "a {\n color: selector-extend(\"c\", \"*\", \"d\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + simple_type_explicit_namespace_and_type_explicit_namespace_equal, + "a {\n color: selector-extend(\"c|d\", \"c|d\", \"e\");\n}\n", + "a {\n color: c|d, e;\n}\n" +); +test!( + simple_type_explicit_namespace_and_type_implicit_namespace, + "a {\n color: selector-extend(\"c|d\", \"d\", \"e\");\n}\n", + "a {\n color: c|d;\n}\n" +); +test!( + simple_type_explicit_namespace_and_type_empty_namespace, + "a {\n color: selector-extend(\"c|d\", \"|d\", \"e\");\n}\n", + "a {\n color: c|d;\n}\n" +); +test!( + simple_type_explicit_namespace_and_type_universal_namespace, + "a {\n color: selector-extend(\"c|d\", \"*|d\", \"e\");\n}\n", + "a {\n color: c|d;\n}\n" +); +test!( + simple_type_empty_namespace_and_type_explicit_namespace_equal, + "a {\n color: selector-extend(\"|c\", \"d|c\", \"e\");\n}\n", + "a {\n color: |c;\n}\n" +); +test!( + simple_type_empty_namespace_and_type_implicit_namespace, + "a {\n color: selector-extend(\"|c\", \"c\", \"d\");\n}\n", + "a {\n color: |c;\n}\n" +); +test!( + simple_type_empty_namespace_and_type_empty_namespace, + "a {\n color: selector-extend(\"|c\", \"|c\", \"d\");\n}\n", + "a {\n color: |c, d;\n}\n" +); +test!( + simple_type_empty_namespace_and_type_universal_namespace, + "a {\n color: selector-extend(\"|c\", \"*|c\", \"d\");\n}\n", + "a {\n color: |c;\n}\n" +); +test!( + simple_type_universal_namespace_and_type_explicit_namespace_equal, + "a {\n color: selector-extend(\"*|c\", \"d|c\", \"d\");\n}\n", + "a {\n color: *|c;\n}\n" +); +test!( + simple_type_universal_namespace_and_type_implicit_namespace, + "a {\n color: selector-extend(\"*|c\", \"c\", \"d\");\n}\n", + "a {\n color: *|c;\n}\n" +); +test!( + simple_type_universal_namespace_and_type_empty_namespace, + "a {\n color: selector-extend(\"*|c\", \"|c\", \"d\");\n}\n", + "a {\n color: *|c;\n}\n" +); +test!( + simple_type_universal_namespace_and_type_universal_namespace, + "a {\n color: selector-extend(\"*|c\", \"*|c\", \"d\");\n}\n", + "a {\n color: *|c, d;\n}\n" +); +test!( + simple_pseudo_class_no_arg_equal, + "a {\n color: selector-extend(\":c\", \":c\", \"e\");\n}\n", + "a {\n color: :c, e;\n}\n" +); +test!( + simple_pseudo_class_no_arg_unequal, + "a {\n color: selector-extend(\":c\", \":d\", \"e\");\n}\n", + "a {\n color: :c;\n}\n" +); +test!( + simple_pseudo_class_no_arg_and_element, + "a {\n color: selector-extend(\":c\", \"::c\", \"e\");\n}\n", + "a {\n color: :c;\n}\n" +); +test!( + simple_pseudo_element_no_arg_and_element_equal, + "a {\n color: selector-extend(\"::c\", \"::c\", \"e\");\n}\n", + "a {\n color: ::c, e;\n}\n" +); +test!( + simple_pseudo_element_no_arg_and_class, + "a {\n color: selector-extend(\"::c\", \":c\", \"e\");\n}\n", + "a {\n color: ::c;\n}\n" +); +test!( + simple_pseudo_class_arg_equal, + "a {\n color: selector-extend(\":c(@#$)\", \":c(@#$)\", \"e\");\n}\n", + "a {\n color: :c(@#$), e;\n}\n" +); +test!( + simple_pseudo_class_arg_unequal_name, + "a {\n color: selector-extend(\":c(@#$)\", \":d(@#$)\", \"e\");\n}\n", + "a {\n color: :c(@#$);\n}\n" +); +test!( + simple_pseudo_class_arg_unequal_arg, + "a {\n color: selector-extend(\":c(@#$)\", \":c(*&^)\", \"e\");\n}\n", + "a {\n color: :c(@#$);\n}\n" +); +test!( + simple_pseudo_class_arg_unequal_no_arg, + "a {\n color: selector-extend(\":c(@#$)\", \":c\", \"e\");\n}\n", + "a {\n color: :c(@#$);\n}\n" +); +test!( + simple_pseudo_class_arg_and_element, + "a {\n color: selector-extend(\":c(@#$)\", \"::c(@#$)\", \"e\");\n}\n", + "a {\n color: :c(@#$);\n}\n" +); +test!( + simple_pseudo_element_arg_and_element_equal, + "a {\n color: selector-extend(\"::c(@#$)\", \"::c(@#$)\", \"e\");\n}\n", + "a {\n color: ::c(@#$), e;\n}\n" +); +test!( + simple_pseudo_element_arg_and_class, + "a {\n color: selector-extend(\"::c(@#$)\", \":c(@#$)\", \"e\");\n}\n", + "a {\n color: ::c(@#$);\n}\n" +); +test!( + complex_parent_without_grandparents_simple, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e\");\n}\n", + "a {\n color: .c .d, .e .d;\n}\n" +); +test!( + complex_parent_without_grandparents_complex, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e .f\");\n}\n", + "a {\n color: .c .d, .e .f .d;\n}\n" +); +test!( + complex_parent_without_grandparents_list, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e, .f\");\n}\n", + "a {\n color: .c .d, .e .d, .f .d;\n}\n" +); +test!( + complex_parent_with_grandparents_simple, + "a {\n color: selector-extend(\".c .d .e\", \".d\", \".f\");\n}\n", + "a {\n color: .c .d .e, .c .f .e;\n}\n" +); +test!( + complex_parent_with_grandparents_complex, + "a {\n color: selector-extend(\".c .d .e\", \".d\", \".f .g\");\n}\n", + "a {\n color: .c .d .e, .c .f .g .e, .f .c .g .e;\n}\n" +); +test!( + complex_parent_with_grandparents_list, + "a {\n color: selector-extend(\".c .d .e\", \".d\", \".f, .g\");\n}\n", + "a {\n color: .c .d .e, .c .f .e, .c .g .e;\n}\n" +); +test!( + complex_trailing_combinator_child, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e >\");\n}\n", + "a {\n color: .c .d, .e > .d;\n}\n" +); +test!( + complex_trailing_combinator_sibling, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e ~\");\n}\n", + "a {\n color: .c .d, .e ~ .d;\n}\n" +); +test!( + complex_trailing_combinator_next_sibling, + "a {\n color: selector-extend(\".c .d\", \".c\", \".e +\");\n}\n", + "a {\n color: .c .d, .e + .d;\n}\n" +); +// todo: https://github.com/sass/sass-spec/tree/master/spec/core_functions/selector/extend/simple/pseudo/selector/ diff --git a/tests/selector-nest.rs b/tests/selector-nest.rs new file mode 100644 index 0000000..d7dde1c --- /dev/null +++ b/tests/selector-nest.rs @@ -0,0 +1,158 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + nest_one_arg, + "a {\n color: selector-nest(\"c\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + nest_many_args, + "a {\n color: selector-nest(\"c\", \"d\", \"e\", \"f\", \"g\");\n}\n", + "a {\n color: c d e f g;\n}\n" +); +test!( + nest_parent_alone, + "a {\n color: selector-nest(\"c\", \"&\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + nest_parent_compound, + "a {\n color: selector-nest(\"c\", \"&.d\");\n}\n", + "a {\n color: c.d;\n}\n" +); +test!( + nest_parent_with_suffix, + "a {\n color: selector-nest(\"c\", \"&d\");\n}\n", + "a {\n color: cd;\n}\n" +); +test!( + nest_complex_parent_compound, + "a {\n color: selector-nest(\"c\", \"d &.e\");\n}\n", + "a {\n color: d c.e;\n}\n" +); +test!( + nest_complex_super_parent_compound, + "a {\n color: selector-nest(\"c d\", \"e &.f\");\n}\n", + "a {\n color: e c d.f;\n}\n" +); +test!( + nest_parent_in_special_pseudo, + "a {\n color: selector-nest(\"c\", \":matches(&)\");\n}\n", + "a {\n color: :matches(c);\n}\n" +); +test!( + nest_complex_super_parent_in_special_pseudo, + "a {\n color: selector-nest(\"c d\", \":matches(&)\");\n}\n", + "a {\n color: :matches(c d);\n}\n" +); +test!( + nest_multiple_parent, + "a {\n color: selector-nest(\"c\", \"&.d &.e\");\n}\n", + "a {\n color: c.d c.e;\n}\n" +); +test!( + nest_compound_parent_in_list, + "a {\n color: selector-nest(\"c\", \"&.d, e\");\n}\n", + "a {\n color: c.d, c e;\n}\n" +); +test!( + nest_list_super, + "a {\n color: selector-nest(\"c, d\", \"e\");\n}\n", + "a {\n color: c e, d e;\n}\n" +); +test!( + nest_list_sub, + "a {\n color: selector-nest(\"c\", \"d, e\");\n}\n", + "a {\n color: c d, c e;\n}\n" +); +test!( + nest_three_args_list, + "a {\n color: selector-nest(\"c, d\", \"e, f\", \"g, h\");\n}\n", + "a {\n color: c e g, c e h, c f g, c f h, d e g, d e h, d f g, d f h;\n}\n" +); +test!( + nest_super_list_parent_alone, + "a {\n color: selector-nest(\"c, d\", \"&\");\n}\n", + "a {\n color: c, d;\n}\n" +); +test!( + nest_super_list_parent_compound, + "a {\n color: selector-nest(\"c, d\", \"&.e\");\n}\n", + "a {\n color: c.e, d.e;\n}\n" +); +test!( + nest_super_list_parent_suffix, + "a {\n color: selector-nest(\"c, d\", \"&e\");\n}\n", + "a {\n color: ce, de;\n}\n" +); +test!( + nest_super_list_parent_complex, + "a {\n color: selector-nest(\"c, d\", \"e &.f\");\n}\n", + "a {\n color: e c.f, e d.f;\n}\n" +); +test!( + nest_super_list_parent_inside_pseudo, + "a {\n color: selector-nest(\"c, d\", \":matches(&)\");\n}\n", + "a {\n color: :matches(c, d);\n}\n" +); +test!( + nest_super_list_multiple_parent, + "a {\n color: selector-nest(\"c, d\", \"&.e &.f\");\n}\n", + "a {\n color: c.e c.f, c.e d.f, d.e c.f, d.e d.f;\n}\n" +); +test!( + nest_super_list_sub_list_contains_parent, + "a {\n color: selector-nest(\"c, d\", \"&.e, f\");\n}\n", + "a {\n color: c.e, c f, d.e, d f;\n}\n" +); +test!( + nest_comma_separated_list_as_super, + "a {\n color: selector-nest((c, d e), \"f\");\n}\n", + "a {\n color: c f, d e f;\n}\n" +); +test!( + nest_comma_separated_list_as_sub, + "a {\n color: selector-nest(\"c\", (d, e f));\n}\n", + "a {\n color: c d, c e f;\n}\n" +); +error!( + #[ignore = "https://github.com/sass/dart-sass/issues/966"] + disallows_parent_selector_as_first_arg, + "a {\n color: selector-nest(\"&\");\n}\n", "Error: Parent selectors aren't allowed here." +); +error!( + disallows_parent_not_at_start_of_compound_selector_attribute, + "a {\n color: selector-nest(\"[d]&\");\n}\n", + "Error: \"&\" may only used at the beginning of a compound selector." +); +error!( + disallows_parent_not_at_start_of_compound_selector_type, + "a {\n color: selector-nest(\"d&\");\n}\n", + "Error: \"&\" may only used at the beginning of a compound selector." +); +error!( + improperly_terminated_attribute_selector_first_arg, + "a {\n color: selector-nest(\"[d\");\n}\n", "Error: expected more input." +); +error!( + improperly_terminated_attribute_selector_second_arg, + "a {\n color: selector-nest(\"c\", \"[d\");\n}\n", "Error: expected more input." +); +error!( + unquoted_integer_first_arg, + "a {\n color: selector-nest(1);\n}\n", + "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." +); +error!( + unquoted_integer_second_arg, + "a {\n color: selector-nest(\"c\", 1);\n}\n", + "Error: $selectors: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." +); +error!( + empty_args, + "a {\n color: selector-nest();\n}\n", + "Error: $selectors: At least one selector must be passed." +); diff --git a/tests/selector-parse.rs b/tests/selector-parse.rs new file mode 100644 index 0000000..12a0568 --- /dev/null +++ b/tests/selector-parse.rs @@ -0,0 +1,100 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + named_args, + "a {\n color: selector-parse($selector: \"c\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + simple_class, + "a {\n color: selector-parse(\".c\");\n}\n", + "a {\n color: .c;\n}\n" +); +test!( + simple_id, + "a {\n color: selector-parse(\"#c\");\n}\n", + "a {\n color: #c;\n}\n" +); +test!( + simple_placeholder, + "a {\n color: selector-parse(\"%c\");\n}\n", + "a {\n color: %c;\n}\n" +); +test!( + simple_attribute, + "a {\n color: selector-parse(\"[c^=d]\");\n}\n", + "a {\n color: [c^=d];\n}\n" +); +test!( + simple_universal, + "a {\n color: selector-parse(\"*\");\n}\n", + "a {\n color: *;\n}\n" +); +test!( + simple_pseudo, + "a {\n color: selector-parse(\":c\");\n}\n", + "a {\n color: :c;\n}\n" +); +test!( + pseudo_weird_args, + "a {\n color: selector-parse(\":c(@#$)\");\n}\n", + "a {\n color: :c(@#$);\n}\n" +); +test!( + pseudo_matches_with_list_args, + "a {\n color: selector-parse(\":matches(b, c)\");\n}\n", + "a {\n color: :matches(b, c);\n}\n" +); +test!( + pseudo_element, + "a {\n color: selector-parse(\"::c\");\n}\n", + "a {\n color: ::c;\n}\n" +); +test!( + pseudo_element_args, + "a {\n color: selector-parse(\"::c(@#$)\");\n}\n", + "a {\n color: ::c(@#$);\n}\n" +); +test!( + pseudo_element_slotted_list_args_output, + "a {\n color: selector-parse(\"::slotted(b, c)\");\n}\n", + "a {\n color: ::slotted(b, c);\n}\n" +); +test!( + pseudo_element_slotted_list_args_structure, + "a {\n color: selector-parse(\"::slotted(b, c)\") == (append((), \"::slotted(b, c)\"),);\n}\n", + "a {\n color: true;\n}\n" +); +test!( + multiple_compound, + "a {\n color: selector-parse(\"b.c:d\");\n}\n", + "a {\n color: b.c:d;\n}\n" +); +test!( + multiple_complex, + "a {\n color: selector-parse(\"b c d\");\n}\n", + "a {\n color: b c d;\n}\n" +); +test!( + sibling_combinator, + "a {\n color: selector-parse(\"b ~ c ~ d\");\n}\n", + "a {\n color: b ~ c ~ d;\n}\n" +); +test!( + adjacent_combinator, + "a {\n color: selector-parse(\"b + c + d\");\n}\n", + "a {\n color: b + c + d;\n}\n" +); +test!( + child_combinator, + "a {\n color: selector-parse(\"b > c > d\");\n}\n", + "a {\n color: b > c > d;\n}\n" +); +test!( + comma_and_space_list, + "a {\n color: selector-parse(\"b c, d e, f g\");\n}\n", + "a {\n color: b c, d e, f g;\n}\n" +); diff --git a/tests/selector-replace.rs b/tests/selector-replace.rs new file mode 100644 index 0000000..853f2bd --- /dev/null +++ b/tests/selector-replace.rs @@ -0,0 +1,40 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + simple, + "a {\n color: selector-replace(\"c\", \"c\", \"d\");\n}\n", + "a {\n color: d;\n}\n" +); +test!( + compound, + "a {\n color: selector-replace(\"c.d\", \"c\", \"e\");\n}\n", + "a {\n color: e.d;\n}\n" +); +test!( + complex, + "a {\n color: selector-replace(\"c d\", \"d\", \"e f\");\n}\n", + "a {\n color: c e f, e c f;\n}\n" +); +test!( + psuedo_matches, + "a {\n color: selector-replace(\":matches(c)\", \"c\", \"d\");\n}\n", + "a {\n color: :matches(d);\n}\n" +); +test!( + psuedo_not, + "a {\n color: selector-replace(\":not(c)\", \"c\", \"d\");\n}\n", + "a {\n color: :not(d);\n}\n" +); +test!( + no_op, + "a {\n color: selector-replace(\"c\", \"d\", \"e\");\n}\n", + "a {\n color: c;\n}\n" +); +test!( + partial_no_op, + "a {\n color: selector-replace(\"c, d\", \"d\", \"e\");\n}\n", + "a {\n color: c, e;\n}\n" +); diff --git a/tests/selector-unify.rs b/tests/selector-unify.rs new file mode 100644 index 0000000..c4fdcc9 --- /dev/null +++ b/tests/selector-unify.rs @@ -0,0 +1,602 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + no_overlap, + "a {\n color: selector-unify(\".c.d\", \".e.f\");\n}\n", + "a {\n color: .c.d.e.f;\n}\n" +); +test!( + partial_overlap, + "a {\n color: selector-unify(\".c.d\", \".d.e\");\n}\n", + "a {\n color: .c.d.e;\n}\n" +); +test!( + full_overlap, + "a {\n color: selector-unify(\".c.d\", \".c.d\");\n}\n", + "a {\n color: .c.d;\n}\n" +); +test!( + order_element_at_start, + "a {\n color: selector-unify(\".c\", \"d\");\n}\n", + "a {\n color: d.c;\n}\n" +); +test!( + order_pseudo_element_at_end, + "a {\n color: selector-unify(\"::c\", \".d\");\n}\n", + "a {\n color: .d::c;\n}\n" +); +test!( + order_pseudo_class_at_end, + "a {\n color: selector-unify(\":c\", \".d\");\n}\n", + "a {\n color: .d:c;\n}\n" +); +test!( + order_pseudo_element_after_pseudo_class, + "a {\n color: selector-unify(\"::c\", \":d\");\n}\n", + "a {\n color: :d::c;\n}\n" +); +test!( + attribute_identical, + "a {\n color: selector-unify(\"[a]\", \"[a]\");\n}\n", + "a {\n color: [a];\n}\n" +); +test!( + attribute_distinct, + "a {\n color: selector-unify(\"[a]\", \"[b]\");\n}\n", + "a {\n color: [a][b];\n}\n" +); +test!( + class_identical, + "a {\n color: selector-unify(\".a\", \".a\");\n}\n", + "a {\n color: .a;\n}\n" +); +test!( + class_distinct, + "a {\n color: selector-unify(\".a\", \".b\");\n}\n", + "a {\n color: .a.b;\n}\n" +); +test!( + element_id, + "a {\n color: selector-unify(\"a\", \"#b\");\n}\n", + "a {\n color: a#b;\n}\n" +); +test!( + id_identical, + "a {\n color: selector-unify(\"#a\", \"#a\");\n}\n", + "a {\n color: #a;\n}\n" +); +test!( + id_distinct, + "a {\n color: selector-unify(\"#a\", \"#b\");\n}\n", + "" +); +test!( + placeholder_identical, + "a {\n color: selector-unify(\"%a\", \"%a\");\n}\n", + "a {\n color: %a;\n}\n" +); +test!( + placeholder_distinct, + "a {\n color: selector-unify(\"%a\", \"%b\");\n}\n", + "a {\n color: %a%b;\n}\n" +); +test!( + universal_and_namespace, + "a {\n color: selector-unify(\"*\", \"a|b\");\n}\n", + "" +); +test!( + universal_and_empty_namespace, + "a {\n color: selector-unify(\"*\", \"|b\");\n}\n", + "" +); +test!( + universal_and_type, + "a {\n color: selector-unify(\"*\", \"a\");\n}\n", + "a {\n color: a;\n}\n" +); +test!( + universal_and_asterisk_namespace, + "a {\n color: selector-unify(\"*\", \"*|a\");\n}\n", + "a {\n color: a;\n}\n" +); +test!( + universal_with_namespace_and_same_namespace, + "a {\n color: selector-unify(\"a|*\", \"a|b\");\n}\n", + "a {\n color: a|b;\n}\n" +); +test!( + universal_with_namespace_and_different_namespace, + "a {\n color: selector-unify(\"a|*\", \"c|b\");\n}\n", + "" +); +test!( + universal_with_namespace_and_no_namespace, + "a {\n color: selector-unify(\"a|*\", \"|b\");\n}\n", + "" +); +test!( + universal_with_namespace_and_asterisk_namespace, + "a {\n color: selector-unify(\"a|*\", \"*|b\");\n}\n", + "a {\n color: a|b;\n}\n" +); +test!( + universal_with_empty_namespace_and_empty_namespace, + "a {\n color: selector-unify(\"|*\", \"|b\");\n}\n", + "a {\n color: |b;\n}\n" +); +test!( + universal_with_empty_namespace_and_no_namespace, + "a {\n color: selector-unify(\"|*\", \"b\");\n}\n", + "" +); +test!( + universal_with_empty_namespace_and_asterisk_namespace, + "a {\n color: selector-unify(\"|*\", \"*|b\");\n}\n", + "a {\n color: |b;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_namespace, + "a {\n color: selector-unify(\"*|*\", \"a|b\");\n}\n", + "a {\n color: a|b;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_empty_namespace, + "a {\n color: selector-unify(\"*|*\", \"|b\");\n}\n", + "a {\n color: |b;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_no_namespace, + "a {\n color: selector-unify(\"*|*\", \"b\");\n}\n", + "a {\n color: b;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_asterisk_namespace, + "a {\n color: selector-unify(\"*|*\", \"*|b\");\n}\n", + "a {\n color: *|b;\n}\n" +); +test!( + universal_with_no_namespace_and_namespace_on_universal, + "a {\n color: selector-unify(\"*\", \"a|*\");\n}\n", + "" +); +test!( + universal_with_no_namespace_and_empty_namespace_on_universal, + "a {\n color: selector-unify(\"*\", \"|*\");\n}\n", + "" +); +test!( + universal_with_no_namespace_and_universal_with_no_namespace, + "a {\n color: selector-unify(\"*\", \"*\");\n}\n", + "a {\n color: *;\n}\n" +); +test!( + universal_with_no_namespace_and_universal_with_asterisk_namespace, + "a {\n color: selector-unify(\"*\", \"*|*\");\n}\n", + "a {\n color: *;\n}\n" +); +test!( + universal_with_namespace_and_universal_with_namespace, + "a {\n color: selector-unify(\"c|*\", \"c|*\");\n}\n", + "a {\n color: c|*;\n}\n" +); +test!( + universal_with_namespace_and_universal_with_empty_namespace, + "a {\n color: selector-unify(\"c|*\", \"|*\");\n}\n", + "" +); +test!( + universal_with_namespace_and_universal_with_no_namespace, + "a {\n color: selector-unify(\"c|*\", \"*\");\n}\n", + "" +); +test!( + universal_with_namespace_and_universal_with_asterisk_namespace, + "a {\n color: selector-unify(\"c|*\", \"*|*\");\n}\n", + "a {\n color: c|*;\n}\n" +); +test!( + universal_with_empty_namespace_and_universal_with_namespace, + "a {\n color: selector-unify(\"|*\", \"b|*\");\n}\n", + "" +); +test!( + universal_with_empty_namespace_and_universal_with_empty_namespace, + "a {\n color: selector-unify(\"|*\", \"|*\");\n}\n", + "a {\n color: |*;\n}\n" +); +test!( + universal_with_empty_namespace_and_universal_with_no_namespace, + "a {\n color: selector-unify(\"|*\", \"*\");\n}\n", + "" +); +test!( + universal_with_empty_namespace_and_universal_with_asterisk_namespace, + "a {\n color: selector-unify(\"|*\", \"*|*\");\n}\n", + "a {\n color: |*;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_universal_with_namespace, + "a {\n color: selector-unify(\"*|*\", \"a|*\");\n}\n", + "a {\n color: a|*;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_universal_with_empty_namespace, + "a {\n color: selector-unify(\"*|*\", \"|*\");\n}\n", + "a {\n color: |*;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_universal_with_asterisk_namespace, + "a {\n color: selector-unify(\"*|*\", \"*|*\");\n}\n", + "a {\n color: *|*;\n}\n" +); +test!( + universal_with_asterisk_namespace_and_universal_with_no_namespace, + "a {\n color: selector-unify(\"*|*\", \"*\");\n}\n", + "a {\n color: *;\n}\n" +); +test!( + complex_two_levels_same_first, + "a {\n color: selector-unify(\".c .s1\", \".c .s2\");\n}\n", + "a {\n color: .c .s1.s2;\n}\n" +); +test!( + complex_three_levels_same_first, + "a {\n color: selector-unify(\".c .s1-1 .s1-2\", \".c .s2-1 .s2-2\");\n}\n", + "a {\n color: .c .s1-1 .s2-1 .s1-2.s2-2, .c .s2-1 .s1-1 .s1-2.s2-2;\n}\n" +); +test!( + complex_three_levels_same_second, + "a {\n color: selector-unify(\".s1-1 .d .s1-2\", \".s2-1 .d .s2-2\");\n}\n", + "a {\n color: .s1-1 .s2-1 .d .s1-2.s2-2, .s2-1 .s1-1 .d .s1-2.s2-2;\n}\n" +); +test!( + second_is_super_selector, + "a {\n color: selector-unify(\"c\", \"d c.e\");\n}\n", + "a {\n color: d c.e;\n}\n" +); +test!( + first_is_super_selector, + "a {\n color: selector-unify(\"d c.e\", \"c\");\n}\n", + "a {\n color: d c.e;\n}\n" +); +test!( + second_parent_is_super_selector, + "a {\n color: selector-unify(\"c d\", \"c.e .f\");\n}\n", + "a {\n color: c.e d.f;\n}\n" +); +test!( + first_parent_is_super_selector, + "a {\n color: selector-unify(\"c.e .f\", \"c d\");\n}\n", + "a {\n color: c.e d.f;\n}\n" +); +test!( + two_level_distinct, + "a {\n color: selector-unify(\".c .d\", \".e .f\");\n}\n", + "a {\n color: .c .e .d.f, .e .c .d.f;\n}\n" +); +test!( + three_level_distinct, + "a {\n color: selector-unify(\".c .d .e\", \".f .g .h\");\n}\n", + "a {\n color: .c .d .f .g .e.h, .f .g .c .d .e.h;\n}\n" +); +test!( + two_level_super_selector, + "a {\n color: selector-unify(\".c.s1-1 .s1-2\", \".c .s2\");\n}\n", + "a {\n color: .c.s1-1 .s1-2.s2;\n}\n" +); +test!( + three_level_outer_super_selector, + "a {\n color: selector-unify(\".c.s1-1 .s1-2 .s1-3\", \".c .s2-1 .s2-2\");\n}\n", + "a {\n color: .c.s1-1 .s1-2 .s2-1 .s1-3.s2-2, .c.s1-1 .s2-1 .s1-2 .s1-3.s2-2;\n}\n" +); +test!( + three_level_inner_super_selector, + "a {\n color: selector-unify(\".s1-1 .c.s1-2 .s1-3\", \".s2-1 .c .s2-2\");\n}\n", + "a {\n color: .s1-1 .s2-1 .c.s1-2 .s1-3.s2-2, .s2-1 .s1-1 .c.s1-2 .s1-3.s2-2;\n}\n" +); +test!( + combinator_child_and_descendant_distinct, + "a {\n color: selector-unify(\".c > .d\", \".e .f\");\n}\n", + "a {\n color: .e .c > .d.f;\n}\n" +); +test!( + combinator_child_and_descendant_same, + "a {\n color: selector-unify(\".c > .s1\", \".c .s2\");\n}\n", + "a {\n color: .c > .s1.s2;\n}\n" +); +test!( + combinator_child_and_descendant_super_selector, + "a {\n color: selector-unify(\".c.s1-1 > .s1-2\", \".c .s2\");\n}\n", + "a {\n color: .c.s1-1 > .s1-2.s2;\n}\n" +); +test!( + combinator_child_and_descendant_overlap, + "a {\n color: selector-unify(\".c.s1-1 > .s1-2\", \".c.s2-1 .s2-2\");\n}\n", + "a {\n color: .c.s2-1 .c.s1-1 > .s1-2.s2-2;\n}\n" +); +test!( + combinator_child_and_child_distinct, + "a {\n color: selector-unify(\".c > .d\", \".e > .f\");\n}\n", + "a {\n color: .e.c > .d.f;\n}\n" +); +test!( + combinator_child_and_child_super_selector, + "a {\n color: selector-unify(\".c.s1-1 > .s1-2\", \".c > .s2\");\n}\n", + "a {\n color: .c.s1-1 > .s1-2.s2;\n}\n" +); +test!( + combinator_child_and_child_overlap, + "a {\n color: selector-unify(\".c.s1-1 > .s1-2\", \".c.s2-1 > .s2-2\");\n}\n", + "a {\n color: .c.s2-1.s1-1 > .s1-2.s2-2;\n}\n" +); +test!( + combinator_child_and_child_conflict, + "a {\n color: selector-unify(\"#s1-1 > .s1-2\", \"#s2-1 > .s2-2\");\n}\n", + "" +); +test!( + combinator_child_and_sibling, + "a {\n color: selector-unify(\".c > .s1\", \".c ~ .s2\");\n}\n", + "a {\n color: .c > .c ~ .s1.s2;\n}\n" +); +test!( + combinator_child_and_next_sibling, + "a {\n color: selector-unify(\".c > .s1\", \".c + .s2\");\n}\n", + "a {\n color: .c > .c + .s1.s2;\n}\n" +); +test!( + combinator_sibling_and_descendant, + "a {\n color: selector-unify(\".c ~ .s1\", \".c .s2\");\n}\n", + "a {\n color: .c .c ~ .s1.s2;\n}\n" +); +test!( + combinator_sibling_and_child, + "a {\n color: selector-unify(\".c ~ .s1\", \".c > .s2\");\n}\n", + "a {\n color: .c > .c ~ .s1.s2;\n}\n" +); +test!( + combinator_sibling_and_sibling_distinct, + "a {\n color: selector-unify(\".c ~ .d\", \".e ~ .f\");\n}\n", + "a {\n color: .c ~ .e ~ .d.f, .e ~ .c ~ .d.f, .e.c ~ .d.f;\n}\n" +); +test!( + combinator_sibling_and_sibling_same, + "a {\n color: selector-unify(\".c ~ .s1\", \".c ~ .s2\");\n}\n", + "a {\n color: .c ~ .s1.s2;\n}\n" +); +test!( + combinator_sibling_and_sibling_super_selector, + "a {\n color: selector-unify(\".c.s1-1 ~ .s1-2\", \".c ~ .s2\");\n}\n", + "a {\n color: .c.s1-1 ~ .s1-2.s2;\n}\n" +); +test!( + combinator_sibling_and_sibling_overlap, + "a {\n color: selector-unify(\".c.s1-1 ~ .s1-2\", \".c.s2-1 ~ .s2-2\");\n}\n", + "a {\n color: .c.s1-1 ~ .c.s2-1 ~ .s1-2.s2-2, .c.s2-1 ~ .c.s1-1 ~ .s1-2.s2-2, .c.s2-1.s1-1 ~ .s1-2.s2-2;\n}\n" +); +test!( + combinator_sibling_and_sibling_conflict, + "a {\n color: selector-unify(\"#s1-1 ~ .s1-2\", \"#s2-1 ~ .s2-2\");\n}\n", + "a {\n color: #s1-1 ~ #s2-1 ~ .s1-2.s2-2, #s2-1 ~ #s1-1 ~ .s1-2.s2-2;\n}\n" +); +test!( + combinator_sibling_and_next_sibling_distinct, + "a {\n color: selector-unify(\".c ~ .d\", \".e + .f\");\n}\n", + "a {\n color: .c ~ .e + .d.f, .e.c + .d.f;\n}\n" +); +test!( + combinator_sibling_and_next_sibling_identical, + "a {\n color: selector-unify(\".c ~ .s1\", \".c + .s2\");\n}\n", + "a {\n color: .c + .s1.s2;\n}\n" +); +test!( + combinator_sibling_and_next_sibling_super_selector, + "a {\n color: selector-unify(\".c.s1-1 ~ .s1-2\", \".c + .s2\");\n}\n", + "a {\n color: .c.s1-1 ~ .c + .s1-2.s2, .c.s1-1 + .s1-2.s2;\n}\n" +); +test!( + combinator_sibling_and_next_sibling_overlap, + "a {\n color: selector-unify(\".c.s1-1 ~ .s1-2\", \".c.s2-1 + .s2-2\");\n}\n", + "a {\n color: .c.s1-1 ~ .c.s2-1 + .s1-2.s2-2, .c.s2-1.s1-1 + .s1-2.s2-2;\n}\n" +); +test!( + combinator_sibling_and_next_sibling_conflict, + "a {\n color: selector-unify(\"#s1-1 ~ .s1-2\", \"#s2-1 + .s2-2\");\n}\n", + "a {\n color: #s1-1 ~ #s2-1 + .s1-2.s2-2;\n}\n" +); +test!( + combinator_next_sibling_and_descendant, + "a {\n color: selector-unify(\".c + .s1\", \".c .s2\");\n}\n", + "a {\n color: .c .c + .s1.s2;\n}\n" +); +test!( + combinator_next_sibling_and_sibling_distinct, + "a {\n color: selector-unify(\".c + .d\", \".e ~ .f\");\n}\n", + "a {\n color: .e ~ .c + .d.f, .e.c + .d.f;\n}\n" +); +test!( + combinator_next_sibling_and_sibling_identical, + "a {\n color: selector-unify(\".c + .s1\", \".c ~ .s2\");\n}\n", + "a {\n color: .c + .s1.s2;\n}\n" +); +test!( + combinator_next_sibling_and_sibling_super_selector, + "a {\n color: selector-unify(\".c.s1-1 + .s1-2\", \".c ~ .s2\");\n}\n", + "a {\n color: .c.s1-1 + .s1-2.s2;\n}\n" +); +test!( + combinator_next_sibling_and_sibling_overlap, + "a {\n color: selector-unify(\".c.s1-1 + .s1-2\", \".c.s2-1 ~ .s2-2\");\n}\n", + "a {\n color: .c.s2-1 ~ .c.s1-1 + .s1-2.s2-2, .c.s2-1.s1-1 + .s1-2.s2-2;\n}\n" +); +test!( + combinator_next_sibling_and_sibling_conflict, + "a {\n color: selector-unify(\"#s1-1 + .s1-2\", \"#s2-1 ~ .s2-2\");\n}\n", + "a {\n color: #s2-1 ~ #s1-1 + .s1-2.s2-2;\n}\n" +); +test!( + combinator_next_sibling_and_next_sibling_distinct, + "a {\n color: selector-unify(\".c + .d\", \".e + .f\");\n}\n", + "a {\n color: .e.c + .d.f;\n}\n" +); +test!( + combinator_next_sibling_and_next_sibling_super_selector, + "a {\n color: selector-unify(\".c.s1-1 + .s1-2\", \".c + .s2\");\n}\n", + "a {\n color: .c.s1-1 + .s1-2.s2;\n}\n" +); +test!( + combinator_next_sibling_and_next_sibling_overlap, + "a {\n color: selector-unify(\".c.s1-1 + .s1-2\", \".c.s2-1 + .s2-2\");\n}\n", + "a {\n color: .c.s2-1.s1-1 + .s1-2.s2-2;\n}\n" +); +test!( + combinator_next_sibling_and_next_sibling_conflict, + "a {\n color: selector-unify(\"#s1-1 + .s1-2\", \"#s2-1 + .s2-2\");\n}\n", + "" +); +test!( + combinator_at_start_first, + "a {\n color: selector-unify(\"> .c\", \".d\");\n}\n", + "a {\n color: > .c.d;\n}\n" +); +test!( + combinator_at_start_second, + "a {\n color: selector-unify(\".c\", \"~ .d\");\n}\n", + "a {\n color: ~ .c.d;\n}\n" +); +test!( + combinator_at_start_both_identical, + "a {\n color: selector-unify(\"+ .c\", \"+ .d\");\n}\n", + "a {\n color: + .c.d;\n}\n" +); +test!( + combinator_at_start_contiguous_super_sequence, + "a {\n color: selector-unify(\"+ ~ > .c\", \"> + ~ > > .d\");\n}\n", + "a {\n color: > + ~ > > .c.d;\n}\n" +); +test!( + combinator_at_start_non_contiguous_super_sequence, + "a {\n color: selector-unify(\"+ ~ > .c\", \"+ > ~ ~ > .d\");\n}\n", + "a {\n color: + > ~ ~ > .c.d;\n}\n" +); +test!( + combinator_at_start_distinct, + "a {\n color: selector-unify(\"+ ~ > .c\", \"+ > ~ ~ .d\");\n}\n", + "" +); +test!( + combinator_multiple, + "a {\n color: selector-unify(\".c > .d + .e\", \".f .g ~ .h\");\n}\n", + "a {\n color: .f .c > .g ~ .d + .e.h, .f .c > .g.d + .e.h;\n}\n" +); +test!( + combinator_multiple_in_a_row_same, + "a {\n color: selector-unify(\".c + ~ > .d\", \".e + ~ > .f\");\n}\n", + "a {\n color: .c .e + ~ > .d.f, .e .c + ~ > .d.f;\n}\n" +); +test!( + combinator_multiple_in_a_row_contiguous_super_sequence, + "a {\n color: selector-unify(\".c + ~ > .d\", \".e > + ~ > > .f\");\n}\n", + "a {\n color: .c .e > + ~ > > .d.f, .e .c > + ~ > > .d.f;\n}\n" +); +test!( + combinator_multiple_in_a_row_non_contiguous_super_sequence, + "a {\n color: selector-unify(\".c + ~ > .d\", \".e + > ~ ~ > .f\");\n}\n", + "a {\n color: .c .e + > ~ ~ > .d.f, .e .c + > ~ ~ > .d.f;\n}\n" +); +test!( + combinator_multiple_in_a_row_distinct, + "a {\n color: selector-unify(\".c + ~ > .d\", \".e + > ~ ~ .f\");\n}\n", + "" +); +test!( + lcs_two_vs_one, + "a {\n color: selector-unify(\".c .d .e .s1\", \".e .c .d .s2\");\n}\n", + "a {\n color: .e .c .d .e .s1.s2;\n}\n" +); +test!( + lcs_three_vs_two, + "a {\n color: selector-unify(\".c .d .e .f .g .s1\", \".f .g .c .d .e .s2\");\n}\n", + "a {\n color: .f .g .c .d .e .f .g .s1.s2;\n}\n" +); +test!( + lcs_non_contiguous_same_position, + "a {\n color: selector-unify(\".s1-1 .c .d .s1-2 .e .s1-3\", \".s2-1 .c .d .s2-2 .e .s2-3\");\n}\n", + "a {\n color: .s1-1 .s2-1 .c .d .s1-2 .s2-2 .e .s1-3.s2-3, .s2-1 .s1-1 .c .d .s1-2 .s2-2 .e .s1-3.s2-3, .s1-1 .s2-1 .c .d .s2-2 .s1-2 .e .s1-3.s2-3, .s2-1 .s1-1 .c .d .s2-2 .s1-2 .e .s1-3.s2-3;\n}\n" +); +test!( + lcs_non_contiguous_different_positions, + "a {\n color: selector-unify(\".s1-1 .c .d .s1-2 .e .s1-3\", \".c .s2-1 .d .e .s2-2 .s2-3\");\n}\n", + "a {\n color: .s1-1 .c .s2-1 .d .s1-2 .e .s2-2 .s1-3.s2-3;\n}\n" +); +test!( + root_in_first_two_layers, + "a {\n color: selector-unify(\":root .c\", \".d .e\");\n}\n", + "a {\n color: :root .d .c.e;\n}\n" +); +test!( + #[ignore = "https://github.com/sass/dart-sass/issues/969"] + root_in_first_three_layers, + "a {\n color: selector-unify(\":root .c .d\", \".e .f\");\n}\n", + "a {\n color: :root .c .e .d.f, :root .e .c .d.f;\n}\n" +); +test!( + root_in_second_two_layers, + "a {\n color: selector-unify(\".c .d\", \":root .e\");\n}\n", + "a {\n color: :root .c .d.e;\n}\n" +); +test!( + #[ignore = "https://github.com/sass/dart-sass/issues/969"] + root_in_second_three_layers, + "a {\n color: selector-unify(\".c .d\", \":root .e .f\");\n}\n", + "a {\n color: :root .c .e .d.f, :root .e .c .d.f;\n}\n" +); +test!( + root_in_both_cant_unify, + "a {\n color: selector-unify(\"c:root .d\", \"e:root .f\");\n}\n", + "" +); +test!( + root_in_both_super_selector, + "a {\n color: selector-unify(\"c:root .d\", \":root .e\");\n}\n", + "a {\n color: c:root .d.e;\n}\n" +); +test!( + root_in_both_can_unify, + "a {\n color: selector-unify(\".c:root .d\", \".e:root .f\");\n}\n", + "a {\n color: .e.c:root .d.f;\n}\n" +); +error!( + parent_in_first_arg, + "a {\n color: selector-unify(\"&\", \"c\");\n}\n", + "Error: $selector1: Parent selectors aren't allowed here." +); +error!( + parent_in_second_arg, + "a {\n color: selector-unify(\"c\", \"&\");\n}\n", + "Error: $selector2: Parent selectors aren't allowed here." +); +error!( + #[ignore = "we don't include the name of the arg in the error message"] + malformed_selector_in_first_arg, + "a {\n color: selector-unify(\"[c\", \"c\");\n}\n", "Error: $selector1: expected more input." +); +error!( + #[ignore = "we don't include the name of the arg in the error message"] + malformed_selector_in_second_arg, + "a {\n color: selector-unify(\"c\", \"[c\");\n}\n", "Error: $selector2: expected more input." +); +error!( + invalid_type_in_first_arg, + "a {\n color: selector-unify(1, \"c\");\n}\n", + "Error: $selector1: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." +); +error!( + invalid_type_in_second_arg, + "a {\n color: selector-unify(\"c\", 1);\n}\n", + "Error: $selector2: 1 is not a valid selector: it must be a string, a list of strings, or a list of lists of strings." +); diff --git a/tests/selectors.rs b/tests/selectors.rs index 5f8b666..eefe45d 100644 --- a/tests/selectors.rs +++ b/tests/selectors.rs @@ -530,17 +530,146 @@ test!( "+ {\n color: &;\n}\n", "+ {\n color: +;\n}\n" ); -error!( - #[ignore = "namespaces are not yet parsed correctly"] - empty_namespace, - "| {}", "Error: Expected identifier." +test!( + invalid_chars_in_pseudo_parens, + ":c(@#$) {\n color: &;\n}\n", + ":c(@#$) {\n color: :c(@#$);\n}\n" ); test!( - #[ignore = "namespaces are not yet parsed correctly"] - simple_namespace, + empty_namespace_element, "|f {\n color: &;\n}\n", "|f {\n color: |f;\n}\n" ); +test!( + universal_with_namespace, + "a|* {\n color: &;\n}\n", + "a|* {\n color: a|*;\n}\n" +); +test!( + psuedo_element_slotted_args, + "::slotted(b, c) {\n color: &;\n}\n", + "::slotted(b, c) {\n color: ::slotted(b, c);\n}\n" +); +test!( + a_n_plus_b, + ":nth-child(2n+0) {\n color: &;\n}\n", + ":nth-child(2n+0) {\n color: :nth-child(2n+0);\n}\n" +); +test!( + a_n_plus_b_leading_negative, + ":nth-child(-1n+6) {\n color: &;\n}\n", + ":nth-child(-1n+6) {\n color: :nth-child(-1n+6);\n}\n" +); +test!( + a_n_plus_b_leading_plus, + ":nth-child(+3n-2) {\n color: &;\n}\n", + ":nth-child(+3n-2) {\n color: :nth-child(+3n-2);\n}\n" +); +test!( + a_n_plus_b_n_alone, + ":nth-child(n) {\n color: &;\n}\n", + ":nth-child(n) {\n color: :nth-child(n);\n}\n" +); +test!( + a_n_plus_b_capital_n, + ":nth-child(N) {\n color: &;\n}\n", + ":nth-child(n) {\n color: :nth-child(n);\n}\n" +); +test!( + a_n_plus_b_n_with_leading_number, + ":nth-child(2n) {\n color: &;\n}\n", + ":nth-child(2n) {\n color: :nth-child(2n);\n}\n" +); +test!( + a_n_plus_b_n_whitespace_on_both_sides, + ":nth-child(3n + 1) {\n color: &;\n}\n", + ":nth-child(3n+1) {\n color: :nth-child(3n+1);\n}\n" +); +test!( + a_n_plus_b_n_of, + ":nth-child(2n+1 of b, c) {\n color: &;\n}\n", + ":nth-child(2n+1 of b, c) {\n color: :nth-child(2n+1 of b, c);\n}\n" +); +test!( + a_n_plus_b_n_number_alone, + ":nth-child(5) {\n color: &;\n}\n", + ":nth-child(5) {\n color: :nth-child(5);\n}\n" +); +test!( + a_n_plus_b_n_number_leading_negative, + ":nth-child(-5) {\n color: &;\n}\n", + ":nth-child(-5) {\n color: :nth-child(-5);\n}\n" +); +test!( + a_n_plus_b_n_number_leading_plus, + ":nth-child(+5) {\n color: &;\n}\n", + ":nth-child(+5) {\n color: :nth-child(+5);\n}\n" +); +test!( + a_n_plus_b_n_leading_negative_no_leading_number, + ":nth-child(-n+ 6) {\n color: &;\n}\n", + ":nth-child(-n+6) {\n color: :nth-child(-n+6);\n}\n" +); +test!( + a_n_plus_b_n_even_all_lowercase, + ":nth-child(even) {\n color: &;\n}\n", + ":nth-child(even) {\n color: :nth-child(even);\n}\n" +); +test!( + a_n_plus_b_n_even_mixed_case, + ":nth-child(eVeN) {\n color: &;\n}\n", + ":nth-child(even) {\n color: :nth-child(even);\n}\n" +); +test!( + a_n_plus_b_n_even_uppercase, + ":nth-child(EVEN) {\n color: &;\n}\n", + ":nth-child(even) {\n color: :nth-child(even);\n}\n" +); +test!( + a_n_plus_b_n_even_whitespace, + ":nth-child( even ) {\n color: &;\n}\n", + ":nth-child(even) {\n color: :nth-child(even);\n}\n" +); +error!( + a_n_plus_b_n_value_after_even, + ":nth-child(even 1) {\n color: &;\n}\n", "Error: Expected \"of\"." +); +error!( + a_n_plus_b_n_invalid_even, + ":nth-child(efven) {\n color: &;\n}\n", "Error: Expected \"even\"." +); +test!( + a_n_plus_b_n_odd_all_lowercase, + ":nth-child(odd) {\n color: &;\n}\n", + ":nth-child(odd) {\n color: :nth-child(odd);\n}\n" +); +test!( + a_n_plus_b_n_odd_mixed_case, + ":nth-child(oDd) {\n color: &;\n}\n", + ":nth-child(odd) {\n color: :nth-child(odd);\n}\n" +); +test!( + a_n_plus_b_n_odd_uppercase, + ":nth-child(ODD) {\n color: &;\n}\n", + ":nth-child(odd) {\n color: :nth-child(odd);\n}\n" +); +error!( + a_n_plus_b_n_invalid_odd, + ":nth-child(ofdd) {\n color: &;\n}\n", "Error: Expected \"odd\"." +); +error!( + a_n_plus_b_n_invalid_starting_char, + ":nth-child(f) {\n color: &;\n}\n", "Error: Expected \"n\"." +); +error!( + #[ignore = "we read until closing paren, giving a different error message"] + a_n_plus_b_n_nothing_after_open_paren, + ":nth-child({\n color: &;\n}\n", "Error: expected more input." +); +error!( + a_n_plus_b_n_invalid_char_after_even, + ":nth-child(even#) {\n color: &;\n}\n", "Error: expected \")\"." +); error!(nothing_after_period, ". {}", "Error: Expected identifier."); error!(nothing_after_hash, "# {}", "Error: Expected identifier."); error!(nothing_after_percent, "% {}", "Error: Expected identifier."); diff --git a/tests/simple-selectors.rs b/tests/simple-selectors.rs new file mode 100644 index 0000000..0c987d9 --- /dev/null +++ b/tests/simple-selectors.rs @@ -0,0 +1,15 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!( + two_classes, + "a {\n color: simple-selectors(\".foo.bar\");\n}\n", + "a {\n color: .foo, .bar;\n}\n" +); +test!( + three_classes, + "a {\n color: simple-selectors(\".foo.bar.baz\");\n}\n", + "a {\n color: .foo, .bar, .baz;\n}\n" +);