diff --git a/packages/ppx/src/Css_to_runtime.re b/packages/ppx/src/Css_to_runtime.re index bdca89f23..273de3cd5 100644 --- a/packages/ppx/src/Css_to_runtime.re +++ b/packages/ppx/src/Css_to_runtime.re @@ -421,36 +421,59 @@ and render_selectors = (~loc, selectors) => { selectors |> List.map(((selector, _loc)) => render_selector(~loc, selector)); } -and render_style_rule = (~loc, rule: style_rule) => { - let (prelude, prelude_loc) = rule.prelude; +and render_style_rule = (~loc, ~ignore_first_level=false, rule: style_rule) => { + let starts_with_double_dot = selector => + String.starts_with(~prefix=":", selector); + let contains_ampersand = selector => String.contains(selector, '&'); + + let (prelude_ast, prelude_ast_loc) = rule.prelude; let selector_location = - Styled_ppx_css_parser.Parser_location.intersection(loc, prelude_loc); + Styled_ppx_css_parser.Parser_location.update_pos_lnum( + prelude_ast_loc, + loc, + ); - let selector_expr = + let declarations = render_declarations(~loc, rule.block) |> Builder.pexp_array(~loc=selector_location); let (delimiter, attrs) = Platform_attributes.string_delimiter(~loc=selector_location); - let selector_name = - prelude + let prelude = + prelude_ast |> render_selectors(~loc=selector_location) - |> List.map(String.trim) - |> List.map( - String_interpolation.transform( - ~attrs, - ~delimiter, - ~loc=selector_location, - ), - ) - |> Builder.pexp_array(~loc=selector_location); + |> List.map(String.trim); - Helper.Exp.apply( - ~loc=selector_location, - CSS.selectorMany(~loc=selector_location), - [(Nolabel, selector_name), (Nolabel, selector_expr)], - ); + let starts_with_double_dot_and_no_ampersand = + List.exists( + selector => { + starts_with_double_dot(selector) && !contains_ampersand(selector) + }, + prelude, + ); + if (!ignore_first_level && starts_with_double_dot_and_no_ampersand) { + Error.expr( + ~loc=selector_location, + "Ampersand is needed if selector begins with pseudo-class or pseudo-element.", + ); + } else { + let prelude_transformed = + prelude + |> List.map( + String_interpolation.transform( + ~attrs, + ~delimiter, + ~loc=selector_location, + ), + ) + |> Builder.pexp_array(~loc=selector_location); + Helper.Exp.apply( + ~loc=selector_location, + CSS.selectorMany(~loc=selector_location), + [(Nolabel, prelude_transformed), (Nolabel, declarations)], + ); + }; }; let addLabel = (~loc, label, emotionExprs) => [ @@ -552,7 +575,8 @@ If your intent is to apply the declaration to all elements, use the universal se ruleList |> List.map(rule => { switch (rule) { - | Style_rule(style_rule) => render_style_rule(~loc, style_rule) + | Style_rule(style_rule) => + render_style_rule(~loc, ~ignore_first_level=true, style_rule) | At_rule(at_rule) => render_at_rule(~loc, at_rule) | _ => Error.expr(~loc=stylesheet_loc, onlyStyleRulesAndAtRulesSupported) diff --git a/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/input.re b/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/input.re new file mode 100644 index 000000000..6cadce8e6 --- /dev/null +++ b/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/input.re @@ -0,0 +1,11 @@ +let selectors = [%cx + {| + :hover { + color: red; + } + + ::first-line { + color: red; + } +|} +]; diff --git a/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/run.t b/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/run.t new file mode 100644 index 000000000..cfa2eef3f --- /dev/null +++ b/packages/ppx/test/css-support/nested-pseudo-class-or-element-without-ampersand.t/run.t @@ -0,0 +1,32 @@ +This test ensures the ppx generates the correct output against styled-ppx.native +If this test fail means that the module is not in sync with the ppx + + $ cat > dune-project << EOF + > (lang dune 3.10) + > EOF + + $ cat > dune << EOF + > (executable + > (name input) + > (libraries styled-ppx.native) + > (preprocess (pps styled-ppx))) + > EOF + + $ dune build + File "input.re", lines 2-3, characters 0-9: + Error: Ampersand is needed if selector begins with pseudo-class or + pseudo-element. + [1] + + $ dune describe pp ./input.re | sed '1,/^];$/d' + + let selectors = + CSS.style([| + CSS.label("selectors"), + [%ocaml.error + "Ampersand is needed if selector begins with pseudo-class or pseudo-element." + ], + [%ocaml.error + "Ampersand is needed if selector begins with pseudo-class or pseudo-element." + ], + |]); diff --git a/packages/ppx/test/css-support/selectors.t/input.re b/packages/ppx/test/css-support/selectors.t/input.re index 486657031..1485af274 100644 --- a/packages/ppx/test/css-support/selectors.t/input.re +++ b/packages/ppx/test/css-support/selectors.t/input.re @@ -6,7 +6,7 @@ let _chart = [%cx .recharts-cartesian-grid-horizontal { line { - :nth-last-child(1), :nth-last-child(2) { + &:nth-last-child(1), &:nth-last-child(2) { stroke-opacity: 0; } } @@ -15,7 +15,7 @@ let _chart = [%cx .recharts-scatter .recharts-scatter-symbol .recharts-symbols { opacity: 0.8; - :hover { + &:hover { opacity: 1; } } diff --git a/packages/ppx/test/css-support/selectors.t/run.t b/packages/ppx/test/css-support/selectors.t/run.t index 93aeac4e5..ea3f222ca 100644 --- a/packages/ppx/test/css-support/selectors.t/run.t +++ b/packages/ppx/test/css-support/selectors.t/run.t @@ -25,7 +25,7 @@ If this test fail means that the module is not in sync with the ppx [|{js|line|js}|], [| CSS.selectorMany( - [|{js|:nth-last-child(1)|js}, {js|:nth-last-child(2)|js}|], + [|{js|&:nth-last-child(1)|js}, {js|&:nth-last-child(2)|js}|], [|CSS.SVG.strokeOpacity(`num(0.))|], ), |], @@ -38,7 +38,7 @@ If this test fail means that the module is not in sync with the ppx |], [| CSS.opacity(0.8), - CSS.selectorMany([|{js|:hover|js}|], [|CSS.opacity(1.)|]), + CSS.selectorMany([|{js|&:hover|js}|], [|CSS.opacity(1.)|]), |], ), |]); diff --git a/packages/ppx/test/native/Selector_test.re b/packages/ppx/test/native/Selector_test.re index 0d3fd3c71..b63aa8187 100644 --- a/packages/ppx/test/native/Selector_test.re +++ b/packages/ppx/test/native/Selector_test.re @@ -5,11 +5,11 @@ let loc = Location.none; let simple_tests = [ ( - ":before { display: none; }", - [%expr [%cx ":before { display: none; }"]], + "&:before { display: none; }", + [%expr [%cx "&:before { display: none; }"]], [%expr CSS.style([| - CSS.selectorMany([|{js|:before|js}|], [|CSS.display(`none)|]), + CSS.selectorMany([|{js|&:before|js}|], [|CSS.display(`none)|]), |]) ], ), @@ -465,12 +465,12 @@ let complex_tests = [ ], ), ( - ":is(h1, $(gap), h3):has(+ :is(h2, h3, h4))", - [%expr [%cx {| :is(h1, $(gap), h3):has(+ :is(h2, h3, h4)) {} |}]], + "&:is(h1, $(gap), h3):has(+ :is(h2, h3, h4))", + [%expr [%cx {| &:is(h1, $(gap), h3):has(+ :is(h2, h3, h4)) {} |}]], [%expr CSS.style([| CSS.selectorMany( - [|{js|:is(h1, |js} ++ gap ++ {js|, h3):has(+ :is(h2, h3, h4))|js}|], + [|{js|&:is(h1, |js} ++ gap ++ {js|, h3):has(+ :is(h2, h3, h4))|js}|], [||], ), |]) diff --git a/packages/runtime/test/test_styles.ml b/packages/runtime/test/test_styles.ml index adb4291d6..70e0a1323 100644 --- a/packages/runtime/test/test_styles.ml +++ b/packages/runtime/test/test_styles.ml @@ -78,11 +78,11 @@ let avoid_hash_collision = {| color: $(color); - :disabled { + &:disabled { color: $(disabledColor); } - :hover { + &:hover { color: $(hoverColor); } |}] @@ -93,8 +93,8 @@ let avoid_hash_collision = {| display: flex; - :before, - :after { + &:before, + &:after { content: ''; flex: 0 0 $(padding); } @@ -106,8 +106,8 @@ let avoid_hash_collision = {| display: flex; - :before, - :after { + &:before, + &:after { content: ''; flex: 0 1 $(padding); } @@ -288,7 +288,7 @@ let ampersand_everywhere_2 = display: none; } } - :first-child { + &:first-child { .felipe & { display: none; } @@ -316,7 +316,7 @@ let ampersand_everywhere_3 = padding: 0; list-style-type: none; - :first-child { + &:first-child { .$(hasTwoColumnList) & { @media $(desktopDown) { padding-bottom: $(px16); @@ -342,9 +342,9 @@ let pseudo_selectors_everywhere = {| display: block; - ::before { + &::before { display: none; - ::after { + &::after { display: none; } } @@ -394,7 +394,7 @@ let selector_nested_with_mq_and_declarations = li { list-style-type: none; - ::before { + &::before { position: absolute; left: -20px; content: "✓"; @@ -581,11 +581,11 @@ let functional_pseudo = [%cx {| .foo, .bar { - :is(ol, ul, menu:unsupported) :is(ol, ul) { + &:is(ol, ul, menu:unsupported) :is(ol, ul) { color: green; } - :is(ol, ul) :is(ol, ul) ol { + &:is(ol, ul) :is(ol, ul) ol { list-style-type: lower-greek; color: chocolate; } @@ -598,16 +598,16 @@ let functional_pseudo = color: darkmagenta; } - :where(ol, ul, menu:unsupported) :where(ol, ul) { + &:where(ol, ul, menu:unsupported) :where(ol, ul) { color: green; } - :where(ol, ul) :where(ol, ul) ol { + &:where(ol, ul) :where(ol, ul) ol { list-style-type: lower-greek; color: chocolate; } - :is(h1, h2, h3):has(+ :is(h2, h3, h4)) { + &:is(h1, h2, h3):has(+ :is(h2, h3, h4)) { margin: 0 0 0.25rem 0; } @@ -746,8 +746,8 @@ let selector_nested_with_pseudo_2 = [%cx {| position: relative; - :hover { - ::after { + &:hover { + &::after { top: 50px; } } @@ -1125,19 +1125,19 @@ let pseudo_selectors = padding-bottom: 9px; border-radius: 0; - ::placeholder { + &::placeholder { color: currentColor; } - :hover { + &:hover { border: 1px solid transparent; } - :focus { + &:focus { outline: none; } - :disabled { + &:disabled { color: transparent; }|}] in @@ -1161,8 +1161,8 @@ let real_world = &.$(buttonActive) { margin: 0px; - ::before, - ::after { + &::before, + &::after { top: 40px; } } @@ -1215,7 +1215,7 @@ let global_with_selector = test "global_with_selector" @@ fun () -> [%global {| html { line-height: 1.15; } - a { :hover { padding: 0; } } + a { &:hover { padding: 0; } } |}]; let css = get_string_style_rules () in assert_string css