Merge branch 'master' of https://github.com/connorskees/grass into modules

This commit is contained in:
Connor Skees 2020-08-06 01:23:09 -04:00
commit d043167015
19 changed files with 287 additions and 114 deletions

View File

@ -33,7 +33,7 @@ pub(crate) fn simple_selectors(mut args: CallArgs, parser: &mut Parser<'_>) -> S
} }
let compound = if let Some(ComplexSelectorComponent::Compound(compound)) = let compound = if let Some(ComplexSelectorComponent::Compound(compound)) =
selector.0.components[0].components.get(0).cloned() selector.0.components[0].components.first().cloned()
{ {
compound compound
} else { } else {

View File

@ -129,8 +129,8 @@ impl Css {
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)?),
Stmt::Style(s) => vals.get_mut(0).unwrap().push_style(s), Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
Stmt::Comment(s) => vals.get_mut(0).unwrap().push_comment(s), Stmt::Comment(s) => vals.first_mut().unwrap().push_comment(s),
Stmt::Media(m) => { Stmt::Media(m) => {
let MediaRule { query, body, .. } = *m; let MediaRule { query, body, .. } = *m;
vals.push(Toplevel::Media { query, body }) vals.push(Toplevel::Media { query, body })
@ -167,7 +167,7 @@ impl Css {
k @ Stmt::KeyframesRuleSet(..) => { k @ Stmt::KeyframesRuleSet(..) => {
unreachable!("@keyframes ruleset {:?}", k) unreachable!("@keyframes ruleset {:?}", k)
} }
Stmt::Import(s) => vals.get_mut(0).unwrap().push_import(s), Stmt::Import(s) => vals.first_mut().unwrap().push_import(s),
}; };
} }
vals vals
@ -204,7 +204,7 @@ impl Css {
let mut vals = vec![Toplevel::new_keyframes_rule(selector)]; let mut vals = vec![Toplevel::new_keyframes_rule(selector)];
for rule in body { for rule in body {
match rule { match rule {
Stmt::Style(s) => vals.get_mut(0).unwrap().push_style(s), Stmt::Style(s) => vals.first_mut().unwrap().push_style(s),
Stmt::KeyframesRuleSet(..) => vals.extend(self.parse_stmt(rule)?), Stmt::KeyframesRuleSet(..) => vals.extend(self.parse_stmt(rule)?),
_ => todo!(), _ => todo!(),
} }
@ -221,7 +221,7 @@ impl Css {
// 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() {
if let Some(Toplevel::MultilineComment(..)) = v.get(0) { if let Some(Toplevel::MultilineComment(..)) = v.first() {
} else if is_first { } else if is_first {
is_first = false; is_first = false;
} else { } else {

View File

@ -341,13 +341,15 @@ impl<'a> Parser<'a> {
return Err(("expected \")\".", pos).into()); return Err(("expected \")\".", pos).into());
} }
} }
Some(..) => unreachable!(), Some(Token { pos, .. }) => {
return Err(("expected \")\".", *pos).into());
}
None => return Err(("expected \")\".", span).into()), None => return Err(("expected \")\".", span).into()),
} }
} }
Some(c) => { Some(Token { pos, .. }) => {
value?; value?;
unreachable!("{:?}", c) return Err(("expected \")\".", *pos).into());
} }
None => return Err(("expected \")\".", span).into()), None => return Err(("expected \")\".", span).into()),
} }

View File

@ -31,6 +31,10 @@ impl<'a> Parser<'a> {
return Err(("Functions may not be declared in control directives.", span).into()); return Err(("Functions may not be declared in control directives.", span).into());
} }
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) { if RESERVED_IDENTIFIERS.contains(&unvendor(&name)) {
return Err(("Invalid function name.", span).into()); return Err(("Invalid function name.", span).into());
} }

View File

