diff --git a/src/css/selectors/parser.rs b/src/css/selectors/parser.rs index ac6dd4d8edfb..9404d01ccbc8 100644 --- a/src/css/selectors/parser.rs +++ b/src/css/selectors/parser.rs @@ -1222,7 +1222,7 @@ impl<'a> SelectorParser<'a> { // `::View-Transition-Group(..)` fall through to `CustomFunction`, // so look up `name` verbatim with no case folding. // - // PERF: 6 entries with near-unique lengths (3/10/19/19/21/26) — + // PERF: 7 entries with near-unique lengths (3/6/10/19/19/21/26) — // a length-gated `match` rejects the overwhelmingly-common miss path // (unknown `::-webkit-foo(...)` etc.) on a single `usize` compare, // versus a hash lookup's hash + table load + slice compare. Only @@ -1236,6 +1236,11 @@ impl<'a> SelectorParser<'a> { selector: Box::new(Selector::parse(self, input)?), }); } + 6 if name == b"picker" => { + return Ok(PseudoElement::PickerFunction { + identifier: Ident::parse(input)?, + }); + } 10 if name == b"cue-region" => { return Ok(PseudoElement::CueRegionFunction { selector: Box::new(Selector::parse(self, input)?), @@ -1552,6 +1557,9 @@ fn lookup_pseudo_element(name: &[u8]) -> Option { b"-webkit-scrollbar-corner" => PE::WebkitScrollbar(WS::Corner), b"-webkit-resizer" => PE::WebkitScrollbar(WS::Resizer), b"view-transition" => PE::ViewTransition, + b"details-content" => PE::DetailsContent, + b"picker-icon" => PE::PickerIcon, + b"checkmark" => PE::Checkmark, _ => return None, } }) } @@ -2959,6 +2967,17 @@ pub enum PseudoElement { /// A part name selector. part_name: ViewTransitionPartName, }, + /// The [::details-content](https://drafts.csswg.org/css-pseudo-4/#details-content-pseudo) pseudo element. + DetailsContent, + /// The [::picker-icon](https://drafts.csswg.org/css-forms-1/#picker-icon-pseudo) pseudo element. + PickerIcon, + /// The [::checkmark](https://drafts.csswg.org/css-forms-1/#checkmark-pseudo) pseudo element. + Checkmark, + /// The [::picker()](https://drafts.csswg.org/css-forms-1/#picker-pseudo) functional pseudo element. + PickerFunction { + /// The identifier argument, e.g. `select` in `::picker(select)`. + identifier: Ident, + }, /// An unknown pseudo element. Custom { /// The name of the pseudo element. @@ -3088,6 +3107,10 @@ impl fmt::Display for PseudoElement { Self::ViewTransitionImagePair { .. } => "view_transition_image_pair", Self::ViewTransitionOld { .. } => "view_transition_old", Self::ViewTransitionNew { .. } => "view_transition_new", + Self::DetailsContent => "details_content", + Self::PickerIcon => "picker_icon", + Self::Checkmark => "checkmark", + Self::PickerFunction { .. } => "picker_function", Self::Custom { .. } => "custom", Self::CustomFunction { .. } => "custom_function", }) diff --git a/src/css/selectors/selector.rs b/src/css/selectors/selector.rs index 2b6728268c4c..3109fdd519d9 100644 --- a/src/css/selectors/selector.rs +++ b/src/css/selectors/selector.rs @@ -1255,6 +1255,14 @@ pub(crate) mod serialize { part_name.to_css(dest)?; dest.write_char(b')')?; } + PseudoElement::DetailsContent => dest.write_str(b"::details-content")?, + PseudoElement::PickerIcon => dest.write_str(b"::picker-icon")?, + PseudoElement::Checkmark => dest.write_str(b"::checkmark")?, + PseudoElement::PickerFunction { identifier } => { + dest.write_str(b"::picker(")?; + identifier.to_css(dest)?; + dest.write_char(b')')?; + } PseudoElement::Custom { name } => { dest.write_str(b"::")?; return dest.serialize_identifier(name); diff --git a/test/bundler/css/forms-pseudo-elements-41120.test.ts b/test/bundler/css/forms-pseudo-elements-41120.test.ts new file mode 100644 index 000000000000..bdd33443ae57 --- /dev/null +++ b/test/bundler/css/forms-pseudo-elements-41120.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "bun:test"; +import { tempDir } from "harness"; +import path from "node:path"; + +// https://github.com/oven-sh/bun/issues/41120 +// ::details-content, ::picker(), ::checkmark and ::picker-icon are known +// pseudo-elements (css-pseudo-4 and css-forms-1, also in lightningcss's +// tables). Bundling them must not emit an "Unsupported pseudo-class or +// pseudo-element" warning. +describe("css", () => { + test("known form pseudo-elements do not warn (#41120)", async () => { + using dir = tempDir("css-41120", { + "in.css": ` + .a::details-content { height: auto } + .b::picker(select) { border: none } + .c::checkmark { color: teal } + .d::picker-icon { rotate: 90deg } + `, + }); + const result = await Bun.build({ + entrypoints: [path.join(String(dir), "in.css")], + minify: true, + throw: true, + }); + expect(result.logs.map(String)).toEqual([]); + const out = await result.outputs[0].text(); + expect(out.trim()).toBe( + ".a::details-content{height:auto}.b::picker(select){border:none}.c::checkmark{color:teal}.d::picker-icon{rotate:90deg}", + ); + }); + + test("the pseudo-element lookup is case-insensitive (#41120)", async () => { + using dir = tempDir("css-41120-case", { + "in.css": ` + .a::DETAILS-CONTENT { height: auto } + .b::Checkmark { color: teal } + `, + }); + const result = await Bun.build({ + entrypoints: [path.join(String(dir), "in.css")], + minify: true, + throw: true, + }); + expect(result.logs.map(String)).toEqual([]); + const out = await result.outputs[0].text(); + expect(out.trim()).toBe(".a::details-content{height:auto}.b::checkmark{color:teal}"); + }); +});