Merge pull request #12 from connorskees/selector-fns

Implement builtin selector functions
This commit is contained in:
Connor Skees 2020-06-07 23:29:06 -04:00 committed by GitHub
commit 365325729a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
36 changed files with 6007 additions and 586 deletions

View File

@ -58,6 +58,7 @@ beef = "0.4.4"
# criterion is not a dev-dependency because it makes tests take too # criterion is not a dev-dependency because it makes tests take too
# long to compile, and you cannot make dev-dependencies optional # long to compile, and you cannot make dev-dependencies optional
criterion = { version = "0.3.2", optional = true } criterion = { version = "0.3.2", optional = true }
indexmap = "1.4.0"
[features] [features]
default = ["commandline", "random"] default = ["commandline", "random"]

View File

@ -155,7 +155,10 @@ impl Mixin {
return Err(("Mixins may not contain mixin declarations.", span).into()) return Err(("Mixins may not contain mixin declarations.", span).into())
} }
Expr::Selector(selector) => { 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 { stmts.push(Spanned {
node: Stmt::RuleSet(RuleSet { node: Stmt::RuleSet(RuleSet {
super_selector: super_selector.clone(), super_selector: super_selector.clone(),

View File

@ -153,14 +153,13 @@ impl AtRule {
} }
} }
AtRuleKind::AtRoot => { 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, super_selector,
Selector::from_tokens( true,
&mut read_until_open_curly_brace(toks)?.into_iter().peekmore(), )?
scope, .resolve_parent_selectors(super_selector, false);
super_selector,
)?,
);
let mut is_some = true; let mut is_some = true;
if selector.is_empty() { if selector.is_empty() {
is_some = false; is_some = false;

View File

@ -33,7 +33,7 @@ pub(crate) fn eat_stmts<I: Iterator<Item = Token>>(
let rules = eat_stmts( let rules = eat_stmts(
toks, toks,
scope, scope,
&super_selector.zip(&selector), &selector.resolve_parent_selectors(super_selector, true),
at_root, at_root,
content, content,
)?; )?;
@ -81,11 +81,8 @@ pub(crate) fn eat_stmts_at_root<I: Iterator<Item = Token>>(
), ),
Expr::MixinDecl(..) | Expr::FunctionDecl(..) => todo!(), Expr::MixinDecl(..) | Expr::FunctionDecl(..) => todo!(),
Expr::Selector(mut selector) => { Expr::Selector(mut selector) => {
if nesting > 1 || is_some { selector =
selector = super_selector.zip(&selector); selector.resolve_parent_selectors(super_selector, nesting > 1 || is_some);
} else {
selector = Selector::replace(super_selector, selector);
}
nesting += 1; nesting += 1;
let rules = eat_stmts_at_root(toks, scope, &selector, nesting, true, content)?; let rules = eat_stmts_at_root(toks, scope, &selector, nesting, true, content)?;
nesting -= 1; nesting -= 1;

View File

@ -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<Value> { fn nth(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let mut list = match arg!(args, scope, super_selector, 0, "list") { let mut list = arg!(args, scope, super_selector, 0, "list").as_list();
Value::List(v, ..) => v,
Value::Map(m) => m.entries(),
v => vec![v],
};
let n = match arg!(args, scope, super_selector, 1, "n") { let n = match arg!(args, scope, super_selector, 1, "n") {
Value::Dimension(num, _) => num, Value::Dimension(num, _) => num,
v => { 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<Value> { fn index(mut args: CallArgs, scope: &Scope, super_selector: &Selector) -> SassResult<Value> {
args.max_args(2)?; args.max_args(2)?;
let list = match arg!(args, scope, super_selector, 0, "list") { let list = arg!(args, scope, super_selector, 0, "list").as_list();
Value::List(v, ..) => v,
Value::Map(m) => m.entries(),
v => vec![v],
};
let value = arg!(args, scope, super_selector, 1, "value"); let value = arg!(args, scope, super_selector, 1, "value");
// TODO: find a way around this unwrap. // TODO: find a way around this unwrap.
// It should be impossible to hit as the arg is // 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
let lists = args let lists = args
.get_variadic(scope, super_selector)? .get_variadic(scope, super_selector)?
.into_iter() .into_iter()
.map(|x| { .map(|x| Ok(x.node.eval(span)?.node.as_list()))
Ok(match x.node.eval(span)?.node {
Value::List(v, ..) => v,
Value::Map(m) => m.entries(),
v => vec![v],
})
})
.collect::<SassResult<Vec<Vec<Value>>>>()?; .collect::<SassResult<Vec<Vec<Value>>>>()?;
let len = lists.iter().map(Vec::len).min().unwrap_or(0); let len = lists.iter().map(Vec::len).min().unwrap_or(0);

View File

@ -50,6 +50,7 @@ pub(crate) static GLOBAL_FUNCTIONS: Lazy<GlobalFunctionMap> = Lazy::new(|| {
map::declare(&mut m); map::declare(&mut m);
math::declare(&mut m); math::declare(&mut m);
meta::declare(&mut m); meta::declare(&mut m);
selector::declare(&mut m);
string::declare(&mut m); string::declare(&mut m);
m m
}); });

View File

@ -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<Value> {
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<Value> {
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<Value> {
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<Value> {
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::<SassResult<Vec<Selector>>>()?
.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<Value> {
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::<SassResult<Vec<Selector>>>()?;
let first = parsed_selectors.remove(0);
Ok(parsed_selectors
.into_iter()
.try_fold(first, |parent, child| -> SassResult<Selector> {
Ok(Selector(SelectorList {
components: child
.0
.components
.into_iter()
.map(|complex| -> SassResult<ComplexSelector> {
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::<SassResult<Vec<ComplexSelector>>>()?,
})
.resolve_parent_selectors(&parent, false))
})?
.into_value())
}
fn selector_extend(
mut args: CallArgs,
scope: &Scope,
super_selector: &Selector,
) -> SassResult<Value> {
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<Value> {
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<Value> {
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));
}

View File

@ -47,7 +47,7 @@ impl Color {
} }
} }
fn new_hsla( const fn new_hsla(
red: Number, red: Number,
green: Number, green: Number,
blue: Number, blue: Number,

View File

@ -105,21 +105,6 @@ impl ListSeparator {
} }
} }
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct QualifiedName {
pub ident: String,
pub namespace: Option<String>,
}
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)] #[derive(Debug, Clone, Eq, PartialEq, Hash)]
pub(crate) struct Identifier(String); pub(crate) struct Identifier(String);

View File

@ -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 { SassError {
kind: SassErrorKind::ParseError { message, loc }, kind: SassErrorKind::ParseError { message, loc },
} }

View File

@ -59,6 +59,9 @@ grass input.scss
clippy::filter_map, clippy::filter_map,
clippy::else_if_without_else, clippy::else_if_without_else,
clippy::new_ret_no_self, clippy::new_ret_no_self,
renamed_and_removed_lints,
clippy::unknown_clippy_lints,
clippy::replace_consts,
// temporarily allowed while under heavy development. // temporarily allowed while under heavy development.
// eventually these allows should be refactored away // eventually these allows should be refactored away
@ -67,6 +70,7 @@ grass input.scss
clippy::todo, clippy::todo,
clippy::too_many_lines, clippy::too_many_lines,
clippy::panic, clippy::panic,
clippy::unwrap_used,
clippy::option_unwrap_used, clippy::option_unwrap_used,
clippy::result_unwrap_used, clippy::result_unwrap_used,
clippy::cast_possible_truncation, clippy::cast_possible_truncation,
@ -282,6 +286,7 @@ pub(crate) fn eat_expr<I: Iterator<Item = Token>>(
&mut values.into_iter().peekmore(), &mut values.into_iter().peekmore(),
scope, scope,
super_selector, super_selector,
true,
)?), )?),
span, span,
})); }));

View File

@ -75,7 +75,9 @@ impl Css {
super_selector, super_selector,
rules, rules,
}) => { }) => {
let selector = super_selector.zip(&selector).remove_placeholders(); let selector = selector
.resolve_parent_selectors(&super_selector, true)
.remove_placeholders();
if selector.is_empty() { if selector.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }

View File

@ -4,15 +4,15 @@ use peekmore::PeekMoreIterator;
use codemap::Span; use codemap::Span;
use super::{Selector, SelectorKind}; use super::{Namespace, QualifiedName, Selector};
use crate::common::{QualifiedName, QuoteKind}; use crate::common::QuoteKind;
use crate::error::SassResult; use crate::error::SassResult;
use crate::scope::Scope; use crate::scope::Scope;
use crate::utils::{devour_whitespace, eat_ident, is_ident, parse_quoted_string}; use crate::utils::{devour_whitespace, eat_ident, is_ident, parse_quoted_string};
use crate::value::Value; use crate::value::Value;
use crate::Token; use crate::Token;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct Attribute { pub(crate) struct Attribute {
attr: QualifiedName, attr: QualifiedName,
value: String, value: String,
@ -40,7 +40,7 @@ fn attribute_name<I: Iterator<Item = Token>>(
let ident = eat_ident(toks, scope, super_selector, span_before)?.node; let ident = eat_ident(toks, scope, super_selector, span_before)?.node;
return Ok(QualifiedName { return Ok(QualifiedName {
ident, ident,
namespace: Some('*'.to_string()), namespace: Namespace::Asterisk,
}); });
} }
let span_before = next.pos; let span_before = next.pos;
@ -49,7 +49,7 @@ fn attribute_name<I: Iterator<Item = Token>>(
Some(v) if v.kind != '|' => { Some(v) if v.kind != '|' => {
return Ok(QualifiedName { return Ok(QualifiedName {
ident: name_or_namespace.node, ident: name_or_namespace.node,
namespace: None, namespace: Namespace::None,
}); });
} }
Some(..) => {} Some(..) => {}
@ -57,14 +57,14 @@ fn attribute_name<I: Iterator<Item = Token>>(
} }
match toks.peek_forward(1) { match toks.peek_forward(1) {
Some(v) if v.kind == '=' => { Some(v) if v.kind == '=' => {
toks.peek_backward(1).unwrap(); toks.reset_view();
return Ok(QualifiedName { return Ok(QualifiedName {
ident: name_or_namespace.node, ident: name_or_namespace.node,
namespace: None, namespace: Namespace::None,
}); });
} }
Some(..) => { Some(..) => {
toks.peek_backward(1).unwrap(); toks.reset_view();
} }
None => return Err(("expected more input.", name_or_namespace.span).into()), None => return Err(("expected more input.", name_or_namespace.span).into()),
} }
@ -72,7 +72,7 @@ fn attribute_name<I: Iterator<Item = Token>>(
let ident = eat_ident(toks, scope, super_selector, span_before)?.node; let ident = eat_ident(toks, scope, super_selector, span_before)?.node;
Ok(QualifiedName { Ok(QualifiedName {
ident, ident,
namespace: Some(name_or_namespace.node), namespace: Namespace::Other(name_or_namespace.node),
}) })
} }
@ -100,19 +100,19 @@ impl Attribute {
scope: &Scope, scope: &Scope,
super_selector: &Selector, super_selector: &Selector,
start: Span, start: Span,
) -> SassResult<SelectorKind> { ) -> SassResult<Attribute> {
devour_whitespace(toks); devour_whitespace(toks);
let attr = attribute_name(toks, scope, super_selector, start)?; let attr = attribute_name(toks, scope, super_selector, start)?;
devour_whitespace(toks); devour_whitespace(toks);
if toks.peek().ok_or(("expected more input.", start))?.kind == ']' { if toks.peek().ok_or(("expected more input.", start))?.kind == ']' {
toks.next(); toks.next();
return Ok(SelectorKind::Attribute(Attribute { return Ok(Attribute {
attr, attr,
value: String::new(), value: String::new(),
modifier: None, modifier: None,
op: AttributeOp::Any, op: AttributeOp::Any,
span: start, span: start,
})); });
} }
let op = attribute_operator(toks, start)?; let op = attribute_operator(toks, start)?;
@ -152,13 +152,13 @@ impl Attribute {
toks.next(); toks.next();
Ok(SelectorKind::Attribute(Attribute { Ok(Attribute {
op, op,
attr, attr,
value, value,
modifier, modifier,
span: start, 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 { enum AttributeOp {
/// \[attr\] /// \[attr\]
/// ///

50
src/selector/common.rs Normal file
View File

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

288
src/selector/complex.rs Normal file
View File

@ -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<ComplexSelectorComponent>,
/// 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<Vec<ComplexSelector>> {
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),
}
}
}

224
src/selector/compound.rs Normal file
View File

@ -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<SimpleSelector>,
}
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<Vec<ComplexSelectorComponent>>,
) -> 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<Vec<ComplexSelector>> {
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<SimpleSelector> = 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<Self> {
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<Self> {
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 }
}
})
}
}

View File

@ -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<SimpleSelector>,
/// 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<CssMediaQuery>,
/// The span in which `extender` was defined.
pub span: Option<Span>,
}
impl Extension {
pub fn one_off(extender: ComplexSelector, specificity: Option<i32>, 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<Vec<CssMediaQuery>>) {
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);")
}
}

View File

@ -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<Vec<ComplexSelectorComponent>>,
) -> Option<Vec<Vec<ComplexSelectorComponent>>> {
debug_assert!(!complexes.is_empty());
if complexes.len() == 1 {
return Some(complexes);
}
let mut unified_base: Option<Vec<SimpleSelector>> = 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<Vec<ComplexSelectorComponent>> = 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<ComplexSelectorComponent>>,
) -> Vec<Vec<ComplexSelectorComponent>> {
let mut prefixes: Vec<Vec<ComplexSelectorComponent>> = 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<ComplexSelectorComponent> =
complex.into_iter().take(complex_len - 1).collect();
let mut new_prefixes: Vec<Vec<ComplexSelectorComponent>> = 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<ComplexSelectorComponent>,
parents_two: Vec<ComplexSelectorComponent>,
) -> Option<Vec<Vec<ComplexSelectorComponent>>> {
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::<Vec<ComplexSelectorComponent>>()]];
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<ComplexSelectorComponent>,
components_two: &mut VecDeque<ComplexSelectorComponent>,
) -> Option<Vec<Combinator>> {
let mut combinators_one: Vec<Combinator> = 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<T: PartialEq + Clone>(
list_one: &[T],
list_two: &[T],
select: Option<&dyn Fn(T, T) -> Option<T>>,
) -> Vec<T> {
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<Option<T>>> = 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<T: Clone>(
i: isize,
j: isize,
lengths: Vec<Vec<i32>>,
selections: &mut Vec<Vec<Option<T>>>,
) -> Vec<T> {
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<ComplexSelectorComponent>,
components_two: &mut VecDeque<ComplexSelectorComponent>,
result: Option<VecDeque<Vec<Vec<ComplexSelectorComponent>>>>,
) -> Option<Vec<Vec<Vec<ComplexSelectorComponent>>>> {
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<ComplexSelectorComponent>) -> Option<CompoundSelector> {
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<ComplexSelectorComponent>,
) -> VecDeque<Vec<ComplexSelectorComponent>> {
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<T: Clone>(
queue_one: &mut VecDeque<T>,
queue_two: &mut VecDeque<T>,
done: impl Fn(&VecDeque<T>) -> bool,
) -> Vec<Vec<T>> {
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<ComplexSelectorComponent>,
mut complex_two: Vec<ComplexSelectorComponent>,
) -> 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<T: Clone>(choices: Vec<Vec<T>>) -> Vec<Vec<T>> {
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, .. }))
}

