Merge pull request #12 from connorskees/selector-fns
Implement builtin selector functions
This commit is contained in:
commit
365325729a
@ -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"]
|
||||||
|
@ -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(),
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
|
@ -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
|
||||||
});
|
});
|
||||||
|
@ -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));
|
||||||
|
}
|
||||||
|
@ -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,
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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 },
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
}));
|
}));
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
|
@ -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
50
src/selector/common.rs
Normal 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
288
src/selector/complex.rs
Normal 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
224
src/selector/compound.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
59
src/selector/extend/extension.rs
Normal file
59
src/selector/extend/extension.rs
Normal 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);")
|
||||||
|
}
|
||||||
|
}
|
788
src/selector/extend/functions.rs
Normal file
788
src/selector/extend/functions.rs
Normal 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
800
src/selector/extend/mod.rs
Normal 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
259
src/selector/list.rs
Normal 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
|
||||||
|
}
|
@ -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
607
src/selector/parse.rs
Normal 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
635
src/selector/simple.rs
Normal 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()
|
||||||
|
}
|
@ -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(),
|
||||||
|
@ -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),
|
||||||
|
}))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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) {
|
||||||
|
@ -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
476
tests/is-superselector.rs
Normal 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
78
tests/selector-append.rs
Normal 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
236
tests/selector-extend.rs
Normal 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
158
tests/selector-nest.rs
Normal 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
100
tests/selector-parse.rs
Normal 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
40
tests/selector-replace.rs
Normal 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
602
tests/selector-unify.rs
Normal 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."
|
||||||
|
);
|
@ -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
15
tests/simple-selectors.rs
Normal 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"
|
||||||
|
);
|
Loading…
x
Reference in New Issue
Block a user