diff --git a/src/evaluate/env.rs b/src/evaluate/env.rs index 0fde857..97e3bb2 100644 --- a/src/evaluate/env.rs +++ b/src/evaluate/env.rs @@ -263,7 +263,7 @@ impl Environment { for name in (*self.scopes.global_variables()).borrow().keys() { if (*module).borrow().var_exists(*name) { return Err(( - format!("This module and the new module both define a variable named \"{}\".", name = name) + format!("This module and the new module both define a variable named \"{name}\".", name = name) , span).into()); } } diff --git a/src/parse/value.rs b/src/parse/value.rs index 6f4960d..83c57d7 100644 --- a/src/parse/value.rs +++ b/src/parse/value.rs @@ -60,7 +60,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { } if value_parser.inside_bracketed_list { - let start = parser.toks().cursor(); + let bracket_start = parser.toks().cursor(); parser.expect_char('[')?; parser.whitespace()?; @@ -71,14 +71,12 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { separator: ListSeparator::Undecided, brackets: Brackets::Bracketed, }) - .span(parser.toks_mut().span_from(start))); + .span(parser.toks_mut().span_from(bracket_start))); } - - Some(start) - } else { - None }; + value_parser.start = parser.toks().cursor(); + value_parser.single_expression = Some(value_parser.parse_single_expression(parser)?); let mut value = value_parser.parse_value(parser)?; @@ -392,6 +390,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { self.reset_state(parser)?; continue; } + // todo: does this branch ever get hit } if self.single_expression.is_none() { @@ -400,7 +399,7 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { self.resolve_space_expressions(parser)?; - // [resolveSpaceExpressions can modify [singleExpression_], but it + // [resolveSpaceExpressions] can modify [singleExpression_], but it // can't set it to null`. self.comma_expressions .get_or_insert_with(Default::default) @@ -1368,62 +1367,6 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { .span(span)) } - fn try_parse_url_contents( - parser: &mut P, - name: Option, - ) -> SassResult> { - let start = parser.toks().cursor(); - - if !parser.scan_char('(') { - return Ok(None); - } - - parser.whitespace_without_comments(); - - // Match Ruby Sass's behavior: parse a raw URL() if possible, and if not - // backtrack and re-parse as a function expression. - let mut buffer = Interpolation::new(); - buffer.add_string(name.unwrap_or_else(|| "url".to_owned())); - buffer.add_char('('); - - while let Some(next) = parser.toks().peek() { - match next.kind { - '\\' => { - buffer.add_string(parser.parse_escape(false)?); - } - '!' | '%' | '&' | '*'..='~' | '\u{80}'..=char::MAX => { - parser.toks_mut().next(); - buffer.add_char(next.kind); - } - '#' => { - if matches!(parser.toks().peek_n(1), Some(Token { kind: '{', .. })) { - buffer.add_interpolation(parser.parse_single_interpolation()?); - } else { - parser.toks_mut().next(); - buffer.add_char(next.kind); - } - } - ')' => { - parser.toks_mut().next(); - buffer.add_char(next.kind); - - return Ok(Some(buffer)); - } - ' ' | '\t' | '\n' | '\r' => { - parser.whitespace_without_comments(); - - if !parser.toks().next_char_is(')') { - break; - } - } - _ => break, - } - } - - parser.toks_mut().set_cursor(start); - Ok(None) - } - pub(crate) fn try_parse_special_function( parser: &mut P, name: &str, @@ -1466,15 +1409,13 @@ impl<'a, 'c, P: StylesheetParser<'a>> ValueParser<'a, 'c, P> { buffer.add_char('('); } "url" => { - return Ok( - ValueParser::try_parse_url_contents(parser, None)?.map(|contents| { - AstExpr::String( - StringExpr(contents, QuoteKind::None), - parser.toks_mut().span_from(start), - ) - .span(parser.toks_mut().span_from(start)) - }), - ) + return Ok(parser.try_url_contents(None)?.map(|contents| { + AstExpr::String( + StringExpr(contents, QuoteKind::None), + parser.toks_mut().span_from(start), + ) + .span(parser.toks_mut().span_from(start)) + })) } _ => return Ok(None), } diff --git a/src/utils/chars.rs b/src/utils/chars.rs index afa1366..862789c 100644 --- a/src/utils/chars.rs +++ b/src/utils/chars.rs @@ -21,6 +21,6 @@ pub(crate) fn as_hex(c: char) -> u32 { '0'..='9' => c as u32 - '0' as u32, 'A'..='F' => 10 + c as u32 - 'A' as u32, 'a'..='f' => 10 + c as u32 - 'a' as u32, - _ => panic!(), + _ => unreachable!(), } } diff --git a/src/utils/map_view.rs b/src/utils/map_view.rs index e0a33ff..c3ea861 100644 --- a/src/utils/map_view.rs +++ b/src/utils/map_view.rs @@ -285,7 +285,7 @@ impl MapView for MergedMapView { } } - panic!("New entries may not be added to MergedMapView") + unreachable!("New entries may not be added to MergedMapView") } fn keys(&self) -> Vec { diff --git a/src/value/mod.rs b/src/value/mod.rs index ba85034..a98c6fd 100644 --- a/src/value/mod.rs +++ b/src/value/mod.rs @@ -55,26 +55,8 @@ impl PartialEq for Value { Value::String(s2, ..) => s1 == s2, _ => false, }, - Value::Dimension(SassNumber { - num: n, - unit, - as_slash: _, - }) => match other { - Value::Dimension(SassNumber { - num: n2, - unit: unit2, - as_slash: _, - }) => { - if !unit.comparable(unit2) { - return false; - } - - if (*unit2 == Unit::None || *unit == Unit::None) && unit != unit2 { - return false; - } - - *n == n2.convert(unit2, unit) - } + Value::Dimension(n1) => match other { + Value::Dimension(n2) => n1 == n2, _ => false, }, Value::List(list1, sep1, brackets1) => match other { diff --git a/src/value/sass_number.rs b/src/value/sass_number.rs index f31378e..a498fd4 100644 --- a/src/value/sass_number.rs +++ b/src/value/sass_number.rs @@ -189,10 +189,20 @@ impl SassNumber { impl PartialEq for SassNumber { fn eq(&self, other: &Self) -> bool { - self.num == other.num && self.unit == other.unit + if !self.unit.comparable(&other.unit) { + return false; + } + + if (other.unit == Unit::None || self.unit == Unit::None) && self.unit != other.unit { + return false; + } + + self.num == other.num.convert(&other.unit, &self.unit) } } +impl Eq for SassNumber {} + impl Add for SassNumber { type Output = SassNumber; fn add(self, rhs: SassNumber) -> Self::Output { @@ -285,5 +295,3 @@ impl Div for SassNumber { self.multiply_units(self.num.0 / rhs.num.0, rhs.unit.invert()) } } - -impl Eq for SassNumber {} diff --git a/tests/at-root.rs b/tests/at-root.rs index 9379b2f..60e7e11 100644 --- a/tests/at-root.rs +++ b/tests/at-root.rs @@ -262,6 +262,15 @@ test!( }", "@unknown {\n .bar {\n a: b;\n }\n}\n" ); +test!( + query_begins_with_interpolation, + "a { + @at-root (#{wi}th: rule) { + color: red; + } + }", + "a {\n color: red;\n}\n" +); error!( missing_closing_curly_brace, "@at-root {", "Error: expected \"}\"." diff --git a/tests/color.rs b/tests/color.rs index f6d95f9..ced9d2e 100644 --- a/tests/color.rs +++ b/tests/color.rs @@ -635,3 +635,26 @@ error!( single_arg_saturate_expects_number, "a {\n color: saturate(red);\n}\n", "Error: $amount: red is not a number." ); +error!( + hex_color_starts_with_number_non_hex_digit_at_position_2, + "a {\n color: #0zz;\n}\n", "Error: Expected hex digit." +); +error!( + hex_color_starts_with_number_non_hex_digit_at_position_3, + "a {\n color: #00z;\n}\n", "Error: Expected hex digit." +); +test!( + hex_color_starts_with_number_non_hex_digit_at_position_4, + "a {\n color: #000z;\n}\n", + "a {\n color: #000 z;\n}\n" +); +test!( + #[ignore = "we don't emit 4 character hex colors correctly"] + hex_color_starts_with_number_non_hex_digit_at_position_5, + "a {\n color: #0000z;\n}\n", + "a {\n color: rgba(0, 0, 0, 0) z;\n}\n" +); +error!( + hex_color_starts_with_number_non_hex_digit_at_position_6, + "a {\n color: #00000z;\n}\n", "Error: Expected hex digit." +); diff --git a/tests/comments.rs b/tests/comments.rs index d915198..4785bfa 100644 --- a/tests/comments.rs +++ b/tests/comments.rs @@ -170,3 +170,4 @@ test!( "a {/**/}", "a { /**/ }\n" ); +test!(silent_comment_as_child, "a {\n// silent\n}\n", ""); diff --git a/tests/equality.rs b/tests/equality.rs index 2b98c01..9f5287b 100644 --- a/tests/equality.rs +++ b/tests/equality.rs @@ -267,3 +267,10 @@ test!( }", "a {\n color: true;\n}\n" ); +test!( + calculation_equality_converts_units, + "a { + color: calc(1in + 1rem) == calc(2.54cm + 1rem); + }", + "a {\n color: true;\n}\n" +); diff --git a/tests/error.rs b/tests/error.rs index 36e6852..50d5376 100644 --- a/tests/error.rs +++ b/tests/error.rs @@ -273,3 +273,7 @@ error!( nothing_after_dot_in_value_preceded_by_minus_sign, "a { color: -.", "Error: Expected digit." ); +error!( + nothing_after_bang_in_space_separated_list, + "a { color: a !", r#"Error: Expected "important"."# +); diff --git a/tests/functions.rs b/tests/functions.rs index 09d93b1..3bc6841 100644 --- a/tests/functions.rs +++ b/tests/functions.rs @@ -409,6 +409,13 @@ test!( }", "a {\n color: before;\n}\n" ); +test!( + can_parse_module_variable_declaration, + "@function foo() { + foo.$bar: red; + }", + "" +); error!( function_no_return, "@function foo() {} diff --git a/tests/imports.rs b/tests/imports.rs index fa40860..4893912 100644 --- a/tests/imports.rs +++ b/tests/imports.rs @@ -523,6 +523,72 @@ test!( ); error!(unclosed_single_quote, r#"@import '"#, "Error: Expected '."); error!(unclosed_double_quote, r#"@import ""#, "Error: Expected \"."); +error!( + dynamic_disallowed_inside_if, + r#"@if true { + @import "foo"; + }"#, + "Error: This at-rule is not allowed here." +); +error!( + dynamic_disallowed_inside_while, + r#"@while true { + @import "foo"; + }"#, + "Error: This at-rule is not allowed here." +); +error!( + dynamic_disallowed_inside_for, + r#"@for $i from 0 through 1 { + @import "foo"; + }"#, + "Error: This at-rule is not allowed here." +); +error!( + dynamic_disallowed_inside_each, + r#"@each $i in a { + @import "foo"; + }"#, + "Error: This at-rule is not allowed here." +); +test!( + static_allowed_inside_if, + r#"@if true { + @import "foo.css"; + }"#, + "@import \"foo.css\";\n" +); +test!( + static_allowed_inside_while, + r#" + $a: 0; + @while $a == 0 { + @import "foo.css"; + $a: 1; + }"#, + "@import \"foo.css\";\n" +); +test!( + static_allowed_inside_for, + r#"@for $i from 0 to 1 { + @import "foo.css"; + }"#, + "@import \"foo.css\";\n" +); +test!( + static_allowed_inside_each, + r#"@each $i in a { + @import "foo.css"; + }"#, + "@import \"foo.css\";\n" +); +error!( + dynamic_disallowed_inside_mixin, + r#"@mixin foo { + @import "foo"; + }"#, + "Error: This at-rule is not allowed here." +); // todo: edge case tests for plain css imports moved to top // todo: test for calling paths, e.g. `grass b\index.scss` diff --git a/tests/list.rs b/tests/list.rs index 51fc535..bd63c71 100644 --- a/tests/list.rs +++ b/tests/list.rs @@ -412,6 +412,26 @@ test!( "a {\n color: [null];\n}\n", "a {\n color: [];\n}\n" ); +test!( + space_separated_bracketed_list_in_parens, + "a {\n color: ([a b]);\n}\n", + "a {\n color: [a b];\n}\n" +); +test!( + does_not_eval_division_inside_space_separated_bracketed_list_in_parens, + "a {\n color: ([1/2 1/2]);\n}\n", + "a {\n color: [1/2 1/2];\n}\n" +); +test!( + comma_separated_bracketed_list_in_parens, + "a {\n color: ([a, b]);\n}\n", + "a {\n color: [a, b];\n}\n" +); +test!( + does_not_eval_division_inside_comma_separated_bracketed_list_in_parens, + "a {\n color: ([1/2, 1/2]);\n}\n", + "a {\n color: [1/2, 1/2];\n}\n" +); test!( comma_separated_list_has_element_beginning_with_capital_A, "a {\n color: a, A, \"Noto Color Emoji\";\n}\n", diff --git a/tests/map.rs b/tests/map.rs index 4087f73..6d3cb86 100644 --- a/tests/map.rs +++ b/tests/map.rs @@ -280,6 +280,27 @@ test!( "a {\n color: (a: b)==(a: c);\n}\n", "a {\n color: false;\n}\n" ); +test!( + important_as_key, + "a {\n color: inspect((a: b, !important: c));\n}\n", + "a {\n color: (a: b, !important: c);\n}\n" +); +error!( + bang_identifier_not_important_as_key, + "a {\n color: inspect((a: b, !a: c));\n}\n", r#"Error: expected ")"."# +); +error!( + bang_identifier_not_important_but_starts_with_i_as_key, + "a {\n color: inspect((a: b, !i: c));\n}\n", r#"Error: Expected "important"."# +); +error!( + bang_identifier_not_important_ascii_whitespace_as_key, + "a {\n color: inspect((a: b, ! : c));\n}\n", r#"Error: Expected "important"."# +); +error!( + bang_identifier_not_important_loud_comment_as_key, + "a {\n color: inspect((a: b, !/**/: c));\n}\n", r#"Error: expected ")"."# +); test!( empty_with_single_line_comments, "$foo: (\n \n // :/a.b\n \n ); @@ -314,3 +335,7 @@ error!( denies_comma_separated_list_without_parens_as_key, "$map: (a: 1, b, c, d: e);", "Error: expected \":\"." ); +error!( + nothing_after_first_comma, + "$map: (a: b,", "Error: expected \")\"." +); diff --git a/tests/media.rs b/tests/media.rs index e02f392..59fa1da 100644 --- a/tests/media.rs +++ b/tests/media.rs @@ -570,6 +570,17 @@ test!( }"#, "@media (min-width: \\0 ) {\n a {\n color: red;\n }\n}\n" ); +test!( + simple_unmergeable, + "a { + @media a { + @media b { + color: red; + } + } + }", + "" +); error!( media_query_has_quoted_closing_paren, r#"@media ('a)'w) { diff --git a/tests/plain-css.rs b/tests/plain-css.rs index d70c318..e792226 100644 --- a/tests/plain-css.rs +++ b/tests/plain-css.rs @@ -35,6 +35,32 @@ test!( "a {\n color: 1 or 2;\n}\n", grass::Options::default().input_syntax(InputSyntax::Css) ); +test!( + simple_calculation, + "a { + color: calc(1 + 1); + }", + "a {\n color: 2;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + simple_url_import, + r#"@import url("foo");"#, + "@import url(\"foo\");\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + import_no_file_extension, + r#"@import "foo";"#, + "@import \"foo\";\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); +test!( + import_with_condition, + r#"@import "foo" screen and (foo: bar);"#, + "@import \"foo\" screen and (foo: bar);\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); test!( does_not_evaluate_not, "a { @@ -155,6 +181,14 @@ error!( "Error: Operators aren't allowed in plain CSS.", grass::Options::default().input_syntax(InputSyntax::Css) ); +error!( + disallows_interpolation, + "a { + color: a#{b}c; + }", + "Error: Interpolation isn't allowed in plain CSS.", + grass::Options::default().input_syntax(InputSyntax::Css) +); test!( allows_rgb_function, "a { @@ -163,3 +197,13 @@ test!( "a {\n color: rgb(true, a, b);\n}\n", grass::Options::default().input_syntax(InputSyntax::Css) ); +test!( + simple_supports, + "@supports (foo) { + a { + color: red; + } + }", + "@supports (foo) {\n a {\n color: red;\n }\n}\n", + grass::Options::default().input_syntax(InputSyntax::Css) +); diff --git a/tests/sass.rs b/tests/sass.rs index dde0b4b..b1c0845 100644 --- a/tests/sass.rs +++ b/tests/sass.rs @@ -81,6 +81,18 @@ test!( "/* loud */\n", grass::Options::default().input_syntax(InputSyntax::Sass) ); +test!( + special_mixin_and_include_characters, + r#" +=foo + color: red + +a + +foo +"#, + "a {\n color: red;\n}\n", + grass::Options::default().input_syntax(InputSyntax::Sass) +); error!( multiline_comment_in_value_position, r#" @@ -90,3 +102,15 @@ loud */ red "Error: expected */.", grass::Options::default().input_syntax(InputSyntax::Sass) ); +error!( + document_starts_with_spaces, + r#" "#, + "Error: Indenting at the beginning of the document is illegal.", + grass::Options::default().input_syntax(InputSyntax::Sass) +); +error!( + document_starts_with_tab, + "\t", + "Error: Indenting at the beginning of the document is illegal.", + grass::Options::default().input_syntax(InputSyntax::Sass) +); diff --git a/tests/selectors.rs b/tests/selectors.rs index 628126a..accb7ff 100644 --- a/tests/selectors.rs +++ b/tests/selectors.rs @@ -927,6 +927,10 @@ error!( denies_optional_in_selector, "a !optional {}", "Error: expected \"{\"." ); +error!( + child_selector_starts_with_forward_slash, + "a { /b { } }", "Error: expected selector." +); // todo: // [attr=url] { diff --git a/tests/special-functions.rs b/tests/special-functions.rs index c9b274d..221bab1 100644 --- a/tests/special-functions.rs +++ b/tests/special-functions.rs @@ -276,6 +276,30 @@ test!( "a {\n color: calc(1dpi + 1dppx);\n}\n", "a {\n color: 97dpi;\n}\n" ); +test!( + ternary_inside_calc, + "a {\n color: calc(if(true, 1, unit(foo)));\n}\n", + "a {\n color: 1;\n}\n" +); +test!( + retains_parens_around_var_in_calc, + "a {\n color: calc((var(--a)) + 1rem);\n}\n", + "a {\n color: calc((var(--a)) + 1rem);\n}\n" +); +test!( + removes_superfluous_parens_around_function_call_in_calc, + "a {\n color: calc((foo(--a)) + 1rem);\n}\n", + "a {\n color: calc(foo(--a) + 1rem);\n}\n" +); +test!( + calculation_inside_calc, + "a {\n color: calc(calc(1px + 1rem) * calc(2px - 2in));\n}\n", + "a {\n color: calc((1px + 1rem) * -190px);\n}\n" +); +error!( + escaped_close_paren_inside_calc, + "a {\n color: calc(\\));\n}\n", r#"Error: Expected "(" or "."."# +); error!( nothing_after_last_arg, "a { color: calc(1 + 1", r#"Error: expected "+", "-", "*", "/", or ")"."# diff --git a/tests/unicode-range.rs b/tests/unicode-range.rs index 0e00d4f..5e67c6b 100644 --- a/tests/unicode-range.rs +++ b/tests/unicode-range.rs @@ -21,6 +21,31 @@ test!( "a {\n color: u+27a;\n}\n", "a {\n color: u+27a;\n}\n" ); +test!( + second_element_in_list, + "a {\n color: a u+55;\n}\n", + "a {\n color: a u+55;\n}\n" +); +test!( + escaped_lowercase_u, + "a {\n color: \\75+55;\n}\n", + "a {\n color: u55;\n}\n" +); +test!( + escaped_uppercase_u, + "a {\n color: \\55+55;\n}\n", + "a {\n color: U55;\n}\n" +); +test!( + escaped_lowercase_u_with_space_after_escape, + "a {\n color: \\75 +55;\n}\n", + "a {\n color: u55;\n}\n" +); +test!( + escaped_uppercase_u_with_space_after_escape, + "a {\n color: \\55 +55;\n}\n", + "a {\n color: U55;\n}\n" +); error!( interpolated_range, "a {\n color: U+2A#{70}C;\n}\n", "Error: Expected end of identifier." @@ -37,6 +62,19 @@ error!( length_of_6_with_question_mark, "a {\n color: U+123456?;\n}\n", "Error: Expected at most 6 digits." ); - -// todo: escaped u at start \75 and \55 -// with and without space +error!( + nothing_after_plus_lowercase, + "a {\n color: u+;\n}\n", r#"Error: Expected hex digit or "?"."# +); +error!( + nothing_after_plus_uppercase, + "a {\n color: U+;\n}\n", r#"Error: Expected hex digit or "?"."# +); +error!( + second_part_of_range_is_empty, + "a {\n color: u+55-;\n}\n", r#"Error: Expected hex digit."# +); +error!( + second_part_of_range_is_more_than_6_chars, + "a {\n color: u+55-1234567;\n}\n", r#"Error: Expected at most 6 digits."# +); diff --git a/tests/url.rs b/tests/url.rs index 1d12c13..4d55e0e 100644 --- a/tests/url.rs +++ b/tests/url.rs @@ -171,6 +171,11 @@ test!( "a {\n color: url(#);\n}\n", "a {\n color: url(#);\n}\n" ); +test!( + escaped_close_paren, + "a {\n color: url(\\));\n}\n", + "a {\n color: url(\\));\n}\n" +); error!( url_nothing_after_forward_slash_in_interpolation, "a { color: url(#{/", "Error: Expected expression."