From eb715f5ab2801bd807c428609ba8366e9400fbae Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Sun, 8 Mar 2026 23:27:33 +0000 Subject: [PATCH 1/7] Invert match_byte feature flag Signed-off-by: Nico Burns --- Cargo.toml | 3 ++- src/lib.rs | 34 +++++++++++++++++++++++++++++++++- src/serializer.rs | 4 +++- src/tokenizer.rs | 15 ++------------- 4 files changed, 40 insertions(+), 16 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 41b6bb53..c0062a80 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,9 @@ inherits = "release" debug = true [features] +default = ["fast_match_byte"] bench = [] -dummy_match_byte = [] +fast_match_byte = [] # Useful for skipping tests when execution is slow, e.g., under miri skip_long_tests = [] diff --git a/src/lib.rs b/src/lib.rs index 3968eea0..c558945b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,39 @@ pub use crate::serializer::{serialize_identifier, serialize_name, serialize_stri pub use crate::serializer::{CssStringWriter, ToCss, TokenSerializationType}; pub use crate::tokenizer::{SourceLocation, SourcePosition, Token}; pub use crate::unicode_range::UnicodeRange; -pub use cssparser_macros::*; +pub use cssparser_macros::_cssparser_internal_max_len; + +#[cfg(feature = "fast_match_byte")] +pub use cssparser_macros::match_byte; + +#[cfg(not(feature = "fast_match_byte"))] +#[macro_use] +mod mac { + /// Expand a TokenStream corresponding to the `match_byte` macro. + /// + /// ## Example + /// + /// ```rust,ignore + /// match_byte! { tokenizer.next_byte_unchecked(), + /// b'a'..b'z' => { ... } + /// b'0'..b'9' => { ... } + /// b'\n' | b'\\' => { ... } + /// foo => { ... } + /// } + /// ``` + /// + #[macro_export] + macro_rules! match_byte { + ($value:expr, $($rest:tt)* ) => { + match $value { + $( + $rest + )+ + } + }; + } +} + #[doc(hidden)] pub use phf as _cssparser_internal_phf; diff --git a/src/serializer.rs b/src/serializer.rs index ca6eda81..8af0cdc5 100644 --- a/src/serializer.rs +++ b/src/serializer.rs @@ -2,10 +2,12 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -use crate::match_byte; use std::fmt::{self, Write}; use std::str; +#[cfg(feature = "fast_match_byte")] +pub use crate::match_byte; + use super::Token; /// Trait for things the can serialize themselves in CSS syntax. diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 65562766..620a0826 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -10,19 +10,8 @@ use crate::parser::{ArbitrarySubstitutionFunctions, ParserState}; use std::char; use std::ops::Range; -#[cfg(not(feature = "dummy_match_byte"))] -use cssparser_macros::match_byte; - -#[cfg(feature = "dummy_match_byte")] -macro_rules! match_byte { - ($value:expr, $($rest:tt)* ) => { - match $value { - $( - $rest - )+ - } - }; -} +#[cfg(feature = "fast_match_byte")] +pub use crate::match_byte; /// One of the pieces the CSS input is broken into. /// From 08a61c87179ecbb773f474f7feb2b56ddfd32abd Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 00:03:16 +0000 Subject: [PATCH 2/7] Use const fn rather than proc_macro for MAX_LENGTH Signed-off-by: Nico Burns --- macros/lib.rs | 32 -------------------------------- src/lib.rs | 1 - src/macros.rs | 51 ++++++++++++++++++++++++++++++++++++--------------- src/tests.rs | 2 +- 4 files changed, 37 insertions(+), 49 deletions(-) diff --git a/macros/lib.rs b/macros/lib.rs index dc7b36e3..bcb96504 100644 --- a/macros/lib.rs +++ b/macros/lib.rs @@ -6,38 +6,6 @@ extern crate proc_macro; use proc_macro::TokenStream; -#[proc_macro] -pub fn _cssparser_internal_max_len(input: TokenStream) -> TokenStream { - struct Input { - max_length: usize, - } - - impl syn::parse::Parse for Input { - fn parse(input: syn::parse::ParseStream) -> syn::parse::Result { - let mut max_length = 0; - while !input.is_empty() { - if input.peek(syn::Token![_]) { - input.parse::().unwrap(); - continue; - } - let lit: syn::LitStr = input.parse()?; - let value = lit.value(); - if value.to_ascii_lowercase() != value { - return Err(syn::Error::new(lit.span(), "must be ASCII-lowercase")); - } - max_length = max_length.max(value.len()); - } - Ok(Input { max_length }) - } - } - - let Input { max_length } = syn::parse_macro_input!(input); - quote::quote!( - pub(super) const MAX_LENGTH: usize = #max_length; - ) - .into() -} - fn get_byte_from_lit(lit: &syn::Lit) -> u8 { if let syn::Lit::Byte(ref byte) = *lit { byte.value() diff --git a/src/lib.rs b/src/lib.rs index c558945b..99287131 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,7 +84,6 @@ pub use crate::serializer::{serialize_identifier, serialize_name, serialize_stri pub use crate::serializer::{CssStringWriter, ToCss, TokenSerializationType}; pub use crate::tokenizer::{SourceLocation, SourcePosition, Token}; pub use crate::unicode_range::UnicodeRange; -pub use cssparser_macros::_cssparser_internal_max_len; #[cfg(feature = "fast_match_byte")] pub use cssparser_macros::match_byte; diff --git a/src/macros.rs b/src/macros.rs index 67d83658..21e793e2 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -34,22 +34,34 @@ macro_rules! match_ignore_ascii_case { ( $input:expr, $( $( #[$meta: meta] )* - $( $pattern: pat )|+ $( if $guard: expr )? => $then: expr + $( $pattern:literal )|+ $( if $guard: expr )? => $then: expr ),+ + $(,_ => $fallback:expr)? $(,)? ) => { { - // This dummy module works around the feature gate - // `error[E0658]: procedural macros cannot be expanded to statements` - // by forcing the macro to be in an item context - // rather than expression/statement context, - // even though the macro only expands to items. - mod cssparser_internal { - $crate::_cssparser_internal_max_len! { - $( $( $pattern )+ )+ + #[inline(always)] + const fn const_usize_max(a: usize, b: usize) -> usize { + if a > b { + a + } else { + b } } - $crate::_cssparser_internal_to_lowercase!($input, cssparser_internal::MAX_LENGTH => lowercase); + + const MAX_LENGTH : usize = { + let mut maxlen : usize = 0; + $( + $( #[$meta] )* + // {} is necessary to work around "[E0658]: attributes on expressions are experimental" + { + $( maxlen = const_usize_max(maxlen, $pattern.len()); )+ + } + )+ + maxlen + }; + + $crate::_cssparser_internal_to_lowercase!($input, MAX_LENGTH => lowercase); // "A" is a short string that we know is different for every string pattern, // since we’ve verified that none of them include ASCII upper case letters. match lowercase.unwrap_or("A") { @@ -57,6 +69,7 @@ macro_rules! match_ignore_ascii_case { $( #[$meta] )* $( $pattern )|+ $( if $guard )? => $then, )+ + $(_ => $fallback,)? } } }; @@ -95,13 +108,21 @@ macro_rules! ascii_case_insensitive_phf_map { ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr, )+ }) => { use $crate::_cssparser_internal_phf as phf; - // See macro above for context. - mod cssparser_internal { - $crate::_cssparser_internal_max_len! { - $( $key )+ + #[inline(always)] + const fn const_usize_max(a: usize, b: usize) -> usize { + if a > b { + a + } else { + b } } + const MAX_LENGTH : usize = { + let mut maxlen : usize = 0; + $( maxlen = const_usize_max(maxlen, ($key).len()); )+ + maxlen + }; + static MAP: phf::Map<&'static str, $ValueType> = phf::phf_map! { $( $key => $value, @@ -122,7 +143,7 @@ macro_rules! ascii_case_insensitive_phf_map { } fn get(input: &str) -> Option<&'static $ValueType> { - $crate::_cssparser_internal_to_lowercase!(input, cssparser_internal::MAX_LENGTH => lowercase); + $crate::_cssparser_internal_to_lowercase!(input, MAX_LENGTH => lowercase); MAP.get(lowercase?) } } diff --git a/src/tests.rs b/src/tests.rs index 0c2acbe8..0ad18404 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1333,7 +1333,7 @@ fn utf16_columns() { #[test] fn servo_define_css_keyword_enum() { macro_rules! define_css_keyword_enum { - (pub enum $name:ident { $($variant:ident = $css:pat,)+ }) => { + (pub enum $name:ident { $($variant:ident = $css:literal,)+ }) => { #[derive(PartialEq, Debug)] pub enum $name { $($variant),+ From 2ca159fde6d41225a8c8e75fab0e6b19f2d9337d Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 00:14:33 +0000 Subject: [PATCH 3/7] Make cssparser-macros optional --- Cargo.toml | 4 ++-- color/Cargo.toml | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c0062a80..1e416c32 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,7 +20,7 @@ difference = "2.0" encoding_rs = "0.8" [dependencies] -cssparser-macros = { path = "./macros", version = "0.6.1" } +cssparser-macros = { path = "./macros", version = "0.6.1", optional = true } dtoa-short = "0.3" itoa = "1.0" phf = { version = "0.13.1", features = ["macros"] } @@ -35,7 +35,7 @@ debug = true [features] default = ["fast_match_byte"] bench = [] -fast_match_byte = [] +fast_match_byte = ["dep:cssparser-macros"] # Useful for skipping tests when execution is slow, e.g., under miri skip_long_tests = [] diff --git a/color/Cargo.toml b/color/Cargo.toml index 48a539f0..89fca060 100644 --- a/color/Cargo.toml +++ b/color/Cargo.toml @@ -12,10 +12,11 @@ edition = "2021" path = "lib.rs" [dependencies] -cssparser = { path = "..", version = "0.36" } +cssparser = { path = "..", version = "0.36", default-features = false } serde = { version = "1.0", features = ["derive"], optional = true } [features] +default = ["cssparser/default"] serde = ["cssparser/serde", "dep:serde"] [dev-dependencies] From ca3dc0d7764f040935044459fd507860663870d6 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 00:25:05 +0000 Subject: [PATCH 4/7] Make phf for color matching optional Signed-off-by: Nico Burns --- Cargo.toml | 5 ++-- src/color.rs | 2 +- src/lib.rs | 3 +++ src/macros.rs | 75 ++++++++++++++++++++++++++++++++++++++++++++++++--- src/tests.rs | 2 +- 5 files changed, 79 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e416c32..24127c3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,7 @@ encoding_rs = "0.8" cssparser-macros = { path = "./macros", version = "0.6.1", optional = true } dtoa-short = "0.3" itoa = "1.0" -phf = { version = "0.13.1", features = ["macros"] } +phf = { version = "0.13.1", features = ["macros"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true } malloc_size_of = { version = "0.1", default-features = false, optional = true } smallvec = "1.0" @@ -33,9 +33,10 @@ inherits = "release" debug = true [features] -default = ["fast_match_byte"] +default = ["fast_match_byte", "fast_match_color"] bench = [] fast_match_byte = ["dep:cssparser-macros"] +fast_match_color = ["dep:phf"] # Useful for skipping tests when execution is slow, e.g., under miri skip_long_tests = [] diff --git a/src/color.rs b/src/color.rs index 472c6478..bc52ed95 100644 --- a/src/color.rs +++ b/src/color.rs @@ -172,7 +172,7 @@ pub fn parse_hash_color(value: &[u8]) -> Result<(u8, u8, u8, f32), ()> { }) } -ascii_case_insensitive_phf_map! { +ascii_case_insensitive_map! { named_colors -> (u8, u8, u8) = { "black" => (0, 0, 0), "silver" => (192, 192, 192), diff --git a/src/lib.rs b/src/lib.rs index 99287131..60d59f83 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -116,6 +116,9 @@ mod mac { } } +// Re-exporting phf here means that the crate using the ascii_case_insensitive_phf_map macro do +// do not have to depend on phf directly. +#[cfg(feature = "fast_match_color")] #[doc(hidden)] pub use phf as _cssparser_internal_phf; diff --git a/src/macros.rs b/src/macros.rs index 21e793e2..81dc223a 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -75,6 +75,8 @@ macro_rules! match_ignore_ascii_case { }; } +#[cfg(not(feature = "fast_match_color"))] +#[macro_export] /// Define a function `$name(&str) -> Option<&'static $ValueType>` /// /// The function finds a match for the input string @@ -88,7 +90,7 @@ macro_rules! match_ignore_ascii_case { /// # fn main() {} // Make doctest not wrap everything in its own main /// /// fn color_rgb(input: &str) -> Option<(u8, u8, u8)> { -/// cssparser::ascii_case_insensitive_phf_map! { +/// cssparser::ascii_case_insensitive_map! { /// keywords -> (u8, u8, u8) = { /// "red" => (255, 0, 0), /// "green" => (0, 255, 0), @@ -100,6 +102,71 @@ macro_rules! match_ignore_ascii_case { /// ``` /// /// You can also iterate over the map entries by using `keywords::entries()`. +macro_rules! ascii_case_insensitive_map { + ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr ),+ }) => { + ascii_case_insensitive_map!($name -> $ValueType = { $( $key => $value, )+ }) + }; + ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr, )+ }) => { + + // While the obvious choice for this would be an inner module, it's not possible to + // reference from types from there, see: + // + // + // So we abuse a struct with static associated functions instead. + #[allow(non_camel_case_types)] + struct $name; + impl $name { + #[allow(dead_code)] + fn entries() -> impl Iterator { + [ $((&$key, &$value),)* ].iter().copied() + } + + fn get(input: &str) -> Option<&'static $ValueType> { + $crate::match_ignore_ascii_case!(input, + $($key => Some(&$value),)* + _ => None, + ) + } + } + } +} + +#[cfg(feature = "fast_match_color")] +#[macro_export] +/// Define a function `$name(&str) -> Option<&'static $ValueType>` +/// +/// The function finds a match for the input string +/// in a [`phf` map](https://github.com/sfackler/rust-phf) +/// and returns a reference to the corresponding value. +/// Matching is case-insensitive in the ASCII range. +/// +/// ## Example: +/// +/// ```rust +/// # fn main() {} // Make doctest not wrap everything in its own main +/// +/// fn color_rgb(input: &str) -> Option<(u8, u8, u8)> { +/// cssparser::ascii_case_insensitive_map! { +/// keywords -> (u8, u8, u8) = { +/// "red" => (255, 0, 0), +/// "green" => (0, 255, 0), +/// "blue" => (0, 0, 255), +/// } +/// } +/// keywords::get(input).cloned() +/// } +/// ``` +/// +/// You can also iterate over the map entries by using `keywords::entries()`. +macro_rules! ascii_case_insensitive_map { + ($($any:tt)+) => { + $crate::ascii_case_insensitive_phf_map!($($any)+); + }; +} + +/// Fast implementation of `ascii_case_insensitive_map!` using a phf map. +/// See `ascii_case_insensitive_map!` above for docs +#[cfg(feature = "fast_match_color")] #[macro_export] macro_rules! ascii_case_insensitive_phf_map { ($name: ident -> $ValueType: ty = { $( $key: tt => $value: expr ),+ }) => { @@ -123,7 +190,7 @@ macro_rules! ascii_case_insensitive_phf_map { maxlen }; - static MAP: phf::Map<&'static str, $ValueType> = phf::phf_map! { + static __MAP: phf::Map<&'static str, $ValueType> = phf::phf_map! { $( $key => $value, )* @@ -139,12 +206,12 @@ macro_rules! ascii_case_insensitive_phf_map { impl $name { #[allow(dead_code)] fn entries() -> impl Iterator { - MAP.entries() + __MAP.entries() } fn get(input: &str) -> Option<&'static $ValueType> { $crate::_cssparser_internal_to_lowercase!(input, MAX_LENGTH => lowercase); - MAP.get(lowercase?) + __MAP.get(lowercase?) } } } diff --git a/src/tests.rs b/src/tests.rs index 0ad18404..5845cdd1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -1085,7 +1085,7 @@ fn one_component_value_to_json(token: Token, input: &mut Parser) -> Value { /// including in string literals. #[test] fn procedural_masquerade_whitespace() { - ascii_case_insensitive_phf_map! { + ascii_case_insensitive_map! { map -> () = { " \t\n" => () } From cc05d642fc226c2d6d999512ebbf16e6586e9908 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 00:47:57 +0000 Subject: [PATCH 5/7] Group optional dependencies in Cargo.toml --- Cargo.toml | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 24127c3f..1f7380fa 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,19 +14,21 @@ rust-version = "1.68" exclude = ["src/css-parsing-tests/**", "src/big-data-url.css"] -[dev-dependencies] -serde_json = "1.0.25" -difference = "2.0" -encoding_rs = "0.8" - [dependencies] -cssparser-macros = { path = "./macros", version = "0.6.1", optional = true } dtoa-short = "0.3" itoa = "1.0" +smallvec = "1.0" + +# Optional dependencies +cssparser-macros = { path = "./macros", version = "0.6.1", optional = true } +malloc_size_of = { version = "0.1", default-features = false, optional = true } phf = { version = "0.13.1", features = ["macros"], optional = true } serde = { version = "1.0", features = ["derive"], optional = true } -malloc_size_of = { version = "0.1", default-features = false, optional = true } -smallvec = "1.0" + +[dev-dependencies] +serde_json = "1.0.25" +difference = "2.0" +encoding_rs = "0.8" [profile.profiling] inherits = "release" From 42a2a2df6b640cce6a7f6ca69c1d26d8e07b31f3 Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 00:50:22 +0000 Subject: [PATCH 6/7] Fixup CI for new features Signed-off-by: Nico Burns --- .github/workflows/main.yml | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 3f669833..5a28a324 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,13 +21,11 @@ jobs: - 1.68.0 features: - - - --features dummy_match_byte + - --no-default-features - --features malloc_size_of include: - toolchain: nightly features: --features bench - - toolchain: nightly - features: --features bench,dummy_match_byte steps: - uses: actions/checkout@v2 From 8cf964d8bc58bb7272a19f317e0884dac0749f0a Mon Sep 17 00:00:00 2001 From: Nico Burns Date: Mon, 9 Mar 2026 11:09:31 -0700 Subject: [PATCH 7/7] Bump MSRV to 1.71 --- .github/workflows/main.yml | 2 +- Cargo.toml | 2 +- color/Cargo.toml | 1 + src/parser.rs | 4 +--- 4 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5a28a324..0c2b0890 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -18,7 +18,7 @@ jobs: - nightly - beta - stable - - 1.68.0 + - 1.71.0 features: - - --no-default-features diff --git a/Cargo.toml b/Cargo.toml index 1f7380fa..0dd2f03a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,7 @@ readme = "README.md" keywords = ["css", "syntax", "parser"] license = "MPL-2.0" edition = "2018" -rust-version = "1.68" +rust-version = "1.71" exclude = ["src/css-parsing-tests/**", "src/big-data-url.css"] diff --git a/color/Cargo.toml b/color/Cargo.toml index 89fca060..077b9196 100644 --- a/color/Cargo.toml +++ b/color/Cargo.toml @@ -7,6 +7,7 @@ documentation = "https://docs.rs/cssparser-color/" repository = "https://github.com/servo/rust-cssparser" license = "MPL-2.0" edition = "2021" +rust-version = "1.71" [lib] path = "lib.rs" diff --git a/src/parser.rs b/src/parser.rs index a7cab1f2..d7df9a69 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -659,9 +659,7 @@ impl<'i: 't, 't> Parser<'i, 't> { .input .cached_token .as_ref() - .map_or(false, |cached_token| { - cached_token.start_position == token_start_position - }); + .is_some_and(|cached_token| cached_token.start_position == token_start_position); let token = if using_cached_token { let cached_token = self.input.cached_token.as_ref().unwrap(); self.input.tokenizer.reset(&cached_token.end_state);