800
src/selector/extend/mod.rs Normal file
View File

@ -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<SimpleSelector, HashSet<SelectorList>>,
/// A map from all extended simple selectors to the sources of those
/// extensions.
extensions: HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
/// A map from all simple selectors in extenders to the extensions that those
/// extenders define.
extensions_by_extender: HashMap<SimpleSelector, Vec<Extension>>,
/// 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<SelectorList, Vec<CssMediaQuery>>,
/// 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<SimpleSelector, i32>,
/// 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<ComplexSelector>,
/// 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<ComplexSelector, Extension> = 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<SimpleSelector, IndexMap<ComplexSelector, Extension>> =
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<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
media_query_context: &Option<Vec<CssMediaQuery>>,
) -> SelectorList {
// This could be written more simply using Vec<Vec<T>>, but we want to avoid
// any allocations in the common case where no extends apply.
let mut extended: Vec<ComplexSelector> = 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<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
media_query_context: &Option<Vec<CssMediaQuery>>,
) -> Option<Vec<ComplexSelector>> {
// 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<ComplexSelector>> = 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<ComplexSelector> = 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::<Vec<ComplexSelector>>()
})
.collect(),
)
}
/// Extends `compound` using `extensions`, and returns the contents of a
/// `SelectorList`.
fn extend_compound(
&mut self,
compound: &CompoundSelector,
extensions: &HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
media_query_context: &Option<Vec<CssMediaQuery>>,
) -> Option<Vec<ComplexSelector>> {
// 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<SimpleSelector> = HashSet::new();
let mut options: Vec<Vec<Extension>> = 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<Option<Vec<ComplexSelector>>> = paths(options)
.into_iter()
.map(|path| {
let complexes: Vec<Vec<ComplexSelectorComponent>> = 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<Vec<ComplexSelectorComponent>> = VecDeque::new();
let mut originals: Vec<SimpleSelector> = 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<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
media_query_context: &Option<Vec<CssMediaQuery>>,
targets_used: &mut HashSet<SimpleSelector>,
) -> Option<Vec<Vec<Extension>>> {
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<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
media_query_context: &Option<Vec<CssMediaQuery>>,
) -> Option<Vec<Pseudo>> {
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 `<div><img></div>` 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::<Vec<Pseudo>>();
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<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
targets_used: &mut HashSet<SimpleSelector>,
mode: ExtendMode,
) -> Option<Vec<Extension>> {
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<SimpleSelector>) -> 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<ComplexSelector>,
is_original: impl Fn(ComplexSelector) -> bool,
) -> Vec<ComplexSelector> {
// 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<ComplexSelector> = 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<T: Clone>(list: &mut VecDeque<T>, 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;
}
}

