support three level extend loop
the last feature stopping us from semantic parity with `dart-sass` when compiling bootstrap. this was a difficult bug -- it essentially boiled down to the fact that we weren't applying extensions to _super_ selectors. i suspect that this has somehow broken another feature of `@extend`, but all of our unit tests, the sass spec, and bootstrap seem to be correct, so i am considering this implemented.
This commit is contained in:
parent
6de7b113cf
commit
0edb60e2b3
71
src/lib.rs
71
src/lib.rs
@ -112,7 +112,7 @@ use crate::{
|
||||
Parser,
|
||||
},
|
||||
scope::{Scope, Scopes},
|
||||
selector::{Extender, Selector},
|
||||
selector::{ExtendedSelector, Extender, SelectorList},
|
||||
};
|
||||
|
||||
mod args;
|
||||
@ -267,30 +267,20 @@ fn raw_to_parse_error(map: &CodeMap, err: Error, unicode: bool) -> Box<Error> {
|
||||
Box::new(Error::from_loc(message, map.look_up_span(span), unicode))
|
||||
}
|
||||
|
||||
/// Compile CSS from a path
|
||||
///
|
||||
/// ```
|
||||
/// fn main() -> Result<(), Box<grass::Error>> {
|
||||
/// let sass = grass::from_path("input.scss", &grass::Options::default())?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
/// (grass does not currently allow files or paths that are not valid UTF-8)
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
||||
#[cfg(not(feature = "wasm"))]
|
||||
pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
fn from_string_with_file_name(input: String, file_name: &str, options: &Options) -> Result<String> {
|
||||
let mut map = CodeMap::new();
|
||||
let file = map.add_file(p.into(), String::from_utf8(fs::read(p)?)?);
|
||||
let file = map.add_file(file_name.to_owned(), input);
|
||||
let empty_span = file.span.subspan(0, 0);
|
||||
|
||||
let stmts = Parser {
|
||||
toks: &mut Lexer::new_from_file(&file),
|
||||
map: &mut map,
|
||||
path: p.as_ref(),
|
||||
path: file_name.as_ref(),
|
||||
scopes: &mut Scopes::new(),
|
||||
global_scope: &mut Scope::new(),
|
||||
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
|
||||
super_selectors: &mut NeverEmptyVec::new(ExtendedSelector::new(SelectorList::new(
|
||||
empty_span,
|
||||
))),
|
||||
span_before: empty_span,
|
||||
content: &mut Vec::new(),
|
||||
flags: ContextFlags::empty(),
|
||||
@ -311,6 +301,22 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
|
||||
}
|
||||
|
||||
/// Compile CSS from a path
|
||||
///
|
||||
/// ```
|
||||
/// fn main() -> Result<(), Box<grass::Error>> {
|
||||
/// let sass = grass::from_path("input.scss", &grass::Options::default())?;
|
||||
/// Ok(())
|
||||
/// }
|
||||
/// ```
|
||||
/// (grass does not currently allow files or paths that are not valid UTF-8)
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
||||
#[cfg(not(feature = "wasm"))]
|
||||
pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
from_string_with_file_name(String::from_utf8(fs::read(p)?)?, p, options)
|
||||
}
|
||||
|
||||
/// Compile CSS from a string
|
||||
///
|
||||
/// ```
|
||||
@ -323,35 +329,8 @@ pub fn from_path(p: &str, options: &Options) -> Result<String> {
|
||||
#[cfg_attr(feature = "profiling", inline(never))]
|
||||
#[cfg_attr(not(feature = "profiling"), inline)]
|
||||
#[cfg(not(feature = "wasm"))]
|
||||
pub fn from_string(p: String, options: &Options) -> Result<String> {
|
||||
let mut map = CodeMap::new();
|
||||
let file = map.add_file("stdin".into(), p);
|
||||
let empty_span = file.span.subspan(0, 0);
|
||||
let stmts = Parser {
|
||||
toks: &mut Lexer::new_from_file(&file),
|
||||
map: &mut map,
|
||||
path: Path::new(""),
|
||||
scopes: &mut Scopes::new(),
|
||||
global_scope: &mut Scope::new(),
|
||||
super_selectors: &mut NeverEmptyVec::new(Selector::new(empty_span)),
|
||||
span_before: empty_span,
|
||||
content: &mut Vec::new(),
|
||||
flags: ContextFlags::empty(),
|
||||
at_root: true,
|
||||
at_root_has_selector: false,
|
||||
extender: &mut Extender::new(empty_span),
|
||||
content_scopes: &mut Scopes::new(),
|
||||
options,
|
||||
modules: &mut Modules::default(),
|
||||
module_config: &mut ModuleConfig::default(),
|
||||
}
|
||||
.parse()
|
||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?;
|
||||
|
||||
Css::from_stmts(stmts, false, options.allows_charset)
|
||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))?
|
||||
.pretty_print(&map, options.style)
|
||||
.map_err(|e| raw_to_parse_error(&map, *e, options.unicode_error_messages))
|
||||
pub fn from_string(input: String, options: &Options) -> Result<String> {
|
||||
from_string_with_file_name(input, "stdin", options)
|
||||
}
|
||||
|
||||
#[cfg(feature = "wasm")]
|
||||
|
@ -71,7 +71,7 @@ pub(crate) struct Parser<'a> {
|
||||
pub global_scope: &'a mut Scope,
|
||||
pub scopes: &'a mut Scopes,
|
||||
pub content_scopes: &'a mut Scopes,
|
||||
pub super_selectors: &'a mut NeverEmptyVec<Selector>,
|
||||
pub super_selectors: &'a mut NeverEmptyVec<ExtendedSelector>,
|
||||
pub span_before: Span,
|
||||
pub content: &'a mut Vec<Content>,
|
||||
pub flags: ContextFlags,
|
||||
@ -106,6 +106,7 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
self.at_root = true;
|
||||
}
|
||||
|
||||
Ok(stmts)
|
||||
}
|
||||
|
||||
@ -358,14 +359,15 @@ impl<'a> Parser<'a> {
|
||||
.parse_selector(true, false, init)?
|
||||
.0
|
||||
.resolve_parent_selectors(
|
||||
self.super_selectors.last(),
|
||||
&self.super_selectors.last().clone().into_selector(),
|
||||
!at_root || self.at_root_has_selector,
|
||||
)?;
|
||||
self.scopes.enter_new_scope();
|
||||
self.super_selectors.push(selector.clone());
|
||||
|
||||
let extended_selector = self.extender.add_selector(selector.0, None);
|
||||
|
||||
self.super_selectors.push(extended_selector.clone());
|
||||
|
||||
let body = self.parse_stmt()?;
|
||||
self.scopes.exit_scope();
|
||||
self.super_selectors.pop();
|
||||
@ -660,9 +662,15 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.super_selectors.last().is_empty() {
|
||||
if !self
|
||||
.super_selectors
|
||||
.last()
|
||||
.clone()
|
||||
.into_selector()
|
||||
.is_empty()
|
||||
{
|
||||
body = vec![Stmt::RuleSet {
|
||||
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
|
||||
selector: self.super_selectors.last().clone(),
|
||||
body,
|
||||
}];
|
||||
}
|
||||
@ -700,9 +708,15 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.super_selectors.last().is_empty() {
|
||||
if !self
|
||||
.super_selectors
|
||||
.last()
|
||||
.clone()
|
||||
.into_selector()
|
||||
.is_empty()
|
||||
{
|
||||
body = vec![Stmt::RuleSet {
|
||||
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
|
||||
selector: self.super_selectors.last().clone(),
|
||||
body,
|
||||
}];
|
||||
}
|
||||
@ -723,9 +737,16 @@ impl<'a> Parser<'a> {
|
||||
self.super_selectors.last().clone()
|
||||
} else {
|
||||
at_root_has_selector = true;
|
||||
self.parse_selector(true, false, String::new())?.0
|
||||
}
|
||||
.resolve_parent_selectors(self.super_selectors.last(), false)?;
|
||||
let selector = self
|
||||
.parse_selector(true, false, String::new())?
|
||||
.0
|
||||
.resolve_parent_selectors(
|
||||
&self.super_selectors.last().clone().into_selector(),
|
||||
false,
|
||||
)?;
|
||||
|
||||
self.extender.add_selector(selector.0, None)
|
||||
};
|
||||
|
||||
self.whitespace();
|
||||
|
||||
@ -760,7 +781,7 @@ impl<'a> Parser<'a> {
|
||||
})
|
||||
.collect::<SassResult<Vec<Stmt>>>()?;
|
||||
let mut stmts = vec![Stmt::RuleSet {
|
||||
selector: ExtendedSelector::new(at_rule_selector.0),
|
||||
selector: at_rule_selector,
|
||||
body: styles,
|
||||
}];
|
||||
stmts.extend(raw_stmts);
|
||||
@ -825,7 +846,7 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
|
||||
self.extender.add_extension(
|
||||
super_selector.clone().0,
|
||||
super_selector.clone().into_selector().0,
|
||||
compound.components.first().unwrap(),
|
||||
&extend_rule,
|
||||
&None,
|
||||
@ -859,9 +880,15 @@ impl<'a> Parser<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
if !self.super_selectors.last().is_empty() {
|
||||
if !self
|
||||
.super_selectors
|
||||
.last()
|
||||
.clone()
|
||||
.into_selector()
|
||||
.is_empty()
|
||||
{
|
||||
body = vec![Stmt::RuleSet {
|
||||
selector: ExtendedSelector::new(self.super_selectors.last().clone().0),
|
||||
selector: self.super_selectors.last().clone(),
|
||||
body,
|
||||
}];
|
||||
}
|
||||
|
@ -839,7 +839,11 @@ impl<'a> Parser<'a> {
|
||||
.span(span)
|
||||
} else {
|
||||
IntermediateValue::Value(HigherIntermediateValue::Literal(
|
||||
self.super_selectors.last().clone().into_value(),
|
||||
self.super_selectors
|
||||
.last()
|
||||
.clone()
|
||||
.into_selector()
|
||||
.into_value(),
|
||||
))
|
||||
.span(span)
|
||||
}
|
||||
|
@ -1239,7 +1239,6 @@ test!(
|
||||
".foo, .bar {\n a: b;\n}\n\n.bar, .foo {\n c: d;\n}\n"
|
||||
);
|
||||
test!(
|
||||
#[ignore = "Rc<RefCell<Selector>>"]
|
||||
three_level_extend_loop,
|
||||
".foo {a: b; @extend .bar}
|
||||
.bar {c: d; @extend .baz}
|
||||
@ -1413,8 +1412,7 @@ test!(
|
||||
#[ignore = "media queries are not yet parsed correctly"]
|
||||
extend_within_separate_nested_at_rules,
|
||||
"@media screen {@flooblehoof {.foo {a: b}}}
|
||||
@media screen {@flooblehoof {.bar {@extend .foo}}}
|
||||
",
|
||||
@media screen {@flooblehoof {.bar {@extend .foo}}}",
|
||||
"@media screen {\n @flooblehoof {\n .foo, .bar {\n a: b;\n }\n }\n}\n@media screen {\n @flooblehoof {}\n}\n"
|
||||
);
|
||||
test!(
|
||||
@ -1771,7 +1769,7 @@ test!(
|
||||
// extending rules that contain their own extends needs special handling.
|
||||
.b {@extend .a}
|
||||
.c {@extend .b}
|
||||
.a {x: y}
|
||||
.a {x: y}
|
||||
",
|
||||
".a, .b, .c {\n x: y;\n}\n"
|
||||
);
|
||||
|
Loading…
x
Reference in New Issue
Block a user