diff --git a/input.scss b/input.scss index 80afe79..67ce83e 100644 --- a/input.scss +++ b/input.scss @@ -1,3 +1,3 @@ body { background: red; -} \ No newline at end of file +} diff --git a/src/atrule/unknown.rs b/src/atrule/unknown.rs index 2d0df7d..93e639d 100644 --- a/src/atrule/unknown.rs +++ b/src/atrule/unknown.rs @@ -6,4 +6,8 @@ pub(crate) struct UnknownAtRule { pub super_selector: Selector, pub params: String, pub body: Vec, + + /// Whether or not this @-rule was declared with curly + /// braces. A body may not necessarily have contents + pub has_body: bool, } diff --git a/src/output.rs b/src/output.rs index 973e177..caadbba 100644 --- a/src/output.rs +++ b/src/output.rs @@ -21,6 +21,15 @@ struct ToplevelUnknownAtRule { name: String, params: String, body: Vec, + has_body: bool, + is_group_end: bool, + inside_rule: bool, +} + +#[derive(Debug, Clone)] +struct BlockEntryUnknownAtRule { + name: String, + params: String, } #[derive(Debug, Clone)] @@ -65,6 +74,7 @@ impl Toplevel { pub fn is_group_end(&self) -> bool { match self { Toplevel::RuleSet { is_group_end, .. } => *is_group_end, + Toplevel::UnknownAtRule(t) => t.is_group_end && t.inside_rule, Toplevel::Media { inside_rule, is_group_end, @@ -87,6 +97,7 @@ fn set_group_end(group: &mut [Toplevel]) { | Some(Toplevel::Media { is_group_end, .. }) => { *is_group_end = true; } + Some(Toplevel::UnknownAtRule(t)) => t.is_group_end = true, _ => {} } } @@ -95,6 +106,7 @@ fn set_group_end(group: &mut [Toplevel]) { enum BlockEntry { Style(Style), MultilineComment(String), + UnknownAtRule(BlockEntryUnknownAtRule), } impl BlockEntry { @@ -102,6 +114,13 @@ impl BlockEntry { match self { BlockEntry::Style(s) => s.to_string(), BlockEntry::MultilineComment(s) => Ok(format!("/*{}*/", s)), + BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule { name, params }) => { + Ok(if params.is_empty() { + format!("@{};", name) + } else { + format!("@{} {};", name, params) + }) + } } } } @@ -138,6 +157,17 @@ impl Toplevel { panic!(); } } + + fn push_unknown_at_rule(&mut self, at_rule: ToplevelUnknownAtRule) { + if let Toplevel::RuleSet { body, .. } = self { + body.push(BlockEntry::UnknownAtRule(BlockEntryUnknownAtRule { + name: at_rule.name, + params: at_rule.params, + })); + } else { + panic!(); + } + } } #[derive(Debug, Clone)] @@ -206,13 +236,27 @@ impl Css { } Stmt::UnknownAtRule(u) => { let UnknownAtRule { - params, body, name, .. + params, + body, + name, + has_body, + .. } = *u; - vals.push(Toplevel::UnknownAtRule(Box::new(ToplevelUnknownAtRule { + + let at_rule = ToplevelUnknownAtRule { name, params, body, - }))); + has_body, + inside_rule: true, + is_group_end: false, + }; + + if has_body { + vals.push(Toplevel::UnknownAtRule(Box::new(at_rule))); + } else { + vals.first_mut().unwrap().push_unknown_at_rule(at_rule); + } } Stmt::Return(..) => unreachable!(), Stmt::AtRoot { body } => { @@ -267,12 +311,19 @@ impl Css { } Stmt::UnknownAtRule(u) => { let UnknownAtRule { - params, body, name, .. + params, + body, + name, + has_body, + .. } = *u; vec![Toplevel::UnknownAtRule(Box::new(ToplevelUnknownAtRule { name, params, body, + has_body, + inside_rule: false, + is_group_end: false, }))] } Stmt::Return(..) => unreachable!("@return: {:?}", stmt), @@ -393,7 +444,9 @@ impl Formatter for CompressedFormatter { write!(buf, "@import {};", s)?; } Toplevel::UnknownAtRule(u) => { - let ToplevelUnknownAtRule { params, name, body } = *u; + let ToplevelUnknownAtRule { + params, name, body, .. + } = *u; if params.is_empty() { write!(buf, "@{}", name)?; @@ -494,6 +547,7 @@ impl CompressedFormatter { break; } BlockEntry::MultilineComment(..) => continue, + b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?, } } @@ -505,6 +559,7 @@ impl CompressedFormatter { write!(buf, ";{}:{}", s.property, value)?; } BlockEntry::MultilineComment(..) => continue, + b @ BlockEntry::UnknownAtRule(_) => write!(buf, "{}", b.to_string()?)?, } } Ok(()) @@ -593,7 +648,14 @@ impl Formatter for ExpandedFormatter { write!(buf, "{}@import {};", padding, s)?; } Toplevel::UnknownAtRule(u) => { - let ToplevelUnknownAtRule { params, name, body } = *u; + let ToplevelUnknownAtRule { + params, + name, + body, + has_body, + inside_rule, + .. + } = *u; if params.is_empty() { write!(buf, "{}@{}", padding, name)?; @@ -601,14 +663,29 @@ impl Formatter for ExpandedFormatter { write!(buf, "{}@{} {}", padding, name, params)?; } - if body.is_empty() { + let css = Css::from_stmts( + body, + if inside_rule { + AtRuleContext::Unknown + } else { + AtRuleContext::None + }, + css.allows_charset, + )?; + + if !has_body { write!(buf, ";")?; prev = Some(Previous { is_group_end }); continue; } + if css.blocks.iter().all(Toplevel::is_invisible) { + write!(buf, " {{}}")?; + prev = Some(Previous { is_group_end }); + continue; + } + writeln!(buf, " {{")?; - let css = Css::from_stmts(body, AtRuleContext::Unknown, css.allows_charset)?; self.write_css(buf, css, map)?; write!(buf, "\n{}}}", padding)?; } diff --git a/src/parse/mixin.rs b/src/parse/mixin.rs index 373780d..a315068 100644 --- a/src/parse/mixin.rs +++ b/src/parse/mixin.rs @@ -218,7 +218,7 @@ impl<'a, 'b> Parser<'a, 'b> { if let Some(ref content_args) = content.content_args { call_args.max_args(content_args.len())?; - let scope = self.eval_args(&content_args, call_args)?; + let scope = self.eval_args(content_args, call_args)?; scope_at_decl.enter_scope(scope); entered_scope = true; } else { diff --git a/src/parse/mod.rs b/src/parse/mod.rs index 5275f8f..237efd2 100644 --- a/src/parse/mod.rs +++ b/src/parse/mod.rs @@ -618,36 +618,47 @@ impl<'a, 'b> Parser<'a, 'b> { let mut params = String::new(); self.whitespace_or_comment(); - if let Some(Token { kind: ';', .. }) | None = self.toks.peek() { - self.toks.next(); - return Ok(Stmt::UnknownAtRule(Box::new(UnknownAtRule { - name, - super_selector: Selector::new(self.span_before), - params: String::new(), - body: Vec::new(), - }))); - } - while let Some(tok) = self.toks.next() { - match tok.kind { - '{' => break, - '#' => { + + loop { + match self.toks.peek() { + Some(Token { kind: '{', .. }) => { + self.toks.next(); + break; + } + Some(Token { kind: ';', .. }) | Some(Token { kind: '}', .. }) | None => { + self.consume_char_if_exists(';'); + return Ok(Stmt::UnknownAtRule(Box::new(UnknownAtRule { + name, + super_selector: Selector::new(self.span_before), + has_body: false, + params: params.trim().to_owned(), + body: Vec::new(), + }))); + } + Some(Token { kind: '#', .. }) => { + self.toks.next(); + if let Some(Token { kind: '{', pos }) = self.toks.peek() { self.span_before = self.span_before.merge(pos); self.toks.next(); params.push_str(&self.parse_interpolation_as_string()?); } else { - params.push(tok.kind); + params.push('#'); } continue; } - '\n' | ' ' | '\t' => { + Some(Token { kind: '\n', .. }) + | Some(Token { kind: ' ', .. }) + | Some(Token { kind: '\t', .. }) => { self.whitespace(); params.push(' '); continue; } - _ => {} + Some(Token { kind, .. }) => { + self.toks.next(); + params.push(kind); + } } - params.push(tok.kind); } let raw_body = self.parse_stmt()?; @@ -680,6 +691,7 @@ impl<'a, 'b> Parser<'a, 'b> { name, super_selector: Selector::new(self.span_before), params: params.trim().to_owned(), + has_body: true, body, }))) } diff --git a/tests/unknown-at-rule.rs b/tests/unknown-at-rule.rs index 52a80d8..c537500 100644 --- a/tests/unknown-at-rule.rs +++ b/tests/unknown-at-rule.rs @@ -12,6 +12,7 @@ test!( "@foo {\n color: red;\n}\n" ); test!(unknown_at_rule_no_body, "@foo;\n", "@foo;\n"); +test!(unknown_at_rule_empty_body, "@foo {}\n", "@foo {}\n"); test!(unknown_at_rule_no_body_eof, "@foo", "@foo;\n"); test!( unknown_at_rule_interpolated_eof_no_body, @@ -32,4 +33,56 @@ test!( }", "@foo (a: b) {\n a {\n color: red;\n }\n}\na {\n color: green;\n}\n" ); +test!( + no_semicolon_no_params_no_body, + "a { + @b + } + + a { + color: red; + }", + "a {\n @b;\n}\n\na {\n color: red;\n}\n" +); +test!( + no_semicolon_has_params_no_body, + "a { + @foo bar + } + + a { + color: red; + }", + "a {\n @foo bar;\n}\n\na {\n color: red;\n}\n" +); +test!( + no_body_remains_inside_style_rule, + "a { + @box-shadow: $btn-focus-box-shadow, $btn-active-box-shadow; + } + + a { + color: red; + }", + "a {\n @box-shadow : $btn-focus-box-shadow, $btn-active-box-shadow;\n}\n\na {\n color: red;\n}\n" +); +test!( + empty_body_moves_outside_style_rule, + "a { + @b {} + } + + a { + color: red; + }", + "@b {}\n\na {\n color: red;\n}\n" +); +test!( + #[ignore = "not sure how dart-sass is parsing this to include the semicolon in the params"] + params_contain_silent_comment_and_semicolon, + "a { + @box-shadow: $btn-focus-box-shadow, // $btn-active-box-shadow; + }", + "a {\n @box-shadow : $btn-focus-box-shadow, / $btn-active-box-shadow;\n}\n" +); test!(contains_multiline_comment, "@foo /**/;\n", "@foo;\n");