initial implementation of @extend
This commit is contained in:
parent
09a322f175
commit
195079de86
@ -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> {
|
||||||
|
12
src/lib.rs
12
src/lib.rs
@ -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))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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,19 +70,28 @@ 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());
|
||||||
|
}
|
||||||
|
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();
|
.remove_placeholders();
|
||||||
if selector.is_empty() {
|
if selector.is_empty() {
|
||||||
return Ok(Vec::new());
|
return Ok(Vec::new());
|
||||||
@ -90,7 +99,7 @@ impl Css {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()?;
|
||||||
|
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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()?;
|
||||||
|
|
||||||
|
107
src/parse/mod.rs
107
src/parse/mod.rs
@ -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,8 +349,7 @@ 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,
|
||||||
@ -364,13 +364,22 @@ 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,
|
||||||
},
|
},
|
||||||
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)]
|
||||||
|
@ -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())),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
'/' => {
|
'/' => {
|
||||||
|
@ -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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
104
src/selector/extend/merged.rs
Normal file
104
src/selector/extend/merged.rs
Normal 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);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
*/
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
})
|
||||||
let compound = match complex.components.first() {
|
.collect::<SassResult<Vec<CompoundSelector>>>()?;
|
||||||
Some(ComplexSelectorComponent::Compound(c)) => c,
|
|
||||||
Some(..) | None => todo!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let extensions: HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>> =
|
let extensions: HashMap<SimpleSelector, IndexMap<ComplexSelector, Extension>> =
|
||||||
|
compound_targets
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|compound| {
|
||||||
compound
|
compound
|
||||||
.components
|
.components
|
||||||
.clone()
|
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|simple| (simple, extenders.clone()))
|
.map(|simple| (simple, extenders.clone()))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut extender = Extender::with_mode(mode);
|
let mut extender = Extender::with_mode(mode);
|
||||||
|
|
||||||
if !selector.is_invisible() {
|
if !selector.is_invisible() {
|
||||||
extender
|
extender
|
||||||
.originals
|
.originals
|
||||||
.extend(selector.components.clone().into_iter());
|
.extend(selector.components.iter().cloned());
|
||||||
}
|
}
|
||||||
|
|
||||||
selector = extender.extend_list(selector, &extensions, &None);
|
Ok(extender.extend_list(selector, &extensions, &None))
|
||||||
}
|
|
||||||
|
|
||||||
selector
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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,22 +765,21 @@ impl Extender {
|
|||||||
let mut num_originals = 0;
|
let mut num_originals = 0;
|
||||||
|
|
||||||
// :outer
|
// :outer
|
||||||
loop {
|
|
||||||
let mut should_break_to_outer = false;
|
|
||||||
for i in (0..=(selectors.len().saturating_sub(1))).rev() {
|
for i in (0..=(selectors.len().saturating_sub(1))).rev() {
|
||||||
|
let mut should_continue_to_outer = false;
|
||||||
let complex1 = selectors.get(i).unwrap();
|
let complex1 = selectors.get(i).unwrap();
|
||||||
if is_original(complex1.clone()) {
|
if is_original(complex1) {
|
||||||
// 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).unwrap() == complex1 {
|
if result.get(j) == Some(complex1) {
|
||||||
rotate_slice(&mut result, 0, j + 1);
|
rotate_slice(&mut result, 0, j + 1);
|
||||||
should_break_to_outer = true;
|
should_continue_to_outer = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if should_break_to_outer {
|
if should_continue_to_outer {
|
||||||
break;
|
continue;
|
||||||
}
|
}
|
||||||
num_originals += 1;
|
num_originals += 1;
|
||||||
result.push_front(complex1.clone());
|
result.push_front(complex1.clone());
|
||||||
@ -778,14 +819,324 @@ impl Extender {
|
|||||||
|
|
||||||
result.push_front(complex1.clone());
|
result.push_front(complex1.clone());
|
||||||
}
|
}
|
||||||
if should_break_to_outer {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
20
src/selector/extend/rule.rs
Normal file
20
src/selector/extend/rule.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
@ -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
1867
tests/extend.rs
Normal file
File diff suppressed because it is too large
Load Diff
@ -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"
|
||||||
|
);
|
||||||
|
@ -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/
|
||||||
|
Loading…
x
Reference in New Issue
Block a user