diff --git a/src/builtin/selector.rs b/src/builtin/selector.rs index 97a5c6d..3e998d3 100644 --- a/src/builtin/selector.rs +++ b/src/builtin/selector.rs @@ -148,7 +148,7 @@ fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { @@ -163,7 +163,7 @@ fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult) -> SassResult { diff --git a/src/lib.rs b/src/lib.rs index 1351e0e..82076ff 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -107,7 +107,7 @@ use crate::{ output::Css, parse::{common::NeverEmptyVec, Parser}, scope::Scope, - selector::Selector, + selector::{Extender, Selector}, }; mod args; @@ -145,6 +145,7 @@ fn raw_to_parse_error(map: &CodeMap, err: Error) -> Error { #[cfg(not(feature = "wasm"))] pub fn from_path(p: &str) -> Result { let mut map = CodeMap::new(); + let mut extender = Extender::new(); let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?); Css::from_stmts( Parser { @@ -164,12 +165,14 @@ pub fn from_path(p: &str) -> Result { in_control_flow: false, at_root: true, at_root_has_selector: false, + extender: &mut extender, } .parse() .map_err(|e| raw_to_parse_error(&map, e))?, + &mut extender, ) .map_err(|e| raw_to_parse_error(&map, e))? - .pretty_print(&map) + .pretty_print(&map, &mut extender) .map_err(|e| raw_to_parse_error(&map, e)) } @@ -187,6 +190,7 @@ pub fn from_path(p: &str) -> Result { #[cfg(not(feature = "wasm"))] pub fn from_string(p: String) -> Result { let mut map = CodeMap::new(); + let mut extender = Extender::new(); let file = map.add_file("stdin".into(), p); Css::from_stmts( Parser { @@ -206,12 +210,14 @@ pub fn from_string(p: String) -> Result { in_control_flow: false, at_root: true, at_root_has_selector: false, + extender: &mut extender, } .parse() .map_err(|e| raw_to_parse_error(&map, e))?, + &mut extender, ) .map_err(|e| raw_to_parse_error(&map, e))? - .pretty_print(&map) + .pretty_print(&map, &mut extender) .map_err(|e| raw_to_parse_error(&map, e)) } diff --git a/src/output.rs b/src/output.rs index 35c541c..2c3c1c3 100644 --- a/src/output.rs +++ b/src/output.rs @@ -3,7 +3,7 @@ use std::io::Write; use codemap::CodeMap; -use crate::{error::SassResult, parse::Stmt, selector::Selector, style::Style}; +use crate::{error::SassResult, parse::Stmt, selector::Extender, selector::Selector, style::Style}; #[derive(Debug, Clone)] enum Toplevel { @@ -61,7 +61,7 @@ impl Toplevel { } #[derive(Debug, Clone)] -pub struct Css { +pub(crate) struct Css { blocks: Vec, } @@ -70,27 +70,36 @@ impl Css { Css { blocks: Vec::new() } } - pub(crate) fn from_stmts(s: Vec) -> SassResult { - Css::new().parse_stylesheet(s) + pub(crate) fn from_stmts(s: Vec, extender: &mut Extender) -> SassResult { + Css::new().parse_stylesheet(s, extender) } - fn parse_stmt(&mut self, stmt: Stmt) -> SassResult> { + fn parse_stmt(&mut self, stmt: Stmt, extender: &mut Extender) -> SassResult> { Ok(match stmt { Stmt::RuleSet { selector, super_selector, body, } => { - let selector = selector - .resolve_parent_selectors(&super_selector, true) - .remove_placeholders(); + if body.is_empty() { + return Ok(Vec::new()); + } + let selector = if extender.is_empty() { + selector.resolve_parent_selectors(&super_selector, true) + } else { + Selector(extender.add_selector( + selector.resolve_parent_selectors(&super_selector, true).0, + None, + )) + } + .remove_placeholders(); if selector.is_empty() { return Ok(Vec::new()); } let mut vals = vec![Toplevel::new_rule(selector)]; for rule in body { match rule { - Stmt::RuleSet { .. } => vals.extend(self.parse_stmt(rule)?), + Stmt::RuleSet { .. } => vals.extend(self.parse_stmt(rule, extender)?), Stmt::Style(s) => vals.get_mut(0).unwrap().push_style(*s)?, Stmt::Comment(s) => vals.get_mut(0).unwrap().push_comment(s), Stmt::Media { params, body, .. } => { @@ -102,7 +111,7 @@ impl Css { Stmt::Return(..) => unreachable!(), Stmt::AtRoot { body } => body .into_iter() - .map(|r| Ok(vals.extend(self.parse_stmt(r)?))) + .map(|r| Ok(vals.extend(self.parse_stmt(r, extender)?))) .collect::>()?, }; } @@ -119,10 +128,10 @@ impl Css { }) } - fn parse_stylesheet(mut self, stmts: Vec) -> SassResult { + fn parse_stylesheet(mut self, stmts: Vec, extender: &mut Extender) -> SassResult { let mut is_first = true; for stmt in stmts { - let v = self.parse_stmt(stmt)?; + let v = self.parse_stmt(stmt, extender)?; // this is how we print newlines between unrelated styles // it could probably be refactored if !v.is_empty() { @@ -138,9 +147,9 @@ impl Css { Ok(self) } - pub fn pretty_print(self, map: &CodeMap) -> SassResult { + pub fn pretty_print(self, map: &CodeMap, extender: &mut Extender) -> SassResult { let mut string = Vec::new(); - self._inner_pretty_print(&mut string, map, 0)?; + self._inner_pretty_print(&mut string, map, extender, 0)?; if string.iter().any(|s| !s.is_ascii()) { return Ok(format!("@charset \"UTF-8\";\n{}", unsafe { String::from_utf8_unchecked(string) @@ -153,10 +162,12 @@ impl Css { self, buf: &mut Vec, map: &CodeMap, + extender: &mut Extender, nesting: usize, ) -> SassResult<()> { let mut has_written = false; let padding = vec![' '; nesting * 2].iter().collect::(); + let mut should_emit_newline = false; for block in self.blocks { match block { Toplevel::RuleSet(selector, styles) => { @@ -164,6 +175,10 @@ impl Css { continue; } has_written = true; + if should_emit_newline { + should_emit_newline = false; + writeln!(buf)?; + } writeln!(buf, "{}{} {{", padding, selector)?; for style in styles { writeln!(buf, "{} {}", padding, style.to_string()?)?; @@ -175,6 +190,11 @@ impl Css { writeln!(buf, "{}/*{}*/", padding, s)?; } Toplevel::UnknownAtRule { params, name, body } => { + if should_emit_newline { + should_emit_newline = false; + writeln!(buf)?; + } + if params.is_empty() { write!(buf, "{}@{}", padding, name)?; } else { @@ -188,15 +208,29 @@ impl Css { writeln!(buf, " {{")?; } - Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?; + Css::from_stmts(body, extender)?._inner_pretty_print( + buf, + map, + extender, + nesting + 1, + )?; writeln!(buf, "{}}}", padding)?; } Toplevel::Media { params, body } => { if body.is_empty() { continue; } + if should_emit_newline { + should_emit_newline = false; + writeln!(buf)?; + } writeln!(buf, "{}@media {} {{", padding, params)?; - Css::from_stmts(body)?._inner_pretty_print(buf, map, nesting + 1)?; + Css::from_stmts(body, extender)?._inner_pretty_print( + buf, + map, + extender, + nesting + 1, + )?; writeln!(buf, "{}}}", padding)?; } Toplevel::Style(s) => { @@ -204,8 +238,9 @@ impl Css { } Toplevel::Newline => { if has_written { - writeln!(buf)? + should_emit_newline = true; } + continue; } } } diff --git a/src/parse/function.rs b/src/parse/function.rs index a8f26a1..5ca1dde 100644 --- a/src/parse/function.rs +++ b/src/parse/function.rs @@ -78,6 +78,7 @@ impl<'a> Parser<'a> { in_control_flow: self.in_control_flow, at_root: false, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; diff --git a/src/parse/import.rs b/src/parse/import.rs index bd8e46b..3fb553e 100644 --- a/src/parse/import.rs +++ b/src/parse/import.rs @@ -86,6 +86,7 @@ impl<'a> Parser<'a> { in_control_flow: self.in_control_flow, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse(); } diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 9a14f9e..82c899d 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -110,6 +110,7 @@ impl<'a> Parser<'a> { content, at_root: false, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; diff --git a/src/parse/mod.rs b/src/parse/mod.rs index bc5c1ea..13bcc06 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -9,7 +9,7 @@ use crate::{ common::{Brackets, ListSeparator}, error::SassResult, scope::Scope, - selector::{Selector, SelectorParser}, + selector::{ComplexSelectorComponent, ExtendRule, Extender, Selector, SelectorParser}, style::Style, unit::Unit, utils::{ @@ -86,6 +86,7 @@ pub(crate) struct Parser<'a> { /// If this parser is inside an `@at-rule` block, this is whether or /// not the `@at-rule` block has a super selector pub at_root_has_selector: bool, + pub extender: &'a mut Extender, } impl<'a> Parser<'a> { @@ -348,29 +349,37 @@ impl<'a> Parser<'a> { let mut iter = sel_toks.into_iter().peekmore(); - Ok(Selector( - SelectorParser::new( - &mut Parser { - toks: &mut iter, - map: self.map, - path: self.path, - scopes: self.scopes, - global_scope: self.global_scope, - super_selectors: self.super_selectors, - span_before: self.span_before, - content: self.content.clone(), - in_mixin: self.in_mixin, - in_function: self.in_function, - in_control_flow: self.in_control_flow, - at_root: self.at_root, - at_root_has_selector: self.at_root_has_selector, - }, - allows_parent, - true, - span, - ) - .parse()?, - )) + let selector = SelectorParser::new( + &mut Parser { + toks: &mut iter, + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content.clone(), + in_mixin: self.in_mixin, + in_function: self.in_function, + in_control_flow: self.in_control_flow, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + }, + allows_parent, + true, + span, + ) + .parse()?; + + // todo: HACK: we have this here to support `&`, but I'm not actually + // sure we shouldn't be adding it. It's tricky to change how we resolve + // parent selectors because of `@at-root` hacks + Ok(Selector(if selector.contains_parent_selector() { + selector + } else { + self.extender.add_selector(selector, None) + })) } /// Eat and return the contents of a comment. @@ -580,6 +589,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse(); } @@ -601,6 +611,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse() } @@ -722,7 +733,7 @@ impl<'a> Parser<'a> { map: self.map, path: self.path, scopes: self.scopes, - global_scope: &mut self.global_scope, + global_scope: self.global_scope, super_selectors: self.super_selectors, span_before: self.span_before, content: self.content.clone(), @@ -731,6 +742,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; if !these_stmts.is_empty() { @@ -743,7 +755,7 @@ impl<'a> Parser<'a> { map: self.map, path: self.path, scopes: self.scopes, - global_scope: &mut self.global_scope, + global_scope: self.global_scope, super_selectors: self.super_selectors, span_before: self.span_before, content: self.content.clone(), @@ -752,6 +764,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?, ); @@ -801,6 +814,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; if !these_stmts.is_empty() { @@ -822,6 +836,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?, ); @@ -932,6 +947,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; if !these_stmts.is_empty() { @@ -953,6 +969,7 @@ impl<'a> Parser<'a> { in_control_flow: true, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?, ); @@ -1072,6 +1089,7 @@ impl<'a> Parser<'a> { in_control_flow: self.in_control_flow, at_root: false, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse()?; @@ -1140,6 +1158,7 @@ impl<'a> Parser<'a> { in_control_flow: self.in_control_flow, at_root: true, at_root_has_selector, + extender: self.extender, } .parse()? .into_iter() @@ -1167,7 +1186,79 @@ impl<'a> Parser<'a> { #[allow(clippy::unused_self)] fn parse_extend(&mut self) -> SassResult<()> { - todo!("@extend not yet implemented") + // todo: track when inside ruleset or `@content` + // if !self.in_style_rule && !self.in_mixin && !self.in_content_block { + // return Err(("@extend may only be used within style rules.", self.span_before).into()); + // } + let value = Parser { + toks: &mut read_until_semicolon_or_closing_curly_brace(self.toks)? + .into_iter() + .peekmore(), + map: self.map, + path: self.path, + scopes: self.scopes, + global_scope: self.global_scope, + super_selectors: self.super_selectors, + span_before: self.span_before, + content: self.content.clone(), + in_mixin: self.in_mixin, + in_function: self.in_function, + in_control_flow: self.in_control_flow, + at_root: self.at_root, + at_root_has_selector: self.at_root_has_selector, + extender: self.extender, + } + .parse_selector(false, true, String::new())?; + + let is_optional = if let Some(Token { kind: '!', .. }) = self.toks.peek() { + self.toks.next(); + assert_eq!( + self.parse_identifier_no_interpolation(false)?.node, + "optional" + ); + true + } else { + false + }; + + self.whitespace(); + + if let Some(Token { kind: ';', .. }) = self.toks.peek() { + self.toks.next(); + } + + let extend_rule = ExtendRule::new(value.clone(), is_optional, self.span_before); + + for complex in value.0.components { + if complex.components.len() != 1 || !complex.components.first().unwrap().is_compound() { + // If the selector was a compound selector but not a simple + // selector, emit a more explicit error. + return Err(("complex selectors may not be extended.", self.span_before).into()); + } + + let compound = match complex.components.first() { + Some(ComplexSelectorComponent::Compound(c)) => c.clone(), + Some(..) | None => todo!(), + }; + if compound.components.len() != 1 { + return Err(( + format!( + "compound selectors may no longer be extended.\nConsider `@extend {}` instead.\nSee http://bit.ly/ExtendCompound for details.\n", + compound.components.into_iter().map(|x| x.to_string()).collect::>().join(", ") + ) + , self.span_before).into()); + } + + self.extender.add_extension( + self.super_selectors.last().clone().0, + compound.components.first().unwrap(), + &extend_rule, + &None, + Some(self.span_before), + ) + } + + Ok(()) } #[allow(clippy::unused_self)] diff --git a/src/parse/value.rs b/src/parse/value.rs index b861d70..0a33bb6 100644 --- a/src/parse/value.rs +++ b/src/parse/value.rs @@ -157,6 +157,7 @@ impl<'a> Parser<'a> { in_control_flow: self.in_control_flow, at_root: self.at_root, at_root_has_selector: self.at_root_has_selector, + extender: self.extender, } .parse_value() } @@ -257,6 +258,8 @@ impl<'a> Parser<'a> { None => return None, }; + self.span_before = span; + if self.whitespace() { return Some(Ok(Spanned { node: IntermediateValue::Whitespace, @@ -491,10 +494,13 @@ impl<'a> Parser<'a> { Err(e) => return Some(Err(e)), }; span = span.merge(v.span); - if v.node.to_ascii_lowercase().as_str() == "important" { - IntermediateValue::Value(Value::Important).span(span) - } else { - return Some(Err(("Expected \"important\".", span).into())); + // TODO: we return `None` when encountering `optional` here as a hack for + // supporting `!optional` in `@extend`. In the future, we should have a better + // check for `!optional` as this technically allows `!optional` everywhere + match v.node.to_ascii_lowercase().as_str() { + "important" => IntermediateValue::Value(Value::Important).span(span), + "optional" => return None, + _ => return Some(Err(("Expected \"important\".", span).into())), } } '/' => { diff --git a/src/selector/extend/extension.rs b/src/selector/extend/extension.rs index d7a2317..c5bba0e 100644 --- a/src/selector/extend/extension.rs +++ b/src/selector/extend/extension.rs @@ -3,7 +3,7 @@ use codemap::Span; use super::{ComplexSelector, CssMediaQuery, SimpleSelector}; #[derive(Clone, Debug, Eq, PartialEq, Hash)] -pub(super) struct Extension { +pub(crate) struct Extension { /// The selector in which the `@extend` appeared. pub extender: ComplexSelector, @@ -26,10 +26,13 @@ pub(super) struct Extension { /// 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, + pub media_context: Option>, /// The span in which `extender` was defined. pub span: Option, + + pub left: Option>, + pub right: Option>, } impl Extension { @@ -41,7 +44,9 @@ impl Extension { span: None, is_optional: true, is_original, - media_context: Vec::new(), + media_context: None, + left: None, + right: None, } } @@ -51,12 +56,16 @@ impl Extension { // from this returning a `Result` will make some code returning `Option`s much uglier (we can't // use `?` to return both `Option` and `Result` from the same function) pub fn assert_compatible_media_context(&self, media_context: &Option>) { - if let Some(media_context) = media_context { - if &self.media_context == media_context { - return; - } + if &self.media_context == media_context { + return; } // Err(("You may not @extend selectors across media queries.", self.span.unwrap()).into()) } + + #[allow(clippy::missing_const_for_fn)] + pub fn with_extender(mut self, extender: ComplexSelector) -> Self { + self.extender = extender; + self + } } diff --git a/src/selector/extend/functions.rs b/src/selector/extend/functions.rs index 5527dc5..98a20af 100644 --- a/src/selector/extend/functions.rs +++ b/src/selector/extend/functions.rs @@ -77,23 +77,21 @@ pub(crate) fn weave( let target = complex.last().unwrap().clone(); - if complex.len() == 1 { + let complex_len = complex.len(); + + if complex_len == 1 { for prefix in &mut prefixes { prefix.push(target.clone()); } continue; } - let complex_len = complex.len(); - let parents: Vec = complex.into_iter().take(complex_len - 1).collect(); let mut new_prefixes: Vec> = Vec::new(); for prefix in prefixes { - let parent_prefixes = weave_parents(prefix, parents.clone()); - - if let Some(parent_prefixes) = parent_prefixes { + if let Some(parent_prefixes) = weave_parents(prefix, parents.clone()) { for mut parent_prefix in parent_prefixes { parent_prefix.push(target.clone()); new_prefixes.push(parent_prefix); @@ -624,24 +622,24 @@ fn group_selectors( let mut iter = complex.into_iter(); - let mut group = if let Some(c) = iter.next() { + groups.push_back(if let Some(c) = iter.next() { vec![c] } else { return groups; - }; - - groups.push_back(group.clone()); + }); for c in iter { - if group + let mut last_group = groups.pop_back().unwrap(); + if last_group .last() .map_or(false, ComplexSelectorComponent::is_combinator) || c.is_combinator() { - group.push(c); + last_group.push(c); + groups.push_back(last_group); } else { - group = vec![c]; - groups.push_back(group.clone()); + groups.push_back(last_group); + groups.push_back(vec![c]); } } diff --git a/src/selector/extend/merged.rs b/src/selector/extend/merged.rs new file mode 100644 index 0000000..84a7074 --- /dev/null +++ b/src/selector/extend/merged.rs @@ -0,0 +1,104 @@ +use crate::error::SassResult; + +use super::Extension; + +/// An `Extension` created by merging two `Extension`s with the same extender +/// and target. +/// +/// This is used when multiple mandatory extensions exist to ensure that both of +/// them are marked as resolved. +pub(super) struct MergedExtension; + +impl MergedExtension { + /// Returns an extension that combines `left` and `right`. + /// + /// Returns an `Err` if `left` and `right` have incompatible media + /// contexts. + /// + /// Returns an `Err` if `left` and `right` don't have the same + /// extender and target. + pub fn merge(left: Extension, right: Extension) -> SassResult { + if left.extender != right.extender || left.target != right.target { + todo!("we need a span to throw a proper error") + // return Err((format!("{} and {} aren't the same extension.", left, right), )) + } + + if left.media_context.is_some() + && right.media_context.is_some() + && left.media_context != right.media_context + { + todo!() + // throw SassException( + // "From ${left.span.message('')}\n" + // "You may not @extend the same selector from within different media " + // "queries.", + // right.span); + } + + if right.is_optional && right.media_context.is_none() { + return Ok(left); + } + + if left.is_optional && left.media_context.is_none() { + return Ok(right); + } + + Ok(MergedExtension::into_extension(left, right)) + } + + fn into_extension(left: Extension, right: Extension) -> Extension { + Extension { + extender: left.extender, + target: left.target, + span: left.span, + media_context: match left.media_context { + Some(v) => Some(v), + None => right.media_context, + }, + specificity: left.specificity, + is_optional: true, + is_original: false, + left: None, + right: None, + } + // : super(left.extender, left.target, left.extenderSpan, left.span, + // left.mediaContext ?? right.mediaContext, + // specificity: left.specificity, optional: true); + } + + /// Returns all leaf-node `Extension`s in the tree or `MergedExtension`s. + #[allow(dead_code, unused_mut, clippy::unused_self)] + pub fn unmerge(mut self) -> Vec { + todo!() + /* Iterable unmerge() sync* { + if (left is MergedExtension) { + yield* (left as MergedExtension).unmerge(); + } else { + yield left; + } + + if (right is MergedExtension) { + yield* (right as MergedExtension).unmerge(); + } else { + yield right; + } + } + */ + } +} +/* +class MergedExtension extends Extension { + /// One of the merged extensions. + final Extension left; + + /// The other merged extension. + final Extension right; + + MergedExtension._(this.left, this.right) + : super(left.extender, left.target, left.extenderSpan, left.span, + left.mediaContext ?? right.mediaContext, + specificity: left.specificity, optional: true); + + +} +*/ diff --git a/src/selector/extend/mod.rs b/src/selector/extend/mod.rs index b2afa00..f5b27b3 100644 --- a/src/selector/extend/mod.rs +++ b/src/selector/extend/mod.rs @@ -1,7 +1,14 @@ -use std::collections::{HashMap, HashSet, VecDeque}; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + hash::Hash, +}; + +use codemap::Span; use indexmap::IndexMap; +use crate::error::SassResult; + use super::{ ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SelectorList, SimpleSelector, @@ -10,9 +17,13 @@ use super::{ use extension::Extension; pub(crate) use functions::unify_complex; use functions::{paths, weave}; +use merged::MergedExtension; +pub(crate) use rule::ExtendRule; mod extension; mod functions; +mod merged; +mod rule; #[derive(Clone, Debug, Eq, PartialEq, Hash)] pub(crate) struct CssMediaQuery; @@ -101,65 +112,79 @@ impl Extender { selector: SelectorList, source: SelectorList, targets: SelectorList, - ) -> SelectorList { + ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::AllTargets) } + pub fn new() -> Self { + Self { + selectors: HashMap::new(), + extensions: HashMap::new(), + extensions_by_extender: HashMap::new(), + media_contexts: HashMap::new(), + source_specificity: HashMap::new(), + originals: HashSet::new(), + mode: ExtendMode::Normal, + } + } + + /// Whether there exist any extensions + pub fn is_empty(&self) -> bool { + self.extensions.is_empty() + } + pub fn replace( selector: SelectorList, source: SelectorList, targets: SelectorList, - ) -> SelectorList { + ) -> SassResult { Self::extend_or_replace(selector, source, targets, ExtendMode::Replace) } fn extend_or_replace( - mut selector: SelectorList, + selector: SelectorList, source: SelectorList, targets: SelectorList, mode: ExtendMode, - ) -> SelectorList { + ) -> SassResult { let extenders: IndexMap = source .components - .clone() .into_iter() - .zip( - source - .components - .into_iter() - .map(|complex| Extension::one_off(complex, None, false)), - ) + .map(|complex| (complex.clone(), 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_targets = targets + .components + .into_iter() + .map(|complex| { + if complex.components.len() == 1 { + Ok(complex.components.first().unwrap().as_compound().clone()) + } else { + todo!("Can't extend complex selector $complex.") + } + }) + .collect::>>()?; - let compound = match complex.components.first() { - Some(ComplexSelectorComponent::Compound(c)) => c, - Some(..) | None => todo!(), - }; + let extensions: HashMap> = + compound_targets + .into_iter() + .flat_map(|compound| { + compound + .components + .into_iter() + .map(|simple| (simple, extenders.clone())) + }) + .collect(); - let extensions: HashMap> = - compound - .components - .clone() - .into_iter() - .map(|simple| (simple, extenders.clone())) - .collect(); + let mut extender = Extender::with_mode(mode); - 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); + if !selector.is_invisible() { + extender + .originals + .extend(selector.components.iter().cloned()); } - selector + Ok(extender.extend_list(selector, &extensions, &None)) } fn with_mode(mode: ExtendMode) -> Self { @@ -199,7 +224,7 @@ impl Extender { } SelectorList { - components: self.trim(extended, |complex| self.originals.contains(&complex)), + components: self.trim(extended, |complex| self.originals.contains(complex)), } } @@ -258,6 +283,13 @@ impl Extender { line_break: false, }]) } + } else if let Some(component @ ComplexSelectorComponent::Combinator(..)) = + complex.components.get(i) + { + extended_not_expanded.push(vec![ComplexSelector { + components: vec![component.clone()], + line_break: false, + }]) } } @@ -267,8 +299,6 @@ impl Extender { let mut first = true; - let mut originals: Vec = Vec::new(); - Some( paths(extended_not_expanded) .into_iter() @@ -287,8 +317,11 @@ impl Extender { || path.iter().any(|input_complex| input_complex.line_break), }; - if first && originals.contains(&complex.clone()) { - originals.push(output_complex.clone()); + // Make sure that copies of `complex` retain their status as "original" + // selectors. This includes selectors that are modified because a :not() + // was extended into. + if first && self.originals.contains(&complex.clone()) { + self.originals.insert(output_complex.clone()); } first = false; @@ -302,6 +335,11 @@ impl Extender { /// Extends `compound` using `extensions`, and returns the contents of a /// `SelectorList`. + /// + /// The `in_original` parameter indicates whether this is in an original + /// complex selector, meaning that `compound` should not be trimmed out. + // todo: `in_original` is actually obsolete and we should upstream its removal + // to dart-sass fn extend_compound( &mut self, compound: &CompoundSelector, @@ -341,7 +379,11 @@ impl Extender { // 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() { + // todo: test for `extensions.len() > 2`. may cause issues + if !targets_used.is_empty() + && targets_used.len() != extensions.len() + && self.mode != ExtendMode::Normal + { return None; } @@ -401,7 +443,7 @@ impl Extender { .clone() .into_iter() .flat_map(|state| { - assert!(state.extender.components.len() == 1); + debug_assert!(state.extender.components.len() == 1); match state.extender.components.last().cloned() { Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(..) | None => unreachable!(), @@ -630,8 +672,8 @@ impl Extender { } } - // Extends `simple` without extending the contents of any selector pseudos - // it contains. + /// Extends `simple` without extending the contents of any selector pseudos + /// it contains. fn without_pseudo( &self, simple: SimpleSelector, @@ -696,15 +738,15 @@ impl Extender { 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. + /// Removes elements from `selectors` if they're subselectors of other + /// elements. + /// + /// The `is_original` callback indicates which selectors are original to the + /// document, and thus should never be trimmed. fn trim( &self, selectors: Vec, - is_original: impl Fn(ComplexSelector) -> bool, + is_original: impl Fn(&ComplexSelector) -> bool, ) -> Vec { // Avoid truly horrific quadratic behavior. // @@ -723,69 +765,378 @@ impl Extender { 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 { + for i in (0..=(selectors.len().saturating_sub(1))).rev() { + let mut should_continue_to_outer = false; + let complex1 = selectors.get(i).unwrap(); + if is_original(complex1) { + // Make sure we don't include duplicate originals, which could happen if + // a style rule extends a component of its own selector. + for j in 0..num_originals { + if result.get(j) == Some(complex1) { + rotate_slice(&mut result, 0, j + 1); + should_continue_to_outer = true; break; } - num_originals += 1; - result.push_front(complex1.clone()); + } + if should_continue_to_outer { 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; - } - + num_originals += 1; result.push_front(complex1.clone()); - } - if should_break_to_outer { continue; } - break; + + // The maximum specificity of the sources that caused `complex1` to be + // generated. In order for `complex1` to be removed, there must be another + // selector that's a superselector of it *and* that has specificity + // greater or equal to this. + let mut max_specificity = 0; + for component in &complex1.components { + if let ComplexSelectorComponent::Compound(compound) = component { + max_specificity = max_specificity.max(self.source_specificity_for(compound)) + } + } + + // Look in `result` rather than `selectors` for selectors after `i`. This + // ensures that we aren't comparing against a selector that's already been + // trimmed, and thus that if there are two identical selectors only one is + // trimmed. + let should_continue = result.iter().any(|complex2| { + complex2.min_specificity() >= max_specificity + && complex2.is_super_selector(complex1) + }); + if should_continue { + continue; + } + + let should_continue = selectors.iter().take(i).any(|complex2| { + complex2.min_specificity() >= max_specificity + && complex2.is_super_selector(complex1) + }); + if should_continue { + continue; + } + + result.push_front(complex1.clone()); } Vec::from(result) } + + /// Adds `selector` to this extender. + /// + /// Extends `selector` using any registered extensions, then returns the resulting + /// selector. If any more relevant extensions are added, the returned selector + /// is automatically updated. + /// + /// The `media_query_context` is the media query context in which the selector was + /// defined, or `null` if it was defined at the top level of the document. + // todo: the docs are wrong, and we may want to consider returning an `Rc>` + // the reason we don't is that it would interfere with hashing + pub fn add_selector( + &mut self, + mut selector: SelectorList, + // span: Span, + media_query_context: Option>, + ) -> SelectorList { + // todo: we should be able to remove this variable and clone + let original_selector = selector.clone(); + if !original_selector.is_invisible() { + for complex in &original_selector.components { + self.originals.insert(complex.clone()); + } + } + + if !self.extensions.is_empty() { + let extensions = self.extensions.clone(); + selector = self.extend_list(original_selector, &extensions, &media_query_context); + /* + todo: when we have error handling + } on SassException catch (error) { + throw SassException( + "From ${error.span.message('')}\n" + "${error.message}", + span); + } + */ + } + if let Some(mut media_query_context) = media_query_context { + self.media_contexts + .get_mut(&selector) + .replace(&mut media_query_context); + } + self.register_selector(selector.clone(), &selector); + selector + } + + /// Registers the `SimpleSelector`s in `list` to point to `selector` in + /// `self.selectors`. + fn register_selector(&mut self, list: SelectorList, selector: &SelectorList) { + for complex in list.components { + for component in complex.components { + if let ComplexSelectorComponent::Compound(component) = component { + for simple in component.components { + self.selectors + .entry(simple.clone()) + .or_insert_with(HashSet::new) + .insert(selector.clone()); + + if let SimpleSelector::Pseudo(Pseudo { + selector: Some(simple_selector), + .. + }) = simple + { + self.register_selector(simple_selector, selector); + } + } + } + } + } + } + + /// Adds an extension to this extender. + /// + /// The `extender` is the selector for the style rule in which the extension + /// is defined, and `target` is the selector passed to `@extend`. The `extend` + /// provides the extend span and indicates whether the extension is optional. + /// + /// The `media_context` defines the media query context in which the extension + /// is defined. It can only extend selectors within the same context. A `None` + /// context indicates no media queries. + pub fn add_extension( + &mut self, + extender: SelectorList, + target: &SimpleSelector, + extend: &ExtendRule, + media_context: &Option>, + span: Option, + ) { + let selectors = self.selectors.get(target).cloned(); + let existing_extensions = self.extensions_by_extender.get(target).cloned(); + + let mut new_extensions: Option> = None; + + let mut sources = self + .extensions + .entry(target.clone()) + .or_insert_with(IndexMap::new) + .clone(); + + for complex in extender.components { + let state = Extension { + specificity: complex.max_specificity(), + extender: complex.clone(), + target: Some(target.clone()), + span, + media_context: media_context.clone(), + is_optional: extend.is_optional, + is_original: false, + left: None, + right: None, + }; + + if let Some(existing_state) = sources.get(&complex) { + // If there's already an extend from `extender` to `target`, we don't need + // to re-run the extension. We may need to mark the extension as + // mandatory, though. + let mut new_val = MergedExtension::merge(existing_state.clone(), state).unwrap(); + sources.get_mut(&complex).replace(&mut new_val); + continue; + } + + sources.insert(complex.clone(), state.clone()); + + for component in complex.components.clone() { + if let ComplexSelectorComponent::Compound(component) = component { + for simple in component.components { + self.extensions_by_extender + .entry(simple.clone()) + .or_insert_with(Vec::new) + .push(state.clone()); + // Only source specificity for the original selector is relevant. + // Selectors generated by `@extend` don't get new specificity. + self.source_specificity + .entry(simple.clone()) + .or_insert_with(|| complex.max_specificity()); + } + } + } + + if selectors.is_some() || existing_extensions.is_some() { + new_extensions + .get_or_insert_with(IndexMap::new) + .insert(complex.clone(), state.clone()); + } + + let new_extensions = if let Some(new) = new_extensions.clone() { + new + } else { + // TODO: HACK: we extend by sources here, but we should be able to mutate sources directly + self.extensions + .get_mut(target) + .get_or_insert(&mut IndexMap::new()) + .extend(sources); + return; + }; + + let mut new_extensions_by_target = HashMap::new(); + new_extensions_by_target.insert(target.clone(), new_extensions); + + if let Some(existing_extensions) = existing_extensions.clone() { + let additional_extensions = + self.extend_existing_extensions(existing_extensions, &new_extensions_by_target); + if let Some(additional_extensions) = additional_extensions { + map_add_all_2(&mut new_extensions_by_target, additional_extensions); + } + } + + if let Some(selectors) = selectors.clone() { + self.extend_existing_selectors(selectors, &new_extensions_by_target); + } + } + + // TODO: HACK: we extend by sources here, but we should be able to mutate sources directly + self.extensions + .get_mut(target) + .get_or_insert(&mut IndexMap::new()) + .extend(sources); + } + + /// Extend `extensions` using `new_extensions`. + /// + /// Note that this does duplicate some work done by + /// `Extender::extend_existing_selectors`, but it's necessary to expand each extension's + /// extender separately without reference to the full selector list, so that + /// relevant results don't get trimmed too early. + /// + /// Returns extensions that should be added to `new_extensions` before + /// extending selectors in order to properly handle extension loops such as: + /// + /// .c {x: y; @extend .a} + /// .x.y.a {@extend .b} + /// .z.b {@extend .c} + /// + /// Returns `null` if there are no extensions to add. + fn extend_existing_extensions( + &mut self, + extensions: Vec, + new_extensions: &HashMap>, + ) -> Option>> { + let mut additional_extensions: Option< + HashMap>, + > = None; + for extension in extensions { + let mut sources = self + .extensions + .get(&extension.target.clone().unwrap()) + .unwrap() + .clone(); + + // `extend_existing_selectors` would have thrown already. + let selectors: Vec = if let Some(v) = self.extend_complex( + extension.extender.clone(), + new_extensions, + &extension.media_context, + ) { + v + } else { + continue; + }; + // todo: when we add error handling, this error is special + /* + } on SassException catch (error) { + throw SassException( + "From ${extension.extenderSpan.message('')}\n" + "${error.message}", + error.span); + } + */ + + let contains_extension = selectors.first() == Some(&extension.extender); + + let mut first = false; + for complex in selectors { + // If the output contains the original complex selector, there's no + // need to recreate it. + if contains_extension && first { + first = false; + continue; + } + + let with_extender = extension.clone().with_extender(complex.clone()); + let existing_extension = sources.get(&complex); + if let Some(existing_extension) = existing_extension.cloned() { + sources.get_mut(&complex).replace( + &mut MergedExtension::merge(existing_extension.clone(), with_extender) + .unwrap(), + ); + } else { + sources + .get_mut(&complex) + .replace(&mut with_extender.clone()); + + for component in complex.components.clone() { + if let ComplexSelectorComponent::Compound(component) = component { + for simple in component.components { + self.extensions_by_extender + .entry(simple) + .or_insert_with(Vec::new) + .push(with_extender.clone()); + } + } + } + + if new_extensions.contains_key(&extension.target.clone().unwrap()) { + additional_extensions + .get_or_insert_with(HashMap::new) + .entry(extension.target.clone().unwrap()) + .or_insert_with(IndexMap::new) + .insert(complex.clone(), with_extender.clone()); + } + } + } + // If `selectors` doesn't contain `extension.extender`, for example if it + // was replaced due to :not() expansion, we must get rid of the old + // version. + if !contains_extension { + sources.remove(&extension.extender); + } + } + additional_extensions + } + + /// Extend `extensions` using `new_extensions`. + fn extend_existing_selectors( + &mut self, + selectors: HashSet, + new_extensions: &HashMap>, + ) { + for mut selector in selectors { + let old_value = selector.clone(); + selector = self.extend_list( + old_value.clone(), + new_extensions, + &self.media_contexts.get(&selector).cloned(), + ); + /* + todo: error handling + } on SassException catch (error) { + throw SassException( + "From ${selector.span.message('')}\n" + "${error.message}", + error.span); + } + + */ + + // If no extends actually happened (for example becaues unification + // failed), we don't need to re-register the selector. + if old_value == selector { + continue; + } + self.register_selector(selector.clone(), &old_value); + } + } } /// Rotates the element in list from `start` (inclusive) to `end` (exclusive) @@ -798,3 +1149,22 @@ fn rotate_slice(list: &mut VecDeque, start: usize, end: usize) { element = next; } } + +/// Like `HashMap::extend`, but for two-layer maps. +/// +/// This avoids copying inner maps from `source` if possible. +fn map_add_all_2( + destination: &mut HashMap>, + source: HashMap>, +) { + source.into_iter().for_each(|(key, mut inner)| { + if destination.contains_key(&key) { + destination + .get_mut(&key) + .get_or_insert(&mut IndexMap::new()) + .extend(inner); + } else { + destination.get_mut(&key).replace(&mut inner); + } + }) +} diff --git a/src/selector/extend/rule.rs b/src/selector/extend/rule.rs new file mode 100644 index 0000000..8a1749e --- /dev/null +++ b/src/selector/extend/rule.rs @@ -0,0 +1,20 @@ +use codemap::Span; + +use crate::selector::Selector; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub(crate) struct ExtendRule { + pub selector: Selector, + pub is_optional: bool, + pub span: Span, +} + +impl ExtendRule { + pub const fn new(selector: Selector, is_optional: bool, span: Span) -> Self { + Self { + selector, + is_optional, + span, + } + } +} diff --git a/src/selector/simple.rs b/src/selector/simple.rs index 1430de2..ebecab1 100644 --- a/src/selector/simple.rs +++ b/src/selector/simple.rs @@ -314,7 +314,7 @@ impl SimpleSelector { }) = simple { // A given compound selector may only contain one pseudo element. If - // [compound] has a different one than [this], unification fails. + // `compound` has a different one than `self`, unification fails. if let Self::Pseudo(Pseudo { is_class: false, .. }) = self diff --git a/src/value/mod.rs b/src/value/mod.rs index 800de76..b9bbaf6 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -359,6 +359,7 @@ impl Value { in_control_flow: parser.in_control_flow, at_root: parser.at_root, at_root_has_selector: parser.at_root_has_selector, + extender: parser.extender, } .parse_selector(allows_parent, true, String::new()) } diff --git a/tests/extend.rs b/tests/extend.rs new file mode 100644 index 0000000..dc83da1 --- /dev/null +++ b/tests/extend.rs @@ -0,0 +1,1867 @@ +#![cfg(test)] + +#[macro_use] +mod macros; + +test!(empty_extend_self, "a { @extend a; }", ""); +test!( + extend_self_with_styles, + "a {\n color: red;\n @extend a;\n}\n", + "a {\n color: red;\n}\n" +); +test!( + list_extends_both_of_compound, + ".foo.bar { + a: b + } + + .x, .y { + @extend .foo, .bar; + } + ", + ".foo.bar, .x, .y {\n a: b;\n}\n" +); +test!( + class_extends_class_placed_second, + ".foo {a: b} + .bar {@extend .foo}", + ".foo, .bar {\n a: b;\n}\n" +); +test!( + class_extends_class_placed_first, + ".bar {@extend .foo} + .foo {a: b}", + ".foo, .bar {\n a: b;\n}\n" +); +test!( + class_extends_class_style_before_extend, + ".foo {a: b} + .bar {c: d; @extend .foo;}", + ".foo, .bar {\n a: b;\n}\n\n.bar {\n c: d;\n}\n" +); +test!( + class_extends_class_style_after_extend, + ".foo {a: b} + .bar {@extend .foo; c: d;}", + ".foo, .bar {\n a: b;\n}\n\n.bar {\n c: d;\n}\n" +); +test!( + class_extends_class_applies_to_multiple_extendees, + ".foo {a: b} + .bar {@extend .foo} + .blip .foo {c: d}", + ".foo, .bar {\n a: b;\n}\n\n.blip .foo, .blip .bar {\n c: d;\n}\n" +); +test!( + class_extends_class_one_class_extends_multiple, + ".foo {a: b} + .bar {c: d} + .baz {@extend .foo; @extend .bar}", + ".foo, .baz {\n a: b;\n}\n\n.bar, .baz {\n c: d;\n}\n" +); +test!( + #[ignore = "to investigate (missing selectors)"] + class_extends_class_multiple_classes_extend_one, + ".foo {a: b} + .bar {@extend .foo} + .baz {@extend .bar} + .bip {@extend .bar} + ", + ".foo, .bar, .bip, .baz {\n a: b;\n}\n" +); +test!( + #[ignore = "different order"] + class_extends_class_all_parts_of_complex_selector_extended_by_one, + ".foo .bar {a: b} + .baz {@extend .foo; @extend .bar} + ", + ".foo .bar, .foo .baz, .baz .bar, .baz .baz {\n a: b;\n}\n" +); +test!( + class_extends_class_all_parts_of_compound_selector_extended_by_one, + ".foo.bar {a: b} + .baz {@extend .foo; @extend .bar} + ", + ".foo.bar, .baz {\n a: b;\n}\n" +); +test!( + #[ignore = "different order"] + class_extends_class_all_parts_of_complex_selector_extended_by_different, + ".foo .bar {a: b} + .baz {@extend .foo} + .bang {@extend .bar} + ", + ".foo .bar, .foo .bang, .baz .bar, .baz .bang {\n a: b;\n}\n" +); +test!( + #[ignore = "different order"] + class_extends_class_all_parts_of_compound_selector_extended_by_different, + ".foo.bar {a: b} + .baz {@extend .foo} + .bang {@extend .bar} + ", + ".foo.bar, .foo.bang, .bar.baz, .baz.bang {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (missing selectors)"] + class_extends_class_simple_selector_extended_chain, + ".foo {a: b} + .bar {@extend .foo} + .baz {@extend .bar} + .bip {@extend .bar} + ", + ".foo, .bar, .bip, .baz {\n a: b;\n}\n" +); +test!( + class_extends_class_interpolated, + ".foo {a: b} + .bar {@extend #{\".foo\"}} + ", + ".foo, .bar {\n a: b;\n}\n" +); +test!( + class_extends_class_target_child_of_complex, + ".foo .bar {a: b} + .baz {@extend .bar} + ", + ".foo .bar, .foo .baz {\n a: b;\n}\n" +); +test!( + class_extends_class_target_parent_of_complex, + ".foo .bar {a: b} + .baz {@extend .foo} + ", + ".foo .bar, .baz .bar {\n a: b;\n}\n" +); +test!( + class_unification_1, + "%-a .foo.bar {a: b} + .baz {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a .bar.baz {\n a: b;\n}\n" +); +test!( + class_unification_2, + "%-a .foo.baz {a: b} + .baz {@extend .foo} -a {@extend %-a} + ", + "-a .baz {\n a: b;\n}\n" +); +test!( + id_unification_1, + "%-a .foo.bar {a: b} + #baz {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a .bar#baz {\n a: b;\n}\n" +); +test!( + id_unification_2, + "%-a .foo#baz {a: b} + #baz {@extend .foo} -a {@extend %-a} + ", + "-a #baz {\n a: b;\n}\n" +); +test!( + universal_unification_simple_target_1, + "%-a .foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a .foo, -a * {\n a: b;\n}\n" +); +test!( + universal_unification_simple_target_2, + "%-a .foo.bar {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a .bar {\n a: b;\n}\n" +); +test!( + universal_unification_simple_target_3, + "%-a .foo.bar {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a .bar {\n a: b;\n}\n" +); +test!( + universal_unification_simple_target_4, + "%-a .foo.bar {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a ns|*.bar {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_1, + "%-a *.foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a * {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_2, + "%-a *.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a * {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_3, + "%-a *|*.foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a *|*.foo, -a * {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_4, + "%-a *|*.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a *|* {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_5, + "%-a *.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a *.foo {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_6, + "%-a *|*.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a *|*.foo, -a ns|* {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_7, + "%-a ns|*.foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a ns|*.foo {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_8, + "%-a ns|*.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a ns|* {\n a: b;\n}\n" +); +test!( + universal_unification_universal_target_without_namespace_9, + "%-a ns|*.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a ns|* {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_1, + "%-a a.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a a {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_2, + "%-a *|a.foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a *|a.foo, -a a {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_3, + "%-a *|a.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a *|a {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_4, + "%-a a.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a a.foo {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_5, + "%-a *|a.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a *|a.foo, -a ns|a {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_6, + "%-a ns|a.foo {a: b} + * {@extend .foo} -a {@extend %-a} + ", + "-a ns|a.foo {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_7, + "%-a ns|a.foo {a: b} + *|* {@extend .foo} -a {@extend %-a} + ", + "-a ns|a {\n a: b;\n}\n" +); +test!( + universal_unification_element_target_without_namespace_8, + "%-a ns|a.foo {a: b} + ns|* {@extend .foo} -a {@extend %-a} + ", + "-a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_simple_target_1, + "%-a .foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a .foo, -a a {\n a: b;\n}\n" +); +test!( + element_unification_simple_target_2, + "%-a .foo.bar {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a a.bar {\n a: b;\n}\n" +); +test!( + element_unification_simple_target_3, + "%-a .foo.bar {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a *|a.bar {\n a: b;\n}\n" +); +test!( + element_unification_simple_target_4, + "%-a .foo.bar {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a .foo.bar, -a ns|a.bar {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_1, + "%-a *.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a *.foo, -a a {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_2, + "%-a *.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a *.foo, -a a {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_3, + "%-a *|*.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a *|*.foo, -a a {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_4, + "%-a *|*.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a *|*.foo, -a *|a {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_5, + "%-a *.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a *.foo {\n a: b;\n}\n" +); +test!( + element_unification_universal_without_namespace_6, + "%-a *|*.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a *|*.foo, -a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_universal_with_namespace_1, + "%-a ns|*.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a ns|*.foo {\n a: b;\n}\n" +); +test!( + element_unification_universal_with_namespace_2, + "%-a ns|*.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a ns|*.foo, -a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_universal_with_namespace_3, + "%-a ns|*.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a ns|*.foo, -a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_1, + "%-a a.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a a {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_2, + "%-a a.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a a {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_3, + "%-a *|a.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a *|a.foo, -a a {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_4, + "%-a *|a.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a *|a {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_5, + "%-a a.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a a.foo {\n a: b;\n}\n" +); +test!( + element_unification_element_without_namespace_6, + "%-a *|a.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a *|a.foo, -a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_element_with_namespace_1, + "%-a ns|a.foo {a: b} + a {@extend .foo} -a {@extend %-a} + ", + "-a ns|a.foo {\n a: b;\n}\n" +); +test!( + element_unification_element_with_namespace_2, + "%-a ns|a.foo {a: b} + *|a {@extend .foo} -a {@extend %-a} + ", + "-a ns|a {\n a: b;\n}\n" +); +test!( + element_unification_element_with_namespace_3, + "%-a ns|a.foo {a: b} + ns|a {@extend .foo} -a {@extend %-a} + ", + "-a ns|a {\n a: b;\n}\n" +); +test!( + attribute_unification_1, + "%-a [foo=bar].baz {a: b} + [foo=baz] {@extend .baz} -a {@extend %-a} + ", + "-a [foo=bar].baz, -a [foo=bar][foo=baz] {\n a: b;\n}\n" +); +test!( + attribute_unification_2, + "%-a [foo=bar].baz {a: b} + [foo^=bar] {@extend .baz} -a {@extend %-a} + ", + "-a [foo=bar].baz, -a [foo=bar][foo^=bar] {\n a: b;\n}\n" +); +test!( + attribute_unification_3, + "%-a [foo=bar].baz {a: b} + [foot=bar] {@extend .baz} -a {@extend %-a} + ", + "-a [foo=bar].baz, -a [foo=bar][foot=bar] {\n a: b;\n}\n" +); +test!( + attribute_unification_4, + "%-a [foo=bar].baz {a: b} + [ns|foo=bar] {@extend .baz} -a {@extend %-a} + ", + "-a [foo=bar].baz, -a [foo=bar][ns|foo=bar] {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (too many selectors)"] + attribute_unification_5, + "%-a %-a [foo=bar].bar {a: b} + [foo=bar] {@extend .bar} -a {@extend %-a} + ", + "-a -a [foo=bar] {\n a: b;\n}\n" +); +test!( + pseudo_unification_1, + "%-a :foo.baz {a: b} + :foo(2n+1) {@extend .baz} -a {@extend %-a} + ", + "-a :foo.baz, -a :foo:foo(2n+1) {\n a: b;\n}\n" +); +test!( + pseudo_unification_2, + "%-a :foo.baz {a: b} + ::foo {@extend .baz} -a {@extend %-a} + ", + "-a :foo.baz, -a :foo::foo {\n a: b;\n}\n" +); +test!( + pseudo_unification_3, + "%-a ::foo.baz {a: b} + ::foo {@extend .baz} -a {@extend %-a} + ", + "-a ::foo {\n a: b;\n}\n" +); +test!( + pseudo_unification_4, + "%-a ::foo(2n+1).baz {a: b} + ::foo(2n+1) {@extend .baz} -a {@extend %-a} + ", + "-a ::foo(2n+1) {\n a: b;\n}\n" +); +test!( + pseudo_unification_5, + "%-a :foo.baz {a: b} + :bar {@extend .baz} -a {@extend %-a} + ", + "-a :foo.baz, -a :foo:bar {\n a: b;\n}\n" +); +test!( + pseudo_unification_6, + "%-a .baz:foo {a: b} + :after {@extend .baz} -a {@extend %-a} + ", + "-a .baz:foo, -a :foo:after {\n a: b;\n}\n" +); +test!( + pseudo_unification_7, + "%-a .baz:after {a: b} + :foo {@extend .baz} -a {@extend %-a} + ", + "-a .baz:after, -a :foo:after {\n a: b;\n}\n" +); +test!( + pseudo_unification_8, + "%-a :foo.baz {a: b} + :foo {@extend .baz} -a {@extend %-a} + ", + "-a :foo {\n a: b;\n}\n" +); +test!( + pseudoelement_remains_at_end_of_selector_1, + ".foo::bar {a: b} + .baz {@extend .foo} + ", + ".foo::bar, .baz::bar {\n a: b;\n}\n" +); +test!( + pseudoelement_remains_at_end_of_selector_2, + "a.foo::bar {a: b} + .baz {@extend .foo} + ", + "a.foo::bar, a.baz::bar {\n a: b;\n}\n" +); +test!( + pseudoclass_remains_at_end_of_selector_1, + ".foo:bar {a: b} + .baz {@extend .foo} + ", + ".foo:bar, .baz:bar {\n a: b;\n}\n" +); +test!( + pseudoclass_remains_at_end_of_selector_2, + "a.foo:bar {a: b} + .baz {@extend .foo} + ", + "a.foo:bar, a.baz:bar {\n a: b;\n}\n" +); +test!( + pseudoclass_not_remains_at_end_of_selector, + ".foo:not(.bar) {a: b} + .baz {@extend .foo} + ", + ".foo:not(.bar), .baz:not(.bar) {\n a: b;\n}\n" +); +test!( + pseudoelement_goes_lefter_than_pseudoclass_1, + ".foo::bar {a: b} + .baz:bang {@extend .foo} + ", + ".foo::bar, .baz:bang::bar {\n a: b;\n}\n" +); +test!( + pseudoelement_goes_lefter_than_pseudoclass_2, + ".foo:bar {a: b} + .baz::bang {@extend .foo} + ", + ".foo:bar, .baz:bar::bang {\n a: b;\n}\n" +); +test!( + pseudoelement_goes_lefter_than_not_1, + ".foo::bar {a: b} + .baz:not(.bang) {@extend .foo} + ", + ".foo::bar, .baz:not(.bang)::bar {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (parsing failure)"] + pseudoelement_goes_lefter_than_not_2, + "%a { + x:y; + } + b:after:not(:first-child) { + @extend %a; + } + c:s { + @extend %a; + } + d::e { + @extend c; + } + ", + "c:s, d:s::e, b:after:not(:first-child) {\n a: b;\n}\n" +); +test!( + pseudoelement_goes_lefter_than_not_3, + ".foo:not(.bang) {a: b} + .baz::bar {@extend .foo} + ", + ".foo:not(.bang), .baz:not(.bang)::bar {\n a: b;\n}\n" +); +test!( + negation_unification_1, + "%-a :not(.foo).baz {a: b} + :not(.bar) {@extend .baz} -a {@extend %-a} + ", + "-a :not(.foo).baz, -a :not(.foo):not(.bar) {\n a: b;\n}\n" +); +test!( + negation_unification_2, + "%-a :not(.foo).baz {a: b} + :not(.foo) {@extend .baz} -a {@extend %-a} + ", + "-a :not(.foo) {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (too many selectors)"] + negation_unification_3, + "%-a :not([a=b]).baz {a: b} + :not([a = b]) {@extend .baz} -a {@extend %-a} + ", + "-a :not([a=b]) {\n a: b;\n}\n" +); +test!( + comma_extendee, + ".foo {a: b} + .bar {c: d} + .baz {@extend .foo, .bar} + ", + ".foo, .baz {\n a: b;\n}\n\n.bar, .baz {\n c: d;\n}\n" +); +test!( + #[ignore = "different order"] + redundant_selector_elimination, + ".foo.bar {a: b} + .x {@extend .foo, .bar} + .y {@extend .foo, .bar} + ", + ".foo.bar, .y, .x {\n a: b;\n}\n" +); +error!( + extend_compound_selector, + "ns|*.foo.bar {a: b} + a.baz {@extend .foo.bar} + ", + "Error: compound selectors may no longer be extended." +); +test!( + compound_extender, + ".foo.bar {a: b} + .baz.bang {@extend .foo} + ", + ".foo.bar, .bar.baz.bang {\n a: b;\n}\n" +); +test!( + compound_extender_unification, + "ns|*.foo.bar {a: b} + a.baz {@extend .foo} + ", + "ns|*.foo.bar {\n a: b;\n}\n" +); +test!( + complex_extender, + ".foo {a: b} + foo bar {@extend .foo} + ", + ".foo, foo bar {\n a: b;\n}\n" +); +test!( + complex_extender_unification, + ".foo.bar {a: b} + foo bar {@extend .foo} + ", + ".foo.bar, foo bar.bar {\n a: b;\n}\n" +); +test!( + complex_extender_alternates_parents, + ".baz .bip .foo {a: b} + foo .grank bar {@extend .foo} + ", + ".baz .bip .foo, .baz .bip foo .grank bar, foo .grank .baz .bip bar {\n a: b;\n}\n" +); +test!( + complex_extender_unifies_identical_parents, + ".baz .bip .foo {a: b} + .baz .bip bar {@extend .foo} + ", + ".baz .bip .foo, .baz .bip bar {\n a: b;\n}\n" +); +test!( + complex_extender_unifies_common_substring, + ".baz .bip .bap .bink .foo {a: b} + .brat .bip .bap bar {@extend .foo} + ", + ".baz .bip .bap .bink .foo, .baz .brat .bip .bap .bink bar, .brat .baz .bip .bap .bink bar {\n a: b;\n}\n" +); +test!( + complex_extender_unifies_common_subsequence, + ".a .x .b .y .foo {a: b} + .a .n .b .m bar {@extend .foo} + ", + ".a .x .b .y .foo, .a .x .n .b .y .m bar, .a .n .x .b .y .m bar, .a .x .n .b .m .y bar, .a .n .x .b .m .y bar {\n a: b;\n}\n" +); +test!( + complex_extender_chooses_first_subsequence, + ".a .b .c .d .foo {a: b} + .c .d .a .b .bar {@extend .foo} + ", + ".a .b .c .d .foo, .a .b .c .d .a .b .bar {\n a: b;\n}\n" +); +test!( + complex_extender_counts_extended_superselectors, + ".a .bip .foo {a: b} + .b .bip.bop .bar {@extend .foo} + ", + ".a .bip .foo, .a .b .bip.bop .bar, .b .a .bip.bop .bar {\n a: b;\n}\n" +); +test!( + complex_extender_child_combinator, + ".baz .foo {a: b} + foo > bar {@extend .foo} + ", + ".baz .foo, .baz foo > bar {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_child_combinator_1, + "a > b c .c1 {a: b} + a c .c2 {@extend .c1} + ", + "a > b c .c1, a > b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_child_combinator_2, + "a > b c .c1 {a: b} + b c .c2 {@extend .c1} + ", + "a > b c .c1, a > b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_adjacent_sibling_combinator_1, + "a + b c .c1 {a: b} + a c .c2 {@extend .c1} + ", + "a + b c .c1, a + b a c .c2, a a + b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_adjacent_sibling_combinator_2, + "a + b c .c1 {a: b} + a b .c2 {@extend .c1} + ", + "a + b c .c1, a a + b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_adjacent_sibling_combinator_3, + "a + b c .c1 {a: b} + b c .c2 {@extend .c1} + ", + "a + b c .c1, a + b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_sibling_combinator_1, + "a ~ b c .c1 {a: b} + a c .c2 {@extend .c1} + ", + "a ~ b c .c1, a ~ b a c .c2, a a ~ b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_sibling_combinator_2, + "a ~ b c .c1 {a: b} + a b .c2 {@extend .c1} + ", + "a ~ b c .c1, a a ~ b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_finds_common_selectors_around_sibling_combinator_3, + "a ~ b c .c1 {a: b} + b c .c2 {@extend .c1} + ", + "a ~ b c .c1, a ~ b c .c2 {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selectors_doesnt_subsequence_them_1, + ".bip > .bap .foo {a: b} + .grip > .bap .bar {@extend .foo} + ", + ".bip > .bap .foo, .bip > .bap .grip > .bap .bar, .grip > .bap .bip > .bap .bar {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selectors_doesnt_subsequence_them_2, + ".bap > .bip .foo {a: b} + .bap > .grip .bar {@extend .foo} + ", + ".bap > .bip .foo, .bap > .bip .bap > .grip .bar, .bap > .grip .bap > .bip .bar {\n a: b;\n}\n" +); +test!( + complex_extender_with_child_selector_unifies_1, + ".baz.foo {a: b} + foo > bar {@extend .foo} + ", + ".baz.foo, foo > bar.baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_child_selector_unifies_2, + ".baz > { + .foo {a: b} + .bar {@extend .foo} + } + ", + ".baz > .foo, .baz > .bar {\n a: b;\n}\n" +); +test!( + complex_extender_with_child_selector_unifies_3, + ".foo { + .bar {a: b} + > .baz {@extend .bar} + } + ", + ".foo .bar, .foo > .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selector_1, + ".foo { + .bar {a: b} + .bip > .baz {@extend .bar} + } + ", + ".foo .bar, .foo .bip > .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selector_2, + ".foo { + .bip .bar {a: b} + > .baz {@extend .bar} + } + ", + ".foo .bip .bar, .foo .bip .foo > .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selector_3, + ".foo > .bar {a: b} + .bip + .baz {@extend .bar} + ", + ".foo > .bar, .foo > .bip + .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selector_4, + ".foo + .bar {a: b} + .bip > .baz {@extend .bar} + ", + ".foo + .bar, .bip > .foo + .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_early_child_selector_5, + ".foo > .bar {a: b} + .bip > .baz {@extend .bar} + ", + ".foo > .bar, .bip.foo > .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_sibling_selector, + ".baz .foo {a: b} + foo + bar {@extend .foo} + ", + ".baz .foo, .baz foo + bar {\n a: b;\n}\n" +); +test!( + complex_extender_with_hacky_selector_1, + ".baz .foo {a: b} + foo + > > + bar {@extend .foo} + ", + ".baz .foo, .baz foo + > > + bar, foo .baz + > > + bar {\n a: b;\n}\n" +); +test!( + complex_extender_with_hacky_selector_2, + ".baz .foo {a: b} + > > bar {@extend .foo} + ", + ".baz .foo, > > .baz bar {\n a: b;\n}\n" +); +test!( + complex_extender_merges_with_the_same_selector, + ".foo { + .bar {a: b} + .baz {@extend .bar} + } + ", + ".foo .bar, .foo .baz {\n a: b;\n}\n" +); +test!( + complex_extender_with_child_selector_merges_with_the_same_selector, + ".foo > .bar .baz {a: b} + .foo > .bar .bang {@extend .baz} + ", + ".foo > .bar .baz, .foo > .bar .bang {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_1, + ".a > + x {a: b} + .b y {@extend x} + ", + ".a > + x, .a .b > + y, .b .a > + y {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_2, + ".a x {a: b} + .b > + y {@extend x} + ", + ".a x, .a .b > + y, .b .a > + y {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_3, + ".a > + x {a: b} + .b > + y {@extend x} + ", + ".a > + x, .a .b > + y, .b .a > + y {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_4, + ".a ~ > + x {a: b} + .b > + y {@extend x} + ", + ".a ~ > + x, .a .b ~ > + y, .b .a ~ > + y {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_5, + ".a + > x {a: b} + .b > + y {@extend x} + ", + ".a + > x {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_6, + ".a + > x {a: b} + .b > + y {@extend x} + ", + ".a + > x {\n a: b;\n}\n" +); +test!( + combinator_unification_for_hacky_combinators_7, + ".a ~ > + .b > x {a: b} + .c > + .d > y {@extend x} + ", + ".a ~ > + .b > x, .a .c ~ > + .d.b > y, .c .a ~ > + .d.b > y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_tilde_1, + ".a.b ~ x {a: b} + .a ~ y {@extend x} + ", + ".a.b ~ x, .a.b ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_tilde_2, + ".a ~ x {a: b} + .a.b ~ y {@extend x} + ", + ".a ~ x, .a.b ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_tilde_3, + ".a ~ x {a: b} + .b ~ y {@extend x} + ", + ".a ~ x, .a ~ .b ~ y, .b ~ .a ~ y, .b.a ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_tilde_4, + "a.a ~ x {a: b} + b.b ~ y {@extend x} + ", + "a.a ~ x, a.a ~ b.b ~ y, b.b ~ a.a ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_1, + ".a.b + x {a: b} + .a ~ y {@extend x} + ", + ".a.b + x, .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_2, + ".a + x {a: b} + .a.b ~ y {@extend x} + ", + ".a + x, .a.b ~ .a + y, .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_3, + ".a + x {a: b} + .b ~ y {@extend x} + ", + ".a + x, .b ~ .a + y, .b.a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_4, + "a.a + x {a: b} + b.b ~ y {@extend x} + ", + "a.a + x, b.b ~ a.a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_5, + ".a.b ~ x {a: b} + .a + y {@extend x} + ", + ".a.b ~ x, .a.b ~ .a + y, .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_6, + ".a ~ x {a: b} + .a.b + y {@extend x} + ", + ".a ~ x, .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_tilde_plus_7, + ".a ~ x {a: b} + .b + y {@extend x} + ", + ".a ~ x, .a ~ .b + y, .b.a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_sibling_1, + ".a > x {a: b} + .b ~ y {@extend x} + ", + ".a > x, .a > .b ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_sibling_2, + ".a > x {a: b} + .b + y {@extend x} + ", + ".a > x, .a > .b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_sibling_3, + ".a ~ x {a: b} + .b > y {@extend x} + ", + ".a ~ x, .b > .a ~ y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_sibling_4, + ".a + x {a: b} + .b > y {@extend x} + ", + ".a + x, .b > .a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_angle_1, + ".a.b > x {a: b} + .b > y {@extend x} + ", + ".a.b > x, .b.a > y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_angle_2, + ".a > x {a: b} + .a.b > y {@extend x} + ", + ".a > x, .a.b > y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_angle_3, + ".a > x {a: b} + .b > y {@extend x} + ", + ".a > x, .b.a > y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_angle_4, + "a.a > x {a: b} + b.b > y {@extend x} + ", + "a.a > x {\n a: b;\n}\n" +); +test!( + combinator_unification_double_plus_1, + ".a.b + x {a: b} + .b + y {@extend x} + ", + ".a.b + x, .b.a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_plus_2, + ".a + x {a: b} + .a.b + y {@extend x} + ", + ".a + x, .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_plus_3, + ".a + x {a: b} + .b + y {@extend x} + ", + ".a + x, .b.a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_double_plus_4, + "a.a + x {a: b} + b.b + y {@extend x} + ", + "a.a + x {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_1, + ".a.b > x {a: b} + .a y {@extend x} + ", + ".a.b > x, .a.b > y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_2, + ".a > x {a: b} + .a.b y {@extend x} + ", + ".a > x, .a.b .a > y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_3, + ".a > x {a: b} + .b y {@extend x} + ", + ".a > x, .b .a > y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_4, + ".a.b x {a: b} + .a > y {@extend x} + ", + ".a.b x, .a.b .a > y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_5, + ".a x {a: b} + .a.b > y {@extend x} + ", + ".a x, .a.b > y {\n a: b;\n}\n" +); +test!( + combinator_unification_angle_space_6, + ".a x {a: b} + .b > y {@extend x} + ", + ".a x, .a .b > y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_1, + ".a.b + x {a: b} + .a y {@extend x} + ", + ".a.b + x, .a .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_2, + ".a + x {a: b} + .a.b y {@extend x} + ", + ".a + x, .a.b .a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_3, + ".a + x {a: b} + .b y {@extend x} + ", + ".a + x, .b .a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_4, + ".a.b x {a: b} + .a + y {@extend x} + ", + ".a.b x, .a.b .a + y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_5, + ".a x {a: b} + .a.b + y {@extend x} + ", + ".a x, .a .a.b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_plus_space_6, + ".a x {a: b} + .b + y {@extend x} + ", + ".a x, .a .b + y {\n a: b;\n}\n" +); +test!( + nested_combinator_unification_1, + ".a > .b + x {a: b} + .c > .d + y {@extend x} + ", + ".a > .b + x, .c.a > .d.b + y {\n a: b;\n}\n" +); +test!( + nested_combinator_unification_2, + ".a > .b + x {a: b} + .c > y {@extend x} + ", + ".a > .b + x, .c.a > .b + y {\n a: b;\n}\n" +); +test!( + combinator_unification_with_newlines, + ".a >\n.b\n+ x {a: b}\n.c\n> .d +\ny {@extend x}\n", + ".a > .b + x, .c.a > .d.b + y {\n a: b;\n}\n" +); +test!( + basic_extend_loop, + ".foo {a: b; @extend .bar} + .bar {c: d; @extend .foo} + ", + ".foo, .bar {\n a: b;\n}\n\n.bar, .foo {\n c: d;\n}\n" +); +test!( + three_level_extend_loop, + ".foo {a: b; @extend .bar} + .bar {c: d; @extend .baz} + .baz {e: f; @extend .foo} + ", + ".foo, .baz, .bar {\n a: b;\n}\n\n.bar, .foo, .baz {\n c: d;\n}\n\n.baz, .bar, .foo {\n e: f;\n}\n" +); +test!( + nested_extend_loop, + ".bar { + a: b; + .foo {c: d; @extend .bar} + } + ", + ".bar, .bar .foo {\n a: b;\n}\n.bar .foo {\n c: d;\n}\n" +); +test!( + multiple_extender_merges_with_superset_selector, + ".foo {@extend .bar; @extend .baz} + a.bar.baz {a: b} + ", + "a.bar.baz, a.foo {\n a: b;\n}\n" +); +test!( + inside_control_flow_if, + ".true { color: green; } + .false { color: red; } + .also-true { + @if true { @extend .true; } + @else { @extend .false; } + } + .also-false { + @if false { @extend .true; } + @else { @extend .false; } + } + ", + ".true, .also-true {\n color: green;\n}\n\n.false, .also-false {\n color: red;\n}\n" +); +test!( + inside_control_flow_for, + " + .base-0 { color: green; } + .base-1 { display: block; } + .base-2 { border: 1px solid blue; } + .added { + @for $i from 0 to 3 { + @extend .base-#{$i}; + } + } + ", + ".base-0, .added {\n color: green;\n}\n\n.base-1, .added {\n display: block;\n}\n\n.base-2, .added {\n border: 1px solid blue;\n}\n" +); +test!( + inside_control_flow_while, + " + .base-0 { color: green; } + .base-1 { display: block; } + .base-2 { border: 1px solid blue; } + .added { + $i : 0; + @while $i < 3 { + @extend .base-#{$i}; + $i : $i + 1; + } + } + ", + ".base-0, .added {\n color: green;\n}\n\n.base-1, .added {\n display: block;\n}\n\n.base-2, .added {\n border: 1px solid blue;\n}\n" +); +test!( + basic_placeholder, + "%foo {a: b} + .bar {@extend %foo} + ", + ".bar {\n a: b;\n}\n" +); +test!( + unused_placeholder, + "%foo {a: b} + %bar {a: b} + .baz {@extend %foo} + ", + ".baz {\n a: b;\n}\n" +); +test!( + placeholder_descendant, + "#context %foo a {a: b} + .bar {@extend %foo} + ", + "#context .bar a {\n a: b;\n}\n" +); +test!( + semi_placeholder, + "#context %foo, .bar .baz {a: b} + + .bat { + @extend %foo; + } + ", + "#context .bat, .bar .baz {\n a: b;\n}\n" +); +test!( + #[ignore = "different order"] + placeholder_with_multiple_extenders, + "%foo {a: b} + .bar {@extend %foo} + .baz {@extend %foo} + ", + ".baz, .bar {\n a: b;\n}\n" +); +test!( + placeholder_interpolation, + "$foo: foo; + + %#{$foo} {a: b} + .bar {@extend %foo} + ", + ".bar {\n a: b;\n}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + media_inside_placeholder, + "%foo {bar {@media screen {a {b: c}}}} + .baz {c: d} + ", + ".baz {\n c: d;\n}\n" +); +test!( + extend_within_media, + "@media screen { + .foo {a: b} + .bar {@extend .foo} + } + ", + "@media screen {\n .foo, .bar {\n a: b;\n }\n}\n" +); +test!( + extend_within_unknown_at_rule, + "@unknown { + .foo {a: b} + .bar {@extend .foo} + } + ", + "@unknown {\n .foo, .bar {\n a: b;\n }\n}\n" +); +test!( + extend_within_nested_at_rules, + "@media screen { + @unknown { + .foo {a: b} + .bar {@extend .foo} + } + } + ", + "@media screen {\n @unknown {\n .foo, .bar {\n a: b;\n }\n }\n}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + extend_within_separate_media_queries, + "@media screen {.foo {a: b}} + @media screen {.bar {@extend .foo}} + ", + "@media screen {\n .foo, .bar {\n a: b;\n }\n}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + extend_within_separate_unknown_at_rules, + "@unknown {.foo {a: b}} + @unknown {.bar {@extend .foo}} + ", + "@unknown {\n .foo, .bar {\n a: b;\n }\n}\n@unknown {}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + extend_within_separate_nested_at_rules, + "@media screen {@flooblehoof {.foo {a: b}}} + @media screen {@flooblehoof {.bar {@extend .foo}}} + ", + "@media screen {\n @flooblehoof {\n .foo, .bar {\n a: b;\n }\n }\n}\n@media screen {\n @flooblehoof {}\n}\n" +); +test!( + extend_succeeds_when_one_extend_fails_but_others_dont, + "a.bar {a: b} + .bar {c: d} + b.foo {@extend .bar} + ", + "a.bar {\n a: b;\n}\n\n.bar, b.foo {\n c: d;\n}\n" +); +test!( + #[ignore = "!optional extend is not yet implemented"] + optional_extend_succeeds_when_extendee_doesnt_exist, + ".foo {@extend .bar !optional}", + "" +); +test!( + #[ignore = "!optional extend is not yet implemented"] + optional_extend_succeeds_when_extension_fails, + "a.bar {a: b} + b.foo {@extend .bar !optional} + ", + "a.bar {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_1, + "%x#bar {a: b} // Add an id to make the results have high specificity + %y, %y::fblthp {@extend %x} + a {@extend %y} + ", + "a#bar, a#bar::fblthp {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_2, + "%x#bar {a: b} + %y, %y:fblthp {@extend %x} + a {@extend %y} + ", + "a#bar {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_3, + "%x#bar {a: b} + %y, %y:first-line {@extend %x} + a {@extend %y} + ", + "a#bar, a#bar:first-line {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_4, + "%x#bar {a: b} + %y, %y:first-letter {@extend %x} + a {@extend %y} + ", + "a#bar, a#bar:first-letter {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_5, + "%x#bar {a: b} + %y, %y:before {@extend %x} + a {@extend %y} + ", + "a#bar, a#bar:before {\n a: b;\n}\n" +); +test!( + #[ignore = "@extend chains do not yet work"] + psuedo_element_superselector_6, + "%x#bar {a: b} + %y, %y:after {@extend %x} + a {@extend %y} + ", + "a#bar, a#bar:after {\n a: b;\n}\n" +); +test!( + #[ignore = "super selectors shouldn't be resolved lazily"] + multiple_source_redundancy_elimination, + "%default-color {color: red} + %alt-color {color: green} + + %default-style { + @extend %default-color; + &:hover {@extend %alt-color} + &:active {@extend %default-color} + } + + .test-case {@extend %default-style} + ", + ".test-case:active, .test-case {\n color: red;\n}\n\n.test-case:hover {\n color: green;\n}\n" +); +test!( + nested_sibling_extend, + ".foo {@extend .bar} + + .parent { + .bar { + a: b; + } + .foo { + @extend .bar + } + } + ", + ".parent .bar, .parent .foo {\n a: b;\n}\n" +); +test!( + parent_and_sibling_extend, + "%foo %bar%baz {a: b} + + .parent1 { + @extend %foo; + .child1 {@extend %bar} + } + + .parent2 { + @extend %foo; + .child2 {@extend %baz} + } + ", + ".parent1 .parent2 .child1.child2, .parent2 .parent1 .child1.child2 {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (parsing failure)"] + nested_extend_specificity, + "%foo {a: b} + + a { + :b {@extend %foo} + :b:c {@extend %foo} + } + ", + "a :b:c, a :b {\n a: b;\n}\n" +); +test!( + double_extend_optimization, + "%foo %bar { + a: b; + } + + .parent1 { + @extend %foo; + + .child { + @extend %bar; + } + } + + .parent2 { + @extend %foo; + } + ", + ".parent1 .child {\n a: b;\n}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + extend_inside_double_nested_media, + "@media all { + @media (orientation: landscape) { + %foo {color: blue} + .bar {@extend %foo} + } + } + ", + "@media (orientation: landscape) {\n .bar {\n color: blue;\n }\n}\n" +); +test!( + partially_failed_extend, + "test { @extend .rc; } + .rc {color: white;} + .prices span.pill span.rc {color: red;} + ", + ".rc, test {\n color: white;\n}\n\n.prices span.pill span.rc {\n color: red;\n}\n" +); +test!( + newline_near_combinator, + ".a + + .b x {a: b} + .c y {@extend x} + ", + ".a + .b x, .a + .b .c y, .c .a + .b y {\n a: b;\n}\n" +); +test!( + duplicated_selector_with_newlines, + ".example-1-1, + .example-1-2, + .example-1-3 { + a: b; + } + + .my-page-1 .my-module-1-1 {@extend .example-1-2} + ", + ".example-1-1,\n.example-1-2,\n.my-page-1 .my-module-1-1,\n.example-1-3 {\n a: b;\n}\n" +); +test!( + nested_selector_with_child_selector_hack_extendee, + "> .foo {a: b} + foo bar {@extend .foo} + ", + "> .foo, > foo bar {\n a: b;\n}\n" +); +test!( + nested_selector_with_child_selector_hack_extender, + ".foo .bar {a: b} + > foo bar {@extend .bar} + ", + ".foo .bar, > .foo foo bar, > foo .foo bar {\n a: b;\n}\n" +); +test!( + nested_selector_with_child_selector_hack_extender_and_extendee, + "> .foo {a: b} + > foo bar {@extend .foo} + ", + "> .foo, > foo bar {\n a: b;\n}\n" +); +test!( + nested_selector_with_child_selector_hack_extender_and_sibling_extendee, + "~ .foo {a: b} + > foo bar {@extend .foo} + ", + "~ .foo {\n a: b;\n}\n" +); +test!( + nested_selector_with_child_selector_hack_extender_and_extendee_newline, + "> .foo {a: b}\nflip,\n> foo bar {@extend .foo}\n", + "> .foo, > flip,\n> foo bar {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (missing selectors)"] + extended_parent_and_child_redundancy_elimination, + "a { + b {a: b} + c {@extend b} + } + d {@extend a} + ", + "a b, d b, a c, d c {\n a: b;\n}\n" +); +test!( + redundancy_elimination_when_it_would_reduce_specificity, + "a {a: b} + a.foo {@extend a} + ", + "a, a.foo {\n a: b;\n}\n" +); +test!( + redundancy_elimination_when_it_would_preserve_specificity, + ".bar a {a: b} + a.foo {@extend a} + ", + ".bar a {\n a: b;\n}\n" +); +test!( + redundancy_elimination_never_eliminates_base_selector, + "a.foo {a: b} + .foo {@extend a} + ", + "a.foo, .foo {\n a: b;\n}\n" +); +test!( + cross_branch_redundancy_elimination_1, + "%x .c %y {a: b} + .a, .b {@extend %x} + .a .d {@extend %y} + ", + ".a .c .d, .b .c .a .d {\n a: b;\n}\n" +); +test!( + #[ignore = "to investigate (missing selectors)"] + cross_branch_redundancy_elimination_2, + ".e %z {a: b} + %x .c %y {@extend %z} + .a, .b {@extend %x} + .a .d {@extend %y} + ", + ".e .a .c .d, .e .b .c .a .d, .a .e .b .c .d, .a .c .e .d, .b .c .e .a .d {\n a: b;\n}\n" +); +test!( + extend_with_universal_selector, + "%-a *.foo1 {a: b} + a {@extend .foo1} + -a {@extend %-a} + + %-b *|*.foo2 {b: b} + b {@extend .foo2} + -b {@extend %-b} + ", + "-a *.foo1, -a a {\n a: b;\n}\n\n-b *|*.foo2, -b b {\n b: b;\n}\n" +); +test!( + extend_with_universal_selector_empty_namespace, + "%-a |*.foo {a: b} + a {@extend .foo} + -a {@extend %-a} + ", + "-a |*.foo {\n a: b;\n}\n" +); +test!( + extend_with_universal_selector_different_namespace, + "%-a ns|*.foo {a: b} + a {@extend .foo} + -a {@extend %-a} + ", + "-a ns|*.foo {\n a: b;\n}\n" +); +test!( + unify_root_pseudo_element, + "// We assume that by default classes don't apply to the :root unless marked explicitly. + :root .foo-1 { test: 1; } + .bar-1 .baz-1 { @extend .foo-1; } + + // We know the two classes must be the same :root element so we can combine them. + .foo-2:root .bar-2 { test: 2; } + .baz-2:root .bang-2 { @extend .bar-2; } + + // This extend should not apply because the :root elements are different. + html:root .bar-3 { test: 3; } + xml:root .bang-3 { @extend .bar-3} + + // We assume that direct descendant of the :root is not the same element as a descendant. + .foo-4:root > .bar-4 .x-4 { test: 4; } + .baz-4:root .bang-4 .y-4 {@extend .x-4} + ", + ":root .foo-1, :root .bar-1 .baz-1 {\n test: 1;\n}\n\n.foo-2:root .bar-2, .baz-2.foo-2:root .bang-2 {\n test: 2;\n}\n\nhtml:root .bar-3 {\n test: 3;\n}\n\n.foo-4:root > .bar-4 .x-4, .baz-4.foo-4:root > .bar-4 .bang-4 .y-4 {\n test: 4;\n}\n" +); +test!( + #[ignore = "to investigate (too many selectors)"] + compound_unification_in_not, + "// Make sure compound selectors are unified when two :not()s are extended. + // :not() is special here because it's the only selector that's extended by + // adding to the compound selector, rather than creating a new selector list. + .a {@extend .c} + .b {@extend .d} + :not(.c):not(.d) {a: b} + ", + ":not(.c):not(.a):not(.d):not(.b) {\n a: b;\n}\n" +); +test!( + #[ignore = "media queries are not yet parsed correctly"] + does_not_move_page_block_in_media, + "@media screen { + a { x:y; } + @page {} + } + ", + "@media screen {\n a {\n x: y;\n }\n\n @page {}\n}\n" +); +test!( + escaped_selector, + "// Escapes in selectors' identifiers should be normalized before `@extend` is + // applied. + .foo {escape: none} + \\.foo {escape: slash dot} + \\2E foo {escape: hex} + + .bar {@extend \\02e foo} + ", + ".foo {\n escape: none;\n}\n\n\\.foo, .bar {\n escape: slash dot;\n}\n\n\\.foo, .bar {\n escape: hex;\n}\n" +); +test!( + extend_extender, + "// For implementations like Dart Sass that process extensions as they occur, + // extending rules that contain their own extends needs special handling. + .b {@extend .a} + .c {@extend .b} + .a {x: y} + ", + ".a, .b, .c {\n x: y;\n}\n" +); +test!( + #[ignore = "to investigate (too many selectors)"] + extend_result_of_extend, + "// The result of :not(.c) being extended should itself be extendable. + .a {@extend :not(.b)} + .b {@extend .c} + :not(.c) {x: y} + ", + ":not(.c):not(.b), .a:not(.c) {\n x: y;\n}\n" +); +test!( + extend_self, + "// This shouldn't change the selector. + .c, .a .b .c, .a .c .b {x: y; @extend .c} + ", + ".c, .a .b .c, .a .c .b {\n x: y;\n}\n" +); +test!( + dart_sass_issue_146, + "%btn-style-default { + background: green; + &:hover{ + background: black; + } + } + + button { + @extend %btn-style-default; + } + ", + "button {\n background: green;\n}\nbutton:hover {\n background: black;\n}\n" +); +test!( + nested_compound_unification, + "// Make sure compound unification properly handles weaving together parent + // selectors. + .a .b {@extend .e} + .c .d {@extend .f} + .e.f {x: y} + ", + ".e.f, .a .f.b, .c .e.d, .a .c .b.d, .c .a .b.d {\n x: y;\n}\n" +); +test!( + not_into_not_not, + "// Regression test for dart-sass#191. + :not(:not(.x)) {a: b} + :not(.y) {@extend .x} + ", + ":not(:not(.x)) {\n a: b;\n}\n" +); +test!( + #[ignore = "different order"] + selector_list, + ".foo {a: b} + .bar {x: y} + + // Extending a selector list is equivalent to writing two @extends. + .baz {@extend .foo, .bar} + + // The selector list should be parsed after interpolation is resolved. + .bang {@extend .foo #{\",\"} .bar} + ", + ".foo, .bang, .baz {\n a: b;\n}\n\n.bar, .bang, .baz {\n x: y;\n}\n" +); + +// todo: extend_loop (massive test) diff --git a/tests/misc.rs b/tests/misc.rs index 1a702c0..09dbfce 100644 --- a/tests/misc.rs +++ b/tests/misc.rs @@ -112,3 +112,8 @@ test!( "a {\n color: ie_hex-str(rgba(0, 255, 0, 0.5));\n}\n", "a {\n color: #8000FF00;\n}\n" ); +test!( + empty_style_after_style_emits_one_newline, + "a {\n a: b\n}\n\nb {}\n", + "a {\n a: b;\n}\n" +); diff --git a/tests/selector-extend.rs b/tests/selector-extend.rs index 0235cbf..c59a379 100644 --- a/tests/selector-extend.rs +++ b/tests/selector-extend.rs @@ -238,4 +238,24 @@ test!( "a {\n color: selector-extend(\"c, d\", \"d\", \"e\");\n}\n", "a {\n color: c, d, e;\n}\n" ); +test!( + combinator_in_selector, + "a {\n color: selector-extend(\"a > b\", \"foo\", \"bar\");\n}\n", + "a {\n color: a > b;\n}\n" +); +test!( + combinator_in_selector_with_complex_child_and_complex_2_as_extender, + "a {\n color: selector-extend(\"a + b .c1\", \".c1\", \"a c\");\n}\n", + "a {\n color: a + b .c1, a + b a c, a a + b c;\n}\n" +); +test!( + combinator_in_selector_with_complex_child_and_complex_3_as_extender, + "a {\n color: selector-extend(\"a + b .c1\", \".c1\", \"a b .c2\");\n}\n", + "a {\n color: a + b .c1, a a + b .c2;\n}\n" +); +test!( + list_as_target_with_compound_selector, + "a {\n color: selector-extend(\".foo.bar\", \".foo, .bar\", \".x\");\n}\n", + "a {\n color: .foo.bar, .x;\n}\n" +); // todo: https://github.com/sass/sass-spec/tree/master/spec/core_functions/selector/extend/simple/pseudo/selector/