initial implementation of @extend

This commit is contained in:
ConnorSkees 2020-06-18 16:56:03 -04:00
parent 09a322f175
commit 195079de86
18 changed files with 2713 additions and 178 deletions

View File

@ -148,7 +148,7 @@ fn selector_extend(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Va
.arg(&mut args, 2, "extender")? .arg(&mut args, 2, "extender")?
.to_selector(parser, "extender", false)?; .to_selector(parser, "extender", false)?;
Ok(Extender::extend(selector.0, source.0, target.0).to_sass_list()) Ok(Extender::extend(selector.0, source.0, target.0)?.to_sass_list())
} }
fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {
@ -163,7 +163,7 @@ fn selector_replace(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<V
parser parser
.arg(&mut args, 2, "replacement")? .arg(&mut args, 2, "replacement")?
.to_selector(parser, "replacement", false)?; .to_selector(parser, "replacement", false)?;
Ok(Extender::replace(selector.0, source.0, target.0).to_sass_list()) Ok(Extender::replace(selector.0, source.0, target.0)?.to_sass_list())
} }
fn selector_unify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> { fn selector_unify(mut args: CallArgs, parser: &mut Parser<'_>) -> SassResult<Value> {

View File

@ -107,7 +107,7 @@ use crate::{
output::Css, output::Css,
parse::{common::NeverEmptyVec, Parser}, parse::{common::NeverEmptyVec, Parser},
scope::Scope, scope::Scope,
selector::Selector, selector::{Extender, Selector},
}; };
mod args; mod args;
@ -145,6 +145,7 @@ fn raw_to_parse_error(map: &CodeMap, err: Error) -> Error {
#[cfg(not(feature = "wasm"))] #[cfg(not(feature = "wasm"))]
pub fn from_path(p: &str) -> Result<String> { pub fn from_path(p: &str) -> Result<String> {
let mut map = CodeMap::new(); let mut map = CodeMap::new();
let mut extender = Extender::new();
let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?); let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?);
Css::from_stmts( Css::from_stmts(
Parser { Parser {
@ -164,12 +165,14 @@ pub fn from_path(p: &str) -> Result<String> {
in_control_flow: false, in_control_flow: false,
at_root: true, at_root: true,
at_root_has_selector: false, at_root_has_selector: false,
extender: &mut extender,
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, e))?, .map_err(|e| raw_to_parse_error(&map, e))?,
&mut extender,
) )
.map_err(|e| raw_to_parse_error(&map, e))? .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)) .map_err(|e| raw_to_parse_error(&map, e))
} }
@ -187,6 +190,7 @@ pub fn from_path(p: &str) -> Result<String> {
#[cfg(not(feature = "wasm"))] #[cfg(not(feature = "wasm"))]
pub fn from_string(p: String) -> Result<String> { pub fn from_string(p: String) -> Result<String> {
let mut map = CodeMap::new(); let mut map = CodeMap::new();
let mut extender = Extender::new();
let file = map.add_file("stdin".into(), p); let file = map.add_file("stdin".into(), p);
Css::from_stmts( Css::from_stmts(
Parser { Parser {
@ -206,12 +210,14 @@ pub fn from_string(p: String) -> Result<String> {
in_control_flow: false, in_control_flow: false,
at_root: true, at_root: true,
at_root_has_selector: false, at_root_has_selector: false,
extender: &mut extender,
} }
.parse() .parse()
.map_err(|e| raw_to_parse_error(&map, e))?, .map_err(|e| raw_to_parse_error(&map, e))?,
&mut extender,
) )
.map_err(|e| raw_to_parse_error(&map, e))? .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)) .map_err(|e| raw_to_parse_error(&map, e))
} }

View File