259
src/selector/list.rs Normal file
View File

@ -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<ComplexSelector>,
}
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<Self> {
let contents: Vec<ComplexSelector> = self
.components
.into_iter()
.flat_map(|c1| {
other.clone().components.into_iter().flat_map(move |c2| {
let unified: Option<Vec<Vec<ComplexSelectorComponent>>> =
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<Self> (the issue is figuring out the span)
pub fn resolve_parent_selectors(self, parent: Option<Self>, 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<ComplexSelectorComponent>> =
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<A: std::fmt::Debug>(iterable: Vec<Vec<A>>) -> Vec<A> {
let mut queues: Vec<VecDeque<A>> = 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
}

View File

@ -1,234 +1,55 @@
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display};
use codemap::Span;
use peekmore::{PeekMore, PeekMoreIterator}; use peekmore::{PeekMore, PeekMoreIterator};
use crate::common::{Brackets, ListSeparator, QuoteKind};
use crate::error::SassResult; use crate::error::SassResult;
use crate::scope::Scope; use crate::scope::Scope;
use crate::utils::{ use crate::utils::{devour_whitespace, eat_comment, parse_interpolation, read_until_newline};
devour_whitespace, eat_comment, eat_ident_no_interpolation, parse_interpolation,
read_until_closing_paren, read_until_newline, IsWhitespace,
};
use crate::value::Value; use crate::value::Value;
use crate::Token; 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 attribute;
mod common;
mod complex;
mod compound;
mod extend;
mod list;
mod parse;
mod simple;
#[derive(Clone, Debug, Eq, PartialEq)] #[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct Selector(Vec<SelectorPart>); pub(crate) struct Selector(pub SelectorList);
#[derive(Clone, Debug, Eq, PartialEq)]
struct SelectorPart {
pub inner: Vec<SelectorKind>,
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(())
}
}
impl Display for Selector { impl Display for Selector {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (idx, part) in self.0.iter().enumerate() { write!(f, "{}", self.0)
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(())
} }
} }
#[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 { impl Selector {
pub fn from_tokens<I: Iterator<Item = Token>>( pub fn from_tokens<I: Iterator<Item = Token>>(
toks: &mut PeekMoreIterator<I>, toks: &mut PeekMoreIterator<I>,
scope: &Scope, scope: &Scope,
super_selector: &Selector, super_selector: &Selector,
allows_parent: bool,
) -> SassResult<Selector> { ) -> SassResult<Selector> {
let mut string = String::new(); let mut string = String::new();
let mut span = if let Some(tok) = toks.peek() { let mut span = if let Some(tok) = toks.peek() {
tok.pos() tok.pos()
} else { } else {
return Ok(Selector::new()); return Ok(Selector::new());
}; };
while let Some(tok) = toks.next() { while let Some(tok) = toks.next() {
span = span.merge(tok.pos()); span = span.merge(tok.pos());
match tok.kind { match tok.kind {
@ -279,325 +100,47 @@ impl Selector {
break; break;
} }
let mut inner = Vec::new(); let sel_toks: Vec<Token> = string.chars().map(|x| Token::new(span, x)).collect();
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 mut iter = sel_toks.into_iter().peekmore(); let mut iter = sel_toks.into_iter().peekmore();
while let Some(tok) = iter.peek() { Ok(Selector(
let Token { kind, pos } = *tok; SelectorParser::new(&mut iter, scope, super_selector, allows_parent, true, span)
inner.push(match kind { .parse()?,
_ 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))
} }
fn consume_pseudo_selector<I: Iterator<Item = Token>>( /// Small wrapper around `SelectorList`'s method that turns an empty parent selector
toks: &mut PeekMoreIterator<I>, /// into `None`. This is a hack and in the future should be replaced.
scope: &Scope, // todo: take Option<Self> for parent
super_selector: &Selector, pub fn resolve_parent_selectors(&self, parent: &Self, implicit_parent: bool) -> Self {
span_before: Span, Self(self.0.clone().resolve_parent_selectors(
) -> SassResult<SelectorKind> { if parent.is_empty() {
let is_pseudo_element = if toks None
.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)
} else { } else {
SelectorKind::Pseudo(name) Some(parent.0.clone())
}, },
) implicit_parent,
))
} }
pub fn replace(super_selector: &Selector, this: Selector) -> Selector { pub fn is_super_selector(&self, other: &Self) -> bool {
if super_selector.0.is_empty() || this.0.is_empty() { self.0.is_superselector(&other.0)
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 zip(&self, other: &Selector) -> Selector { pub fn contains_parent_selector(&self) -> bool {
if self.0.is_empty() { self.0.contains_parent_selector()
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<SelectorKind> = 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 remove_placeholders(self) -> Selector { pub fn remove_placeholders(self) -> Selector {
Selector( Self(SelectorList {
self.0 components: self
.0
.components
.into_iter() .into_iter()
.filter_map(|s| { .filter(|c| !c.is_invisible())
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,
})
}
})
.collect(), .collect(),
) })
} }
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@ -605,18 +148,14 @@ impl Selector {
} }
pub const fn new() -> Selector { pub const fn new() -> Selector {
Selector(Vec::new()) Selector(SelectorList::new())
} }
pub fn contains_super_selector(&self) -> bool { pub fn into_value(self) -> Value {
self.0.iter().any(|s| s.contains_super_selector) self.0.to_sass_list()
} }
pub fn into_value(&self) -> Value { pub fn unify(self, other: &Self) -> Option<Self> {
Value::List( Some(Selector(self.0.unify(&other.0)?))
self.0.iter().map(SelectorPart::into_value).collect(),
ListSeparator::Comma,
Brackets::None,
)
} }
} }

607
src/selector/parse.rs Normal file
View File

@ -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<Item = Token>> {
/// 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<I>,
scope: &'a Scope,
super_selector: &'a Selector,
span: Span,
}
impl<'a, I: Iterator<Item = Token>> SelectorParser<'a, I> {
pub fn new(
toks: &'a mut PeekMoreIterator<I>,
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<SelectorList> {
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<SelectorList> {
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<ComplexSelector> {
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<CompoundSelector> {
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<SimpleSelector> {
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<SimpleSelector> {
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<SimpleSelector> {
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<SimpleSelector> {
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<SimpleSelector> {
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<String> = None;
let mut selector: Option<SelectorList> = 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<SimpleSelector> {
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<SimpleSelector> {
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<SimpleSelector> {
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<String> {
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<String> {
// 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::<String>())
}
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,
}
}

635
src/selector/simple.rs Normal file
View File

@ -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<String>),
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<Self>) -> Option<Vec<Self>> {
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<Self>) -> Option<Vec<Self>> {
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<SimpleSelector> = 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<Self>) -> Option<Vec<Self>> {
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<Self> {
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<Self>) -> Option<Vec<Self>> {
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<Self>) -> Option<Vec<Self>> {
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<String>,
/// 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<SelectorList>,
}
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<Vec<ComplexSelectorComponent>>,
) -> 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<SelectorList>) -> 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<Item = Pseudo>`
fn selector_pseudos_named(compound: CompoundSelector, name: &str, is_class: bool) -> Vec<Pseudo> {
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()
}

View File

@ -421,7 +421,8 @@ impl<'a> StyleSheetParser<'a> {
} }
Expr::Selector(s) => { Expr::Selector(s) => {
self.nesting += 1; 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 { stmts.push(Spanned {
node: Stmt::RuleSet(RuleSet { node: Stmt::RuleSet(RuleSet {
super_selector: super_selector.clone(), super_selector: super_selector.clone(),

View File

@ -1,13 +1,17 @@
use std::iter::Iterator; use std::iter::Iterator;
use peekmore::PeekMore;
use codemap::{Span, Spanned}; use codemap::{Span, Spanned};
use crate::color::Color; use crate::color::Color;
use crate::common::{Brackets, ListSeparator, Op, QuoteKind}; use crate::common::{Brackets, ListSeparator, Op, QuoteKind};
use crate::error::SassResult; use crate::error::SassResult;
use crate::scope::Scope;
use crate::selector::Selector;
use crate::unit::Unit; use crate::unit::Unit;
use crate::utils::hex_char_for; use crate::utils::hex_char_for;
use crate::Cow; use crate::{Cow, Token};
use css_function::is_special_function; use css_function::is_special_function;
pub(crate) use map::SassMap; pub(crate) use map::SassMap;
@ -309,4 +313,77 @@ impl Value {
v => v.to_css_string(span)?, v => v.to_css_string(span)?,
}) })
} }
pub fn as_list(self) -> Vec<Value> {
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<Selector> {
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<Option<String>> {
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),
}))
}
} }

View File

@ -809,7 +809,7 @@ impl Value {
} }
'&' => { '&' => {
let span = toks.next().unwrap().pos(); 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) { if let Some(Token { kind: '{', pos }) = toks.peek_forward(1) {

View File

@ -187,6 +187,7 @@ error!(toplevel_caret_alone, "^", "Error: expected \"{\".");
test!(toplevel_gt_as_selector, "> {}", ""); test!(toplevel_gt_as_selector, "> {}", "");
test!(toplevel_tilde_as_selector, "~ {}", ""); test!(toplevel_tilde_as_selector, "~ {}", "");
error!(toplevel_lt_as_selector, "< {}", "Error: expected selector."); error!(toplevel_lt_as_selector, "< {}", "Error: expected selector.");
error!(toplevel_pipe, "| {}", "Error: Expected identifier.");
error!( error!(
toplevel_question_as_selector, toplevel_question_as_selector,
"? {}", "Error: expected selector." "? {}", "Error: expected selector."

476
tests/is-superselector.rs Normal file
View File

@ -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

78
tests/selector-append.rs Normal file
View File

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

236
tests/selector-extend.rs Normal file
View File

@ -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/

158
tests/selector-nest.rs Normal file
View File

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

100
tests/selector-parse.rs Normal file
View File

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

40
tests/selector-replace.rs Normal file
View File

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

602
tests/selector-unify.rs Normal file
View File

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

View File

@ -530,17 +530,146 @@ test!(
"+ {\n color: &;\n}\n", "+ {\n color: &;\n}\n",
"+ {\n color: +;\n}\n" "+ {\n color: +;\n}\n"
); );
error!( test!(
#[ignore = "namespaces are not yet parsed correctly"] invalid_chars_in_pseudo_parens,
empty_namespace, ":c(@#$) {\n color: &;\n}\n",
"| {}", "Error: Expected identifier." ":c(@#$) {\n color: :c(@#$);\n}\n"
); );
test!( test!(
#[ignore = "namespaces are not yet parsed correctly"] empty_namespace_element,
simple_namespace,
"|f {\n color: &;\n}\n", "|f {\n color: &;\n}\n",
"|f {\n color: |f;\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_period, ". {}", "Error: Expected identifier.");
error!(nothing_after_hash, "# {}", "Error: Expected identifier."); error!(nothing_after_hash, "# {}", "Error: Expected identifier.");
error!(nothing_after_percent, "% {}", "Error: Expected identifier."); error!(nothing_after_percent, "% {}", "Error: Expected identifier.");

15
tests/simple-selectors.rs Normal file
View File

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