@ -112,6 +112,10 @@ impl<'a> Parser<'a> {
} }
pub(super) fn import(&mut self) -> SassResult<Vec<Stmt>> { pub(super) fn import(&mut self) -> SassResult<Vec<Stmt>> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
self.whitespace(); self.whitespace();
match self.toks.peek() { match self.toks.peek() {

View File

@ -186,6 +186,10 @@ impl<'a> Parser<'a> {
} }
pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> { pub(super) fn parse_keyframes(&mut self, rule: String) -> SassResult<Stmt> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
let name = self.parse_keyframes_name()?; let name = self.parse_keyframes_name()?;
self.whitespace(); self.whitespace();

View File

@ -66,6 +66,10 @@ impl<'a> Parser<'a> {
} }
pub(super) fn parse_include(&mut self) -> SassResult<Vec<Stmt>> { pub(super) fn parse_include(&mut self) -> SassResult<Vec<Stmt>> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
self.whitespace_or_comment(); self.whitespace_or_comment();
let name = self.parse_identifier()?.map_node(Into::into); let name = self.parse_identifier()?.map_node(Into::into);

View File

@ -310,6 +310,14 @@ impl<'a> Parser<'a> {
} }
} }
AtRuleKind::AtRoot => { AtRuleKind::AtRoot => {
if self.flags.in_function() {
return Err((
"This at-rule is not allowed here.",
kind_string.span,
)
.into());
}
if self.at_root { if self.at_root {
stmts.append(&mut self.parse_at_root()?); stmts.append(&mut self.parse_at_root()?);
} else { } else {
@ -365,6 +373,14 @@ impl<'a> Parser<'a> {
AtRuleKind::For => stmts.append(&mut self.parse_for()?), AtRuleKind::For => stmts.append(&mut self.parse_for()?),
AtRuleKind::While => stmts.append(&mut self.parse_while()?), AtRuleKind::While => stmts.append(&mut self.parse_while()?),
AtRuleKind::Charset => { AtRuleKind::Charset => {
if self.flags.in_function() {
return Err((
"This at-rule is not allowed here.",
kind_string.span,
)
.into());
}
read_until_semicolon_or_closing_curly_brace(self.toks)?; read_until_semicolon_or_closing_curly_brace(self.toks)?;
if let Some(Token { kind: ';', .. }) = self.toks.peek() { if let Some(Token { kind: ';', .. }) = self.toks.peek() {
self.toks.next(); self.toks.next();
@ -401,7 +417,11 @@ impl<'a> Parser<'a> {
self.whitespace(); self.whitespace();
match comment.node { match comment.node {
Comment::Silent => continue, Comment::Silent => continue,
Comment::Loud(s) => stmts.push(Stmt::Comment(s)), Comment::Loud(s) => {
if !self.flags.in_function() {
stmts.push(Stmt::Comment(s));
}
}
} }
} }
'\u{0}'..='\u{8}' | '\u{b}'..='\u{1f}' => { '\u{0}'..='\u{8}' | '\u{b}'..='\u{1f}' => {
@ -414,6 +434,13 @@ impl<'a> Parser<'a> {
// dart-sass seems to special-case the error message here? // dart-sass seems to special-case the error message here?
'!' | '{' => return Err(("expected \"}\".", *pos).into()), '!' | '{' => return Err(("expected \"}\".", *pos).into()),
_ => { _ => {
if self.flags.in_function() {
return Err((
"Functions can only contain variable declarations and control directives.",
self.span_before
)
.into());
}
if self.flags.in_keyframes() { if self.flags.in_keyframes() {
match self.is_selector_or_style()? { match self.is_selector_or_style()? {
SelectorOrStyle::Style(property, value) => { SelectorOrStyle::Style(property, value) => {
@ -526,6 +553,12 @@ impl<'a> Parser<'a> {
found_curly = true; found_curly = true;
break; break;
} }
'\\' => {
string.push('\\');
if let Some(Token { kind, .. }) = self.toks.next() {
string.push(kind);
}
}
'!' => { '!' => {
if peek_ident_no_interpolation(self.toks, false, self.span_before)?.node if peek_ident_no_interpolation(self.toks, false, self.span_before)?.node
== "optional" == "optional"
@ -718,6 +751,10 @@ impl<'a> Parser<'a> {
impl<'a> Parser<'a> { impl<'a> Parser<'a> {
fn parse_unknown_at_rule(&mut self, name: String) -> SassResult<Stmt> { fn parse_unknown_at_rule(&mut self, name: String) -> SassResult<Stmt> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
let mut params = String::new(); let mut params = String::new();
self.whitespace(); self.whitespace();
if let Some(Token { kind: ';', .. }) | None = self.toks.peek() { if let Some(Token { kind: ';', .. }) | None = self.toks.peek() {
@ -781,6 +818,10 @@ impl<'a> Parser<'a> {
} }
fn parse_media(&mut self) -> SassResult<Stmt> { fn parse_media(&mut self) -> SassResult<Stmt> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
let query = self.parse_media_query_list()?; let query = self.parse_media_query_list()?;
self.whitespace(); self.whitespace();
@ -877,6 +918,9 @@ impl<'a> Parser<'a> {
} }
fn parse_extend(&mut self) -> SassResult<()> { fn parse_extend(&mut self) -> SassResult<()> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
// todo: track when inside ruleset or `@content` // todo: track when inside ruleset or `@content`
// if !self.in_style_rule && !self.in_mixin && !self.in_content_block { // 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()); // return Err(("@extend may only be used within style rules.", self.span_before).into());
@ -945,6 +989,10 @@ impl<'a> Parser<'a> {
} }
fn parse_supports(&mut self) -> SassResult<Stmt> { fn parse_supports(&mut self) -> SassResult<Stmt> {
if self.flags.in_function() {
return Err(("This at-rule is not allowed here.", self.span_before).into());
}
let params = self.parse_media_args()?; let params = self.parse_media_args()?;
if params.is_empty() { if params.is_empty() {

View File

@ -15,8 +15,7 @@ use crate::{
error::SassResult, error::SassResult,
unit::Unit, unit::Unit,
utils::{ utils::{
devour_whitespace, eat_whole_number, read_until_closing_paren, devour_whitespace, eat_whole_number, read_until_closing_paren, IsWhitespace, ParsedNumber,
read_until_closing_square_brace, IsWhitespace, ParsedNumber,
}, },
value::{Number, SassFunction, SassMap, Value}, value::{Number, SassFunction, SassMap, Value},
Token, Token,
@ -30,7 +29,6 @@ use super::super::Parser;
enum IntermediateValue { enum IntermediateValue {
Value(HigherIntermediateValue), Value(HigherIntermediateValue),
Op(Op), Op(Op),
Bracketed(Vec<Token>),
Paren(Vec<Token>), Paren(Vec<Token>),
Comma, Comma,
Whitespace, Whitespace,
@ -107,7 +105,7 @@ impl<'a> Parser<'a> {
comma_separated.push(space_separated.pop().unwrap()); comma_separated.push(space_separated.pop().unwrap());
} else { } else {
let mut span = space_separated let mut span = space_separated
.get(0) .first()
.ok_or(("Expected expression.", val.span))? .ok_or(("Expected expression.", val.span))?
.span; .span;
comma_separated.push( comma_separated.push(
@ -127,22 +125,6 @@ impl<'a> Parser<'a> {
); );
} }
} }
IntermediateValue::Bracketed(t) => {
last_was_whitespace = false;
space_separated.push(
HigherIntermediateValue::Literal(
match iter.parser.parse_value_from_vec(t, in_paren)?.node {
Value::List(v, sep, Brackets::None) => {
Value::List(v, sep, Brackets::Bracketed)
}
v => {
Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed)
}
},
)
.span(val.span),
)
}
IntermediateValue::Paren(t) => { IntermediateValue::Paren(t) => {
last_was_whitespace = false; last_was_whitespace = false;
space_separated.push(iter.parse_paren(Spanned { space_separated.push(iter.parse_paren(Spanned {
@ -657,28 +639,42 @@ impl<'a> Parser<'a> {
.span(span_start.merge(span)) .span(span_start.merge(span))
} }
'[' => { '[' => {
let mut span = self.toks.next().unwrap().pos(); let mut span = self.span_before;
self.toks.next();
self.whitespace_or_comment(); self.whitespace_or_comment();
let mut inner = match read_until_closing_square_brace(self.toks) {
Ok(v) => v, if let Some(Token { kind: ']', pos }) = self.toks.peek() {
Err(e) => return Some(Err(e)), span = span.merge(*pos);
}; self.toks.next();
if let Some(last_tok) = inner.pop() {
if last_tok.kind != ']' {
return Some(Err(("expected \"]\".", span).into()));
}
span = span.merge(last_tok.pos());
}
if inner.is_empty() {
IntermediateValue::Value(HigherIntermediateValue::Literal(Value::List( IntermediateValue::Value(HigherIntermediateValue::Literal(Value::List(
Vec::new(), Vec::new(),
ListSeparator::Space, ListSeparator::Space,
Brackets::Bracketed, Brackets::Bracketed,
))) )))
} else {
IntermediateValue::Bracketed(inner)
}
.span(span) .span(span)
} else {
// todo: we don't know if we're `in_paren` here
let inner = match self.parse_value(false, &|toks| {
matches!(toks.peek(), Some(Token { kind: ']', .. }))
}) {
Ok(v) => v,
Err(e) => return Some(Err(e)),
};
span = span.merge(inner.span);
if !matches!(self.toks.next(), Some(Token { kind: ']', .. })) {
return Some(Err(("expected \"]\".", span).into()));
}
IntermediateValue::Value(HigherIntermediateValue::Literal(match inner.node {
Value::List(els, sep, Brackets::None) => {
Value::List(els, sep, Brackets::Bracketed)
}
v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed),
}))
.span(span)
}
} }
'$' => { '$' => {
self.toks.next(); self.toks.next();
@ -1180,14 +1176,6 @@ impl<'a, 'b: 'a> IntermediateValueIterator<'a, 'b> {
IntermediateValue::Comma => { IntermediateValue::Comma => {
return Err(("Expected expression.", self.parser.span_before).into()) return Err(("Expected expression.", self.parser.span_before).into())
} }
IntermediateValue::Bracketed(t) => {
let v = self.parser.parse_value_from_vec(t, in_paren)?;
HigherIntermediateValue::Literal(match v.node {
Value::List(v, sep, Brackets::None) => Value::List(v, sep, Brackets::Bracketed),
v => Value::List(vec![v], ListSeparator::Space, Brackets::Bracketed),
})
.span(v.span)
}
IntermediateValue::Paren(t) => { IntermediateValue::Paren(t) => {
let val = self.parse_paren(Spanned { let val = self.parse_paren(Spanned {
node: t, node: t,

View File

@ -186,7 +186,13 @@ fn weave_parents(
for group in lcs { for group in lcs {
choices.push( choices.push(
chunks(&mut groups_one, &mut groups_two, |sequence| { chunks(&mut groups_one, &mut groups_two, |sequence| {
complex_is_parent_superselector(sequence.get(0).unwrap().clone(), group.clone()) complex_is_parent_superselector(
match sequence.get(0) {
Some(v) => v.clone(),
None => return true,
},
group.clone(),
)
}) })
.into_iter() .into_iter()
.map(|chunk| chunk.into_iter().flatten().collect()) .map(|chunk| chunk.into_iter().flatten().collect())
@ -340,16 +346,8 @@ fn merge_final_combinators(
) -> Option<Vec<Vec<Vec<ComplexSelectorComponent>>>> { ) -> Option<Vec<Vec<Vec<ComplexSelectorComponent>>>> {
let mut result = result.unwrap_or_default(); let mut result = result.unwrap_or_default();
if (components_one.is_empty() if (components_one.is_empty() || !components_one.back().unwrap().is_combinator())
|| !components_one && (components_two.is_empty() || !components_two.back().unwrap().is_combinator())
.get(components_one.len() - 1)
.unwrap()
.is_combinator())
&& (components_two.is_empty()
|| !components_two
.get(components_two.len() - 1)
.unwrap()
.is_combinator())
{ {
return Some(Vec::from(result)); return Some(Vec::from(result));
} }
@ -533,12 +531,8 @@ fn merge_final_combinators(
} }
(Some(combinator_one), None) => { (Some(combinator_one), None) => {
if *combinator_one == Combinator::Child && !components_two.is_empty() { if *combinator_one == Combinator::Child && !components_two.is_empty() {
if let Some(ComplexSelectorComponent::Compound(c1)) = if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() {
components_one.get(components_one.len() - 1) if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() {
{
if let Some(ComplexSelectorComponent::Compound(c2)) =
components_two.get(components_two.len() - 1)
{
if c2.is_super_selector(c1, &None) { if c2.is_super_selector(c1, &None) {
components_two.pop_back(); components_two.pop_back();
} }
@ -555,12 +549,8 @@ fn merge_final_combinators(
} }
(None, Some(combinator_two)) => { (None, Some(combinator_two)) => {
if *combinator_two == Combinator::Child && !components_one.is_empty() { if *combinator_two == Combinator::Child && !components_one.is_empty() {
if let Some(ComplexSelectorComponent::Compound(c1)) = if let Some(ComplexSelectorComponent::Compound(c1)) = components_one.back() {
components_one.get(components_one.len() - 1) if let Some(ComplexSelectorComponent::Compound(c2)) = components_two.back() {
{
if let Some(ComplexSelectorComponent::Compound(c2)) =
components_two.get(components_two.len() - 1)
{
if c1.is_super_selector(c2, &None) { if c1.is_super_selector(c2, &None) {
components_one.pop_back(); components_one.pop_back();
} }

View File

@ -561,7 +561,7 @@ fn is_simple_selector_start(c: char) -> bool {
/// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or /// with pseudo-class syntax (`:before`, `:after`, `:first-line`, or
/// `:first-letter`) /// `:first-letter`)
fn is_fake_pseudo_element(name: &str) -> bool { fn is_fake_pseudo_element(name: &str) -> bool {
match name.as_bytes().get(0) { match name.as_bytes().first() {
Some(b'a') | Some(b'A') => name.to_ascii_lowercase() == "after", Some(b'a') | Some(b'A') => name.to_ascii_lowercase() == "after",
Some(b'b') | Some(b'B') => name.to_ascii_lowercase() == "before", Some(b'b') | Some(b'B') => name.to_ascii_lowercase() == "before",
Some(b'f') | Some(b'F') => match name.to_ascii_lowercase().as_str() { Some(b'f') | Some(b'F') => match name.to_ascii_lowercase().as_str() {

View File

@ -364,7 +364,7 @@ impl SimpleSelector {
}; };
complex complex
.components .components
.get(0) .first()
.unwrap() .unwrap()
.as_compound() .as_compound()
.components .components

View File

@ -133,7 +133,8 @@ pub(crate) fn peek_escape(toks: &mut PeekMoreIterator<IntoIter<Token>>) -> SassR
toks.peek_forward(1); toks.peek_forward(1);
} }
} else { } else {
value = toks.peek_forward(1).unwrap().kind as u32; value = first.kind as u32;
toks.advance_cursor();
} }
let c = std::char::from_u32(value).ok_or(("Invalid escape sequence.", span))?; let c = std::char::from_u32(value).ok_or(("Invalid escape sequence.", span))?;

View File

@ -235,39 +235,3 @@ pub(crate) fn read_until_closing_paren(
} }
Ok(t) Ok(t)
} }
pub(crate) fn read_until_closing_square_brace(
toks: &mut PeekMoreIterator<IntoIter<Token>>,
) -> SassResult<Vec<Token>> {
let mut t = Vec::new();
let mut scope = 0;
while let Some(tok) = toks.next() {
// TODO: comments
match tok.kind {
']' => {
if scope < 1 {
t.push(tok);
return Ok(t);
} else {
scope -= 1;
}
}
'[' => scope += 1,
'"' | '\'' => {
t.push(tok);
t.extend(read_until_closing_quote(toks, tok.kind)?);
continue;
}
'\\' => {
t.push(toks.next().unwrap());
t.push(match toks.next() {
Some(tok) => tok,
None => continue,
});
}
_ => {}
}
t.push(tok)
}
Ok(t)
}

View File

@ -168,6 +168,14 @@ error!(
filter_value_after_equal_is_last_char, filter_value_after_equal_is_last_char,
"a {\n color: foo(a=a", "Error: expected \")\"." "a {\n color: foo(a=a", "Error: expected \")\"."
); );
error!(
unclosed_paren_in_nested_args,
"a { color: a(b(red); }", "Error: expected \")\"."
);
error!(
filter_rhs_missing_closing_paren,
"a { color: lighten(red=(green); }", "Error: expected \")\"."
);
test!( test!(
space_after_loud_comment, space_after_loud_comment,
"@mixin foo($x) { "@mixin foo($x) {

View File

@ -245,3 +245,11 @@ error!(
"a {foo: {bar: red", "Error: Expected identifier." "a {foo: {bar: red", "Error: Expected identifier."
); );
error!(toplevel_nullbyte, "\u{0}", "Error: expected selector."); error!(toplevel_nullbyte, "\u{0}", "Error: expected selector.");
error!(
double_escaped_bang_at_toplevel,
"\\!\\!", "Error: expected \"{\"."
);
error!(
nothing_after_escape_inside_brackets,
"a { color: [\\", "Error: Expected expression."
);

View File

@ -1874,6 +1874,14 @@ test!(
}", }",
"foo, a:current(foo),\n:current(foo) {\n color: black;\n}\n" "foo, a:current(foo),\n:current(foo) {\n color: black;\n}\n"
); );
test!(
extend_pseudo_selector_class_containing_combinator_without_rhs_selector,
":has(a >) b {
@extend b;
color: red;
}",
":has(a >) b, :has(a >) :has(a >) :has(a >) b, :has(a >) :has(a >) :has(a >) b {\n color: red;\n}\n"
);
// todo: extend_loop (massive test) // todo: extend_loop (massive test)
// todo: extend tests in folders // todo: extend tests in folders

View File

@ -195,3 +195,138 @@ test!(
}", }",
"a {\n color: foo;\n color: bar;\n}\n" "a {\n color: foo;\n color: bar;\n}\n"
); );
error!(
disallows_unknown_at_rule,
"@function foo() {
@foo;
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_media_query,
"@function foo() {
@media screen {};
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_at_root,
"@function foo() {
@at-root {};
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_charset,
"@function foo() {
@charset 'utf-8';
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_extend,
"@function foo() {
@extend a;
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_keyframes,
"@function foo() {
@keyframes foo {}
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_supports,
"@function foo() {
@supports foo {}
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_import,
"@function foo() {
@import \"foo.css\";
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_inner_function_declaration,
"@function foo() {
@function bar() {}
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_include,
"@function foo() {
@include bar;
}
a {
color: foo();
}",
"Error: This at-rule is not allowed here."
);
error!(
disallows_selectors,
"@function foo($a) {
functiona {
@return $a;
}
}
a {
color: foo(nul);
}",
"Error: Functions can only contain variable declarations and control directives."
);
test!(
allows_multiline_comment,
"@function foo($a) {
/* foo */
@return $a;
}
a {
color: foo(nul);
}",
"a {\n color: nul;\n}\n"
);

View File

@ -350,6 +350,11 @@ test!(
"ul li#foo {\n foo: bar;\n}\n" "ul li#foo {\n foo: bar;\n}\n"
); );
test!(escaped_space, "a\\ b {\n color: foo;\n}\n"); test!(escaped_space, "a\\ b {\n color: foo;\n}\n");
test!(
escaped_bang,
"\\! {\n color: red;\n}\n",
"\\! {\n color: red;\n}\n"
);
test!( test!(
multiple_consecutive_immediate_child, multiple_consecutive_immediate_child,
"> > foo {\n color: foo;\n}\n" "> > foo {\n color: foo;\n}\n"