@ -3,7 +3,7 @@ use std::io::Write;
use codemap::CodeMap; 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)] #[derive(Debug, Clone)]
enum Toplevel { enum Toplevel {
@ -61,7 +61,7 @@ impl Toplevel {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Css { pub(crate) struct Css {
blocks: Vec<Toplevel>, blocks: Vec<Toplevel>,
} }
@ -70,27 +70,36 @@ impl Css {
Css { blocks: Vec::new() } Css { blocks: Vec::new() }
} }
pub(crate) fn from_stmts(s: Vec<Stmt>) -> SassResult<Self> { pub(crate) fn from_stmts(s: Vec<Stmt>, extender: &mut Extender) -> SassResult<Self> {
Css::new().parse_stylesheet(s) Css::new().parse_stylesheet(s, extender)
} }
fn parse_stmt(&mut self, stmt: Stmt) -> SassResult<Vec<Toplevel>> { fn parse_stmt(&mut self, stmt: Stmt, extender: &mut Extender) -> SassResult<Vec<Toplevel>> {
Ok(match stmt { Ok(match stmt {
Stmt::RuleSet { Stmt::RuleSet {
selector, selector,
super_selector, super_selector,
body, body,
} => { } => {
let selector = selector if body.is_empty() {
.resolve_parent_selectors(&super_selector, true) return Ok(Vec::new());
.remove_placeholders(); }
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() { if selector.is_empty() {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut vals = vec![Toplevel::new_rule(selector)]; let mut vals = vec![Toplevel::new_rule(selector)];
for rule in body { for rule in body {
match rule { 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::Style(s) => vals.get_mut(0).unwrap().push_style(*s)?,
Stmt::Comment(s) => vals.get_mut(0).unwrap().push_comment(s), Stmt::Comment(s) => vals.get_mut(0).unwrap().push_comment(s),
Stmt::Media { params, body, .. } => { Stmt::Media { params, body, .. } => {
@ -102,7 +111,7 @@ impl Css {
Stmt::Return(..) => unreachable!(), Stmt::Return(..) => unreachable!(),
Stmt::AtRoot { body } => body Stmt::AtRoot { body } => body
.into_iter() .into_iter()
.map(|r| Ok(vals.extend(self.parse_stmt(r)?))) .map(|r| Ok(vals.extend(self.parse_stmt(r, extender)?)))
.collect::<SassResult<()>>()?, .collect::<SassResult<()>>()?,
}; };
} }
@ -119,10 +128,10 @@ impl Css {
}) })
} }
fn parse_stylesheet(mut self, stmts: Vec<Stmt>) -> SassResult<Css> { fn parse_stylesheet(mut self, stmts: Vec<Stmt>, extender: &mut Extender) -> SassResult<Css> {
let mut is_first = true; let mut is_first = true;
for stmt in stmts { 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 // this is how we print newlines between unrelated styles
// it could probably be refactored // it could probably be refactored
if !v.is_empty() { if !v.is_empty() {
@ -138,9 +147,9 @@ impl Css {
Ok(self) Ok(self)
} }
pub fn pretty_print(self, map: &CodeMap) -> SassResult<String> { pub fn pretty_print(self, map: &CodeMap, extender: &mut Extender) -> SassResult<String> {
let mut string = Vec::new(); 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()) { if string.iter().any(|s| !s.is_ascii()) {
return Ok(format!("@charset \"UTF-8\";\n{}", unsafe { return Ok(format!("@charset \"UTF-8\";\n{}", unsafe {
String::from_utf8_unchecked(string) String::from_utf8_unchecked(string)
@ -153,10 +162,12 @@ impl Css {
self, self,
buf: &mut Vec<u8>, buf: &mut Vec<u8>,
map: &CodeMap, map: &CodeMap,
extender: &mut Extender,
nesting: usize, nesting: usize,
) -> SassResult<()> { ) -> SassResult<()> {
let mut has_written = false; let mut has_written = false;
let padding = vec![' '; nesting * 2].iter().collect::<String>(); let padding = vec![' '; nesting * 2].iter().collect::<String>();
let mut should_emit_newline = false;
for block in self.blocks { for block in self.blocks {
match block { match block {
Toplevel::RuleSet(selector, styles) => { Toplevel::RuleSet(selector, styles) => {
@ -164,6 +175,10 @@ impl Css {
continue; continue;
} }
has_written = true; has_written = true;
if should_emit_newline {
should_emit_newline = false;
writeln!(buf)?;
}
writeln!(buf, "{}{} {{", padding, selector)?; writeln!(buf, "{}{} {{", padding, selector)?;
for style in styles { for style in styles {
writeln!(buf, "{} {}", padding, style.to_string()?)?; writeln!(buf, "{} {}", padding, style.to_string()?)?;
@ -175,6 +190,11 @@ impl Css {
writeln!(buf, "{}/*{}*/", padding, s)?; writeln!(buf, "{}/*{}*/", padding, s)?;
} }
Toplevel::UnknownAtRule { params, name, body } => { Toplevel::UnknownAtRule { params, name, body } => {
if should_emit_newline {
should_emit_newline = false;
writeln!(buf)?;
}
if params.is_empty() { if params.is_empty() {
write!(buf, "{}@{}", padding, name)?; write!(buf, "{}@{}", padding, name)?;
} else { } else {
@ -188,15 +208,29 @@ impl Css {
writeln!(buf, " {{")?; 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)?; writeln!(buf, "{}}}", padding)?;
} }
Toplevel::Media { params, body } => { Toplevel::Media { params, body } => {
if body.is_empty() { if body.is_empty() {
continue; continue;
} }
if should_emit_newline {
should_emit_newline = false;
writeln!(buf)?;
}
writeln!(buf, "{}@media {} {{", padding, params)?; 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)?; writeln!(buf, "{}}}", padding)?;
} }
Toplevel::Style(s) => { Toplevel::Style(s) => {
@ -204,8 +238,9 @@ impl Css {
} }
Toplevel::Newline => { Toplevel::Newline => {
if has_written { if has_written {
writeln!(buf)? should_emit_newline = true;
} }
continue;
} }
} }
} }

View File

@ -78,6 +78,7 @@ impl<'a> Parser<'a> {
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;

View File

@ -86,6 +86,7 @@ impl<'a> Parser<'a> {
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse(); .parse();
} }

View File

@ -110,6 +110,7 @@ impl<'a> Parser<'a> {
content, content,
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;

View File

@ -9,7 +9,7 @@ use crate::{
common::{Brackets, ListSeparator}, common::{Brackets, ListSeparator},
error::SassResult, error::SassResult,
scope::Scope, scope::Scope,
selector::{Selector, SelectorParser}, selector::{ComplexSelectorComponent, ExtendRule, Extender, Selector, SelectorParser},
style::Style, style::Style,
unit::Unit, unit::Unit,
utils::{ utils::{
@ -86,6 +86,7 @@ pub(crate) struct Parser<'a> {
/// If this parser is inside an `@at-rule` block, this is whether or /// If this parser is inside an `@at-rule` block, this is whether or
/// not the `@at-rule` block has a super selector /// not the `@at-rule` block has a super selector
pub at_root_has_selector: bool, pub at_root_has_selector: bool,
pub extender: &'a mut Extender,
} }
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
@ -348,29 +349,37 @@ impl<'a> Parser<'a> {
let mut iter = sel_toks.into_iter().peekmore(); let mut iter = sel_toks.into_iter().peekmore();
Ok(Selector( let selector = SelectorParser::new(
SelectorParser::new( &mut Parser {
&mut Parser { toks: &mut iter,
toks: &mut iter, map: self.map,
map: self.map, path: self.path,
path: self.path, scopes: self.scopes,
scopes: self.scopes, global_scope: self.global_scope,
global_scope: self.global_scope, super_selectors: self.super_selectors,
super_selectors: self.super_selectors, span_before: self.span_before,
span_before: self.span_before, content: self.content.clone(),
content: self.content.clone(), in_mixin: self.in_mixin,
in_mixin: self.in_mixin, in_function: self.in_function,
in_function: self.in_function, in_control_flow: self.in_control_flow,
in_control_flow: self.in_control_flow, at_root: self.at_root,
at_root: self.at_root, at_root_has_selector: self.at_root_has_selector,
at_root_has_selector: self.at_root_has_selector, extender: self.extender,
}, },
allows_parent, allows_parent,
true, true,
span, span,
) )
.parse()?, .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. /// Eat and return the contents of a comment.
@ -580,6 +589,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse(); .parse();
} }
@ -601,6 +611,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse() .parse()
} }
@ -722,7 +733,7 @@ impl<'a> Parser<'a> {
map: self.map, map: self.map,
path: self.path, path: self.path,
scopes: self.scopes, scopes: self.scopes,
global_scope: &mut self.global_scope, global_scope: self.global_scope,
super_selectors: self.super_selectors, super_selectors: self.super_selectors,
span_before: self.span_before, span_before: self.span_before,
content: self.content.clone(), content: self.content.clone(),
@ -731,6 +742,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -743,7 +755,7 @@ impl<'a> Parser<'a> {
map: self.map, map: self.map,
path: self.path, path: self.path,
scopes: self.scopes, scopes: self.scopes,
global_scope: &mut self.global_scope, global_scope: self.global_scope,
super_selectors: self.super_selectors, super_selectors: self.super_selectors,
span_before: self.span_before, span_before: self.span_before,
content: self.content.clone(), content: self.content.clone(),
@ -752,6 +764,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?, .parse()?,
); );
@ -801,6 +814,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -822,6 +836,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?, .parse()?,
); );
@ -932,6 +947,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;
if !these_stmts.is_empty() { if !these_stmts.is_empty() {
@ -953,6 +969,7 @@ impl<'a> Parser<'a> {
in_control_flow: true, in_control_flow: true,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?, .parse()?,
); );
@ -1072,6 +1089,7 @@ impl<'a> Parser<'a> {
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
at_root: false, at_root: false,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse()?; .parse()?;
@ -1140,6 +1158,7 @@ impl<'a> Parser<'a> {
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
at_root: true, at_root: true,
at_root_has_selector, at_root_has_selector,
extender: self.extender,
} }
.parse()? .parse()?
.into_iter() .into_iter()
@ -1167,7 +1186,79 @@ impl<'a> Parser<'a> {
#[allow(clippy::unused_self)] #[allow(clippy::unused_self)]
fn parse_extend(&mut self) -> SassResult<()> { 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::<Vec<String>>().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)] #[allow(clippy::unused_self)]

View File

@ -157,6 +157,7 @@ impl<'a> Parser<'a> {
in_control_flow: self.in_control_flow, in_control_flow: self.in_control_flow,
at_root: self.at_root, at_root: self.at_root,
at_root_has_selector: self.at_root_has_selector, at_root_has_selector: self.at_root_has_selector,
extender: self.extender,
} }
.parse_value() .parse_value()
} }
@ -257,6 +258,8 @@ impl<'a> Parser<'a> {
None => return None, None => return None,
}; };
self.span_before = span;
if self.whitespace() { if self.whitespace() {
return Some(Ok(Spanned { return Some(Ok(Spanned {
node: IntermediateValue::Whitespace, node: IntermediateValue::Whitespace,
@ -491,10 +494,13 @@ impl<'a> Parser<'a> {
Err(e) => return Some(Err(e)), Err(e) => return Some(Err(e)),
}; };
span = span.merge(v.span); span = span.merge(v.span);
if v.node.to_ascii_lowercase().as_str() == "important" { // TODO: we return `None` when encountering `optional` here as a hack for
IntermediateValue::Value(Value::Important).span(span) // supporting `!optional` in `@extend`. In the future, we should have a better
} else { // check for `!optional` as this technically allows `!optional` everywhere
return Some(Err(("Expected \"important\".", span).into())); 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())),
} }
} }
'/' => { '/' => {

View File

@ -3,7 +3,7 @@ use codemap::Span;
use super::{ComplexSelector, CssMediaQuery, SimpleSelector}; use super::{ComplexSelector, CssMediaQuery, SimpleSelector};
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(super) struct Extension { pub(crate) struct Extension {
/// The selector in which the `@extend` appeared. /// The selector in which the `@extend` appeared.
pub extender: ComplexSelector, pub extender: ComplexSelector,
@ -26,10 +26,13 @@ pub(super) struct Extension {
/// The media query context to which this extend is restricted, or `None` if /// The media query context to which this extend is restricted, or `None` if
/// it can apply within any context. /// it can apply within any context.
// todo: Option // todo: Option
pub media_context: Vec<CssMediaQuery>, pub media_context: Option<Vec<CssMediaQuery>>,
/// The span in which `extender` was defined. /// The span in which `extender` was defined.
pub span: Option<Span>, pub span: Option<Span>,
pub left: Option<Box<Extension>>,
pub right: Option<Box<Extension>>,
} }
impl Extension { impl Extension {
@ -41,7 +44,9 @@ impl Extension {
span: None, span: None,
is_optional: true, is_optional: true,
is_original, 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 // 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) // use `?` to return both `Option` and `Result` from the same function)
pub fn assert_compatible_media_context(&self, media_context: &Option<Vec<CssMediaQuery>>) { 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 {
if &self.media_context == media_context { return;
return;
}
} }
// Err(("You may not @extend selectors across media queries.", self.span.unwrap()).into()) // 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
}
} }

View File

@ -77,23 +77,21 @@ pub(crate) fn weave(
let target = complex.last().unwrap().clone(); let target = complex.last().unwrap().clone();
if complex.len() == 1 { let complex_len = complex.len();
if complex_len == 1 {
for prefix in &mut prefixes { for prefix in &mut prefixes {
prefix.push(target.clone()); prefix.push(target.clone());
} }
continue; continue;
} }
let complex_len = complex.len();
let parents: Vec<ComplexSelectorComponent> = let parents: Vec<ComplexSelectorComponent> =
complex.into_iter().take(complex_len - 1).collect(); complex.into_iter().take(complex_len - 1).collect();
let mut new_prefixes: Vec<Vec<ComplexSelectorComponent>> = Vec::new(); let mut new_prefixes: Vec<Vec<ComplexSelectorComponent>> = Vec::new();
for prefix in prefixes { for prefix in prefixes {
let parent_prefixes = weave_parents(prefix, parents.clone()); if let Some(parent_prefixes) = weave_parents(prefix, parents.clone()) {
if let Some(parent_prefixes) = parent_prefixes {
for mut parent_prefix in parent_prefixes { for mut parent_prefix in parent_prefixes {
parent_prefix.push(target.clone()); parent_prefix.push(target.clone());
new_prefixes.push(parent_prefix); new_prefixes.push(parent_prefix);
@ -624,24 +622,24 @@ fn group_selectors(
let mut iter = complex.into_iter(); 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] vec![c]
} else { } else {
return groups; return groups;
}; });
groups.push_back(group.clone());
for c in iter { for c in iter {
if group let mut last_group = groups.pop_back().unwrap();
if last_group
.last() .last()
.map_or(false, ComplexSelectorComponent::is_combinator) .map_or(false, ComplexSelectorComponent::is_combinator)
|| c.is_combinator() || c.is_combinator()
{ {
group.push(c); last_group.push(c);
groups.push_back(last_group);
} else { } else {
group = vec![c]; groups.push_back(last_group);
groups.push_back(group.clone()); groups.push_back(vec![c]);
} }
} }

View File

@ -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<Extension> {
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<Extension> {
todo!()
/* Iterable<Extension> 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);
}
*/

View File

@ -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 indexmap::IndexMap;
use crate::error::SassResult;
use super::{ use super::{
ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SelectorList, ComplexSelector, ComplexSelectorComponent, CompoundSelector, Pseudo, SelectorList,
SimpleSelector, SimpleSelector,
@ -10,9 +17,13 @@ use super::{
use extension::Extension; use extension::Extension;
pub(crate) use functions::unify_complex; pub(crate) use functions::unify_complex;
use functions::{paths, weave}; use functions::{paths, weave};
use merged::MergedExtension;
pub(crate) use rule::ExtendRule;
mod extension; mod extension;
mod functions; mod functions;
mod merged;
mod rule;
#[derive(Clone, Debug, Eq, PartialEq, Hash)] #[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub(crate) struct CssMediaQuery; pub(crate) struct CssMediaQuery;
@ -101,65 +112,79 @@ impl Extender {
selector: SelectorList, selector: SelectorList,
source: SelectorList, source: SelectorList,
targets: SelectorList, targets: SelectorList,
) -> SelectorList { ) -> SassResult<SelectorList> {
Self::extend_or_replace(selector, source, targets, ExtendMode::AllTargets) 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( pub fn replace(
selector: SelectorList, selector: SelectorList,
source: SelectorList, source: SelectorList,
targets: SelectorList, targets: SelectorList,
) -> SelectorList { ) -> SassResult<SelectorList> {
Self::extend_or_replace(selector, source, targets, ExtendMode::Replace) Self::extend_or_replace(selector, source, targets, ExtendMode::Replace)
} }
fn extend_or_replace( fn extend_or_replace(
mut selector: SelectorList, selector: SelectorList,
source: SelectorList, source: SelectorList,
targets: SelectorList, targets: SelectorList,
mode: ExtendMode, mode: ExtendMode,
) -> SelectorList { ) -> SassResult<SelectorList> {
let extenders: IndexMap<ComplexSelector, Extension> = source let extenders: IndexMap<ComplexSelector, Extension> = source
.components .components
.clone()
.into_iter() .into_iter()
.zip( .map(|complex| (complex.clone(), Extension::one_off(complex, None, false)))
source
.components
.into_iter()
.map(|complex| Extension::one_off(complex, None, false)),
)
.collect(); .collect();
for complex in targets.components { let compound_targets = targets
if complex.components.len() != 1 { .components
todo!("throw SassScriptException(\"Can't extend complex selector $complex.\");") .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::<SassResult<Vec<CompoundSelector>>>()?;
let compound = match complex.components.first() { let extensions: HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>> =
Some(ComplexSelectorComponent::Compound(c)) => c, compound_targets
Some(..) | None => todo!(), .into_iter()
}; .flat_map(|compound| {
compound
.components
.into_iter()
.map(|simple| (simple, extenders.clone()))
})
.collect();
let extensions: HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>> = let mut extender = Extender::with_mode(mode);
compound
.components
.clone()
.into_iter()
.map(|simple| (simple, extenders.clone()))
.collect();
let mut extender = Extender::with_mode(mode); if !selector.is_invisible() {
if !selector.is_invisible() { extender
extender .originals
.originals .extend(selector.components.iter().cloned());
.extend(selector.components.clone().into_iter());
}
selector = extender.extend_list(selector, &extensions, &None);
} }
selector Ok(extender.extend_list(selector, &extensions, &None))
} }
fn with_mode(mode: ExtendMode) -> Self { fn with_mode(mode: ExtendMode) -> Self {
@ -199,7 +224,7 @@ impl Extender {
} }
SelectorList { 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, 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 first = true;
let mut originals: Vec<ComplexSelector> = Vec::new();
Some( Some(
paths(extended_not_expanded) paths(extended_not_expanded)
.into_iter() .into_iter()
@ -287,8 +317,11 @@ impl Extender {
|| path.iter().any(|input_complex| input_complex.line_break), || path.iter().any(|input_complex| input_complex.line_break),
}; };
if first && originals.contains(&complex.clone()) { // Make sure that copies of `complex` retain their status as "original"
originals.push(output_complex.clone()); // 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; first = false;
@ -302,6 +335,11 @@ impl Extender {
/// Extends `compound` using `extensions`, and returns the contents of a /// Extends `compound` using `extensions`, and returns the contents of a
/// `SelectorList`. /// `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( fn extend_compound(
&mut self, &mut self,
compound: &CompoundSelector, compound: &CompoundSelector,
@ -341,7 +379,11 @@ impl Extender {
// If `self.mode` isn't `ExtendMode::Normal` and we didn't use all the targets in // If `self.mode` isn't `ExtendMode::Normal` and we didn't use all the targets in
// `extensions`, extension fails for `compound`. // `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; return None;
} }
@ -401,7 +443,7 @@ impl Extender {
.clone() .clone()
.into_iter() .into_iter()
.flat_map(|state| { .flat_map(|state| {
assert!(state.extender.components.len() == 1); debug_assert!(state.extender.components.len() == 1);
match state.extender.components.last().cloned() { match state.extender.components.last().cloned() {
Some(ComplexSelectorComponent::Compound(c)) => c.components, Some(ComplexSelectorComponent::Compound(c)) => c.components,
Some(..) | None => unreachable!(), Some(..) | None => unreachable!(),
@ -630,8 +672,8 @@ impl Extender {
} }
} }
// Extends `simple` without extending the contents of any selector pseudos /// Extends `simple` without extending the contents of any selector pseudos
// it contains. /// it contains.
fn without_pseudo( fn without_pseudo(
&self, &self,
simple: SimpleSelector, simple: SimpleSelector,
@ -696,15 +738,15 @@ impl Extender {
specificity specificity
} }
// Removes elements from `selectors` if they're subselectors of other /// Removes elements from `selectors` if they're subselectors of other
// elements. /// elements.
// ///
// The `is_original` callback indicates which selectors are original to the /// The `is_original` callback indicates which selectors are original to the
// document, and thus should never be trimmed. /// document, and thus should never be trimmed.
fn trim( fn trim(
&self, &self,
selectors: Vec<ComplexSelector>, selectors: Vec<ComplexSelector>,
is_original: impl Fn(ComplexSelector) -> bool, is_original: impl Fn(&ComplexSelector) -> bool,
) -> Vec<ComplexSelector> { ) -> Vec<ComplexSelector> {
// Avoid truly horrific quadratic behavior. // Avoid truly horrific quadratic behavior.
// //
@ -723,69 +765,378 @@ impl Extender {
let mut num_originals = 0; let mut num_originals = 0;
// :outer // :outer
loop { for i in (0..=(selectors.len().saturating_sub(1))).rev() {
let mut should_break_to_outer = false; let mut should_continue_to_outer = false;
for i in (0..=(selectors.len().saturating_sub(1))).rev() { let complex1 = selectors.get(i).unwrap();
let complex1 = selectors.get(i).unwrap(); if is_original(complex1) {
if is_original(complex1.clone()) { // Make sure we don't include duplicate originals, which could happen if
// Make sure we don't include duplicate originals, which could happen if // a style rule extends a component of its own selector.
// a style rule extends a component of its own selector. for j in 0..num_originals {
for j in 0..num_originals { if result.get(j) == Some(complex1) {
if result.get(j).unwrap() == complex1 { rotate_slice(&mut result, 0, j + 1);
rotate_slice(&mut result, 0, j + 1); should_continue_to_outer = true;
should_break_to_outer = true;
break;
}
}
if should_break_to_outer {
break; break;
} }
num_originals += 1; }
result.push_front(complex1.clone()); if should_continue_to_outer {
continue; continue;
} }
num_originals += 1;
// 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()); result.push_front(complex1.clone());
}
if should_break_to_outer {
continue; 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) 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<RefCell<SelectorList>>`
// 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<Vec<CssMediaQuery>>,
) -> 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<Vec<CssMediaQuery>>,
span: Option<Span>,
) {
let selectors = self.selectors.get(target).cloned();
let existing_extensions = self.extensions_by_extender.get(target).cloned();
let mut new_extensions: Option<IndexMap<ComplexSelector, Extension>> = 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<Extension>,
new_extensions: &HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
) -> Option<HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>> {
let mut additional_extensions: Option<
HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
> = 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<ComplexSelector> = 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<SelectorList>,
new_extensions: &HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>>,
) {
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) /// Rotates the element in list from `start` (inclusive) to `end` (exclusive)
@ -798,3 +1149,22 @@ fn rotate_slice<T: Clone>(list: &mut VecDeque<T>, start: usize, end: usize) {
element = next; element = next;
} }
} }
/// Like `HashMap::extend`, but for two-layer maps.
///
/// This avoids copying inner maps from `source` if possible.
fn map_add_all_2<K1: Hash + Eq, K2: Hash + Eq, V>(
destination: &mut HashMap<K1, IndexMap<K2, V>>,
source: HashMap<K1, IndexMap<K2, V>>,
) {
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);
}
})
}

View File

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

View File

@ -314,7 +314,7 @@ impl SimpleSelector {
}) = simple }) = simple
{ {
// A given compound selector may only contain one pseudo element. If // 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 { if let Self::Pseudo(Pseudo {
is_class: false, .. is_class: false, ..
}) = self }) = self

View File

@ -359,6 +359,7 @@ impl Value {
in_control_flow: parser.in_control_flow, in_control_flow: parser.in_control_flow,
at_root: parser.at_root, at_root: parser.at_root,
at_root_has_selector: parser.at_root_has_selector, at_root_has_selector: parser.at_root_has_selector,
extender: parser.extender,
} }
.parse_selector(allows_parent, true, String::new()) .parse_selector(allows_parent, true, String::new())
} }

1867
tests/extend.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -112,3 +112,8 @@ test!(
"a {\n color: ie_hex-str(rgba(0, 255, 0, 0.5));\n}\n", "a {\n color: ie_hex-str(rgba(0, 255, 0, 0.5));\n}\n",
"a {\n color: #8000FF00;\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"
);

View File

@ -238,4 +238,24 @@ test!(
"a {\n color: selector-extend(\"c, d\", \"d\", \"e\");\n}\n", "a {\n color: selector-extend(\"c, d\", \"d\", \"e\");\n}\n",
"a {\n color: c, 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/ // todo: https://github.com/sass/sass-spec/tree/master/spec/core_functions/selector/extend/simple/pseudo/selector/