From bfde1cd8356025a6d00ff06342891d580b91cb18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Emilio=20Cobos=20=C3=81lvarez?= Date: Thu, 31 Mar 2022 22:05:02 +0000 Subject: [PATCH] Bug 1762088 - Allow to derive Parse/ToCss/SpecifiedValueInfo on bitflags. r=firefox-style-system-reviewers,layout-reviewers,boris We keep getting this pattern of properties that have a set of joint and disjoint flags, and copy-pasting or writing the same parsing and serialization code in slightly different ways. container-type is one such type, and I think we should have a single way of dealing with this, thus implement deriving for various traits for bitflags, with an attribute that says which flags are single vs mixed. See docs and properties I ported. The remaining ones I left TODOs with, they are a bit trickier but can be ported with some care. Differential Revision: https://phabricator.services.mozilla.com/D142418 --- servo/components/derive_common/cg.rs | 5 + .../components/style/values/specified/box.rs | 225 +----------------- .../style/values/specified/position.rs | 1 + .../components/style/values/specified/text.rs | 94 +------- servo/components/style_derive/parse.rs | 78 +++++- .../style_derive/specified_value_info.rs | 9 +- servo/components/style_derive/to_css.rs | 98 +++++++- servo/components/style_traits/values.rs | 23 ++ 8 files changed, 222 insertions(+), 311 deletions(-) diff --git a/servo/components/derive_common/cg.rs b/servo/components/derive_common/cg.rs index b4ac5bb47772..2c5268839ffd 100644 --- a/servo/components/derive_common/cg.rs +++ b/servo/components/derive_common/cg.rs @@ -373,6 +373,11 @@ pub fn to_css_identifier(mut camel_case: &str) -> String { result } +/// Transforms foo-bar to FOO_BAR. +pub fn to_scream_case(css_case: &str) -> String { + css_case.to_uppercase().replace('-', "_") +} + /// Given "FooBar", returns "Foo" and sets `camel_case` to "Bar". fn split_camel_segment<'input>(camel_case: &mut &'input str) -> Option<&'input str> { let index = match camel_case.chars().next() { diff --git a/servo/components/style/values/specified/box.rs b/servo/components/style/values/specified/box.rs index cc8f81916ff1..bde78c31c69e 100644 --- a/servo/components/style/values/specified/box.rs +++ b/servo/components/style/values/specified/box.rs @@ -17,7 +17,6 @@ use crate::values::{CustomIdent, KeyframesName, TimelineName}; use crate::Atom; use cssparser::Parser; use num_traits::FromPrimitive; -use selectors::parser::SelectorParseErrorKind; use std::fmt::{self, Write}; use style_traits::{CssWriter, KeywordsCollectFn, ParseError}; use style_traits::{SpecifiedValueInfo, StyleParseErrorKind, ToCss}; @@ -1239,9 +1238,8 @@ impl Parse for WillChange { bitflags! { /// Values for the `touch-action` property. - #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] - /// These constants match Gecko's `NS_STYLE_TOUCH_ACTION_*` constants. - #[value_info(other_values = "auto,none,manipulation,pan-x,pan-y,pinch-zoom")] + #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem, Parse)] + #[css(bitflags(single = "none,auto,manipulation", mixed = "pan-x,pan-y,pinch-zoom"))] #[repr(C)] pub struct TouchAction: u8 { /// `none` variant @@ -1267,82 +1265,9 @@ impl TouchAction { } } -impl ToCss for TouchAction { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - if self.contains(TouchAction::AUTO) { - return dest.write_str("auto"); - } - if self.contains(TouchAction::NONE) { - return dest.write_str("none"); - } - if self.contains(TouchAction::MANIPULATION) { - return dest.write_str("manipulation"); - } - - let mut has_any = false; - macro_rules! maybe_write_value { - ($ident:path => $str:expr) => { - if self.contains($ident) { - if has_any { - dest.write_str(" ")?; - } - has_any = true; - dest.write_str($str)?; - } - }; - } - maybe_write_value!(TouchAction::PAN_X => "pan-x"); - maybe_write_value!(TouchAction::PAN_Y => "pan-y"); - maybe_write_value!(TouchAction::PINCH_ZOOM => "pinch-zoom"); - - debug_assert!(has_any); - Ok(()) - } -} - -impl Parse for TouchAction { - /// auto | none | [ pan-x || pan-y || pinch-zoom ] | manipulation - fn parse<'i, 't>( - _context: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - let mut result = TouchAction::empty(); - while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { - let flag = match_ignore_ascii_case! { &name, - "pan-x" => Some(TouchAction::PAN_X), - "pan-y" => Some(TouchAction::PAN_Y), - "pinch-zoom" => Some(TouchAction::PINCH_ZOOM), - "none" if result.is_empty() => return Ok(TouchAction::NONE), - "manipulation" if result.is_empty() => return Ok(TouchAction::MANIPULATION), - "auto" if result.is_empty() => return Ok(TouchAction::AUTO), - _ => None - }; - - let flag = match flag { - Some(flag) if !result.contains(flag) => flag, - _ => { - return Err( - input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) - ); - }, - }; - result.insert(flag); - } - - if !result.is_empty() { - Ok(result) - } else { - Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) - } - } -} - bitflags! { - #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] - #[value_info(other_values = "none,strict,content,size,layout,paint")] + #[derive(MallocSizeOf, Parse, SpecifiedValueInfo, ToComputedValue, ToCss, ToResolvedValue, ToShmem)] + #[css(bitflags(single = "none,strict,content", mixed="size,layout,paint"))] #[repr(C)] /// Constants for contain: https://drafts.csswg.org/css-contain/#contain-property pub struct Contain: u8 { @@ -1355,90 +1280,13 @@ bitflags! { /// `paint` variant, turns on paint containment const PAINT = 1 << 2; /// `strict` variant, turns on all types of containment - const STRICT = 1 << 3; + const STRICT = 1 << 3 | Contain::LAYOUT.bits | Contain::PAINT.bits | Contain::SIZE.bits; /// 'content' variant, turns on layout and paint containment - const CONTENT = 1 << 4; - /// variant with all the bits that contain: strict turns on - const STRICT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits | Contain::SIZE.bits; - /// variant with all the bits that contain: content turns on - const CONTENT_BITS = Contain::LAYOUT.bits | Contain::PAINT.bits; + const CONTENT = 1 << 4 | Contain::LAYOUT.bits | Contain::PAINT.bits; } } -impl ToCss for Contain { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - if self.is_empty() { - return dest.write_str("none"); - } - if self.contains(Contain::STRICT) { - return dest.write_str("strict"); - } - if self.contains(Contain::CONTENT) { - return dest.write_str("content"); - } - - let mut has_any = false; - macro_rules! maybe_write_value { - ($ident:path => $str:expr) => { - if self.contains($ident) { - if has_any { - dest.write_str(" ")?; - } - has_any = true; - dest.write_str($str)?; - } - }; - } - maybe_write_value!(Contain::SIZE => "size"); - maybe_write_value!(Contain::LAYOUT => "layout"); - maybe_write_value!(Contain::PAINT => "paint"); - - debug_assert!(has_any); - Ok(()) - } -} - -impl Parse for Contain { - /// none | strict | content | [ size || layout || paint ] - fn parse<'i, 't>( - _context: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - let mut result = Contain::empty(); - while let Ok(name) = input.try_parse(|i| i.expect_ident_cloned()) { - let flag = match_ignore_ascii_case! { &name, - "size" => Some(Contain::SIZE), - "layout" => Some(Contain::LAYOUT), - "paint" => Some(Contain::PAINT), - "strict" if result.is_empty() => return Ok(Contain::STRICT | Contain::STRICT_BITS), - "content" if result.is_empty() => return Ok(Contain::CONTENT | Contain::CONTENT_BITS), - "none" if result.is_empty() => return Ok(result), - _ => None - }; - - let flag = match flag { - Some(flag) if !result.contains(flag) => flag, - _ => { - return Err( - input.new_custom_error(SelectorParseErrorKind::UnexpectedIdent(name)) - ); - }, - }; - result.insert(flag); - } - - if !result.is_empty() { - Ok(result) - } else { - Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) - } - } -} - -#[allow(missing_docs)] +/// https://drafts.csswg.org/css-contain-2/#content-visibility #[cfg_attr(feature = "servo", derive(Deserialize, Serialize))] #[derive( Clone, @@ -2123,9 +1971,9 @@ impl Overflow { } bitflags! { - #[derive(MallocSizeOf, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] - #[value_info(other_values = "auto,stable,both-edges")] + #[derive(MallocSizeOf, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem, Parse)] #[repr(C)] + #[css(bitflags(single = "auto", mixed = "stable,both-edges", validate_mixed="Self::has_stable"))] /// Values for scrollbar-gutter: /// pub struct ScrollbarGutter: u8 { @@ -2138,56 +1986,9 @@ bitflags! { } } -impl ToCss for ScrollbarGutter { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - if self.is_empty() { - return dest.write_str("auto"); - } - - debug_assert!( - self.contains(ScrollbarGutter::STABLE), - "We failed to parse the syntax!" - ); - dest.write_str("stable")?; - if self.contains(ScrollbarGutter::BOTH_EDGES) { - dest.write_str(" both-edges")?; - } - - Ok(()) - } -} - -impl Parse for ScrollbarGutter { - /// auto | stable && both-edges? - fn parse<'i, 't>( - _context: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - if input.try_parse(|i| i.expect_ident_matching("auto")).is_ok() { - return Ok(ScrollbarGutter::AUTO); - } - - let mut result = ScrollbarGutter::empty(); - while let Ok(ident) = input.try_parse(|i| i.expect_ident_cloned()) { - let flag = match_ignore_ascii_case! { &ident, - "stable" => Some(ScrollbarGutter::STABLE), - "both-edges" => Some(ScrollbarGutter::BOTH_EDGES), - _ => None - }; - - match flag { - Some(flag) if !result.contains(flag) => result.insert(flag), - _ => return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)), - } - } - - if result.contains(ScrollbarGutter::STABLE) { - Ok(result) - } else { - Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) - } +impl ScrollbarGutter { + #[inline] + fn has_stable(self) -> bool { + self.intersects(Self::STABLE) } } diff --git a/servo/components/style/values/specified/position.rs b/servo/components/style/values/specified/position.rs index 26359c155c62..b40460cdcd72 100644 --- a/servo/components/style/values/specified/position.rs +++ b/servo/components/style/values/specified/position.rs @@ -556,6 +556,7 @@ impl From for u8 { } } +// TODO: Can be derived with some care. impl Parse for GridAutoFlow { /// [ row | column ] || dense fn parse<'i, 't>( diff --git a/servo/components/style/values/specified/text.rs b/servo/components/style/values/specified/text.rs index 98f1f480cc1f..31013c9434e2 100644 --- a/servo/components/style/values/specified/text.rs +++ b/servo/components/style/values/specified/text.rs @@ -232,8 +232,8 @@ impl ToComputedValue for TextOverflow { } bitflags! { - #[derive(MallocSizeOf, Serialize, SpecifiedValueInfo, ToComputedValue, ToResolvedValue, ToShmem)] - #[value_info(other_values = "none,underline,overline,line-through,blink")] + #[derive(MallocSizeOf, Parse, Serialize, SpecifiedValueInfo, ToCss, ToComputedValue, ToResolvedValue, ToShmem)] + #[css(bitflags(single = "none", mixed = "underline,overline,line-through,blink"))] #[repr(C)] /// Specified keyword values for the text-decoration-line property. pub struct TextDecorationLine: u8 { @@ -265,94 +265,6 @@ impl Default for TextDecorationLine { } } -impl Parse for TextDecorationLine { - /// none | [ underline || overline || line-through || blink ] - fn parse<'i, 't>( - _context: &ParserContext, - input: &mut Parser<'i, 't>, - ) -> Result> { - let mut result = TextDecorationLine::empty(); - - // NOTE(emilio): this loop has this weird structure because we run this - // code to parse the text-decoration shorthand as well, so we need to - // ensure we don't return an error if we don't consume the whole thing - // because we find an invalid identifier or other kind of token. - loop { - let flag: Result<_, ParseError<'i>> = input.try_parse(|input| { - let flag = try_match_ident_ignore_ascii_case! { input, - "none" if result.is_empty() => TextDecorationLine::NONE, - "underline" => TextDecorationLine::UNDERLINE, - "overline" => TextDecorationLine::OVERLINE, - "line-through" => TextDecorationLine::LINE_THROUGH, - "blink" => TextDecorationLine::BLINK, - }; - - Ok(flag) - }); - - let flag = match flag { - Ok(flag) => flag, - Err(..) => break, - }; - - if flag.is_empty() { - return Ok(TextDecorationLine::NONE); - } - - if result.contains(flag) { - return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); - } - - result.insert(flag) - } - - if !result.is_empty() { - Ok(result) - } else { - Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)) - } - } -} - -impl ToCss for TextDecorationLine { - fn to_css(&self, dest: &mut CssWriter) -> fmt::Result - where - W: Write, - { - if self.is_empty() { - return dest.write_str("none"); - } - - #[cfg(feature = "gecko")] - { - if *self == TextDecorationLine::COLOR_OVERRIDE { - return Ok(()); - } - } - - let mut writer = SequenceWriter::new(dest, " "); - let mut any = false; - - macro_rules! maybe_write { - ($ident:ident => $str:expr) => { - if self.contains(TextDecorationLine::$ident) { - any = true; - writer.raw_item($str)?; - } - }; - } - - maybe_write!(UNDERLINE => "underline"); - maybe_write!(OVERLINE => "overline"); - maybe_write!(LINE_THROUGH => "line-through"); - maybe_write!(BLINK => "blink"); - - debug_assert!(any); - - Ok(()) - } -} - impl TextDecorationLine { #[inline] /// Returns the initial value of text-decoration-line @@ -399,6 +311,7 @@ impl TextTransform { } } +// TODO: This can be simplified by deriving it. impl Parse for TextTransform { fn parse<'i, 't>( _context: &ParserContext, @@ -1194,6 +1107,7 @@ bitflags! { } } +// TODO: This can be derived with some care. impl Parse for TextUnderlinePosition { fn parse<'i, 't>( _context: &ParserContext, diff --git a/servo/components/style_derive/parse.rs b/servo/components/style_derive/parse.rs index 345decce28c4..8739269a0a6b 100644 --- a/servo/components/style_derive/parse.rs +++ b/servo/components/style_derive/parse.rs @@ -2,10 +2,11 @@ * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ -use crate::to_css::CssVariantAttrs; +use crate::to_css::{CssBitflagAttrs, CssVariantAttrs}; use derive_common::cg; -use proc_macro2::TokenStream; -use syn::{self, DeriveInput, Path}; +use proc_macro2::{TokenStream, Span}; +use quote::TokenStreamExt; +use syn::{self, Ident, DeriveInput, Path}; use synstructure::{Structure, VariantInfo}; #[derive(Default, FromVariant)] @@ -21,6 +22,70 @@ pub struct ParseFieldAttrs { field_bound: bool, } +fn parse_bitflags(bitflags: &CssBitflagAttrs) -> TokenStream { + let mut match_arms = TokenStream::new(); + for (rust_name, css_name) in bitflags.single_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + match_arms.append_all(quote! { + #css_name if result.is_empty() => { + single_flag = true; + Self::#rust_ident + }, + }); + } + + for (rust_name, css_name) in bitflags.mixed_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + match_arms.append_all(quote! { + #css_name => Self::#rust_ident, + }); + } + + let mut validate_condition = quote! { !result.is_empty() }; + if let Some(ref function) = bitflags.validate_mixed { + validate_condition.append_all(quote! { + && #function(result) + }); + } + + // NOTE(emilio): this loop has this weird structure because we run this code + // to parse stuff like text-decoration-line in the text-decoration + // shorthand, so we need to be a bit careful that we don't error if we don't + // consume the whole thing because we find an invalid identifier or other + // kind of token. Instead, we should leave it unconsumed. + quote! { + let mut result = Self::empty(); + loop { + let mut single_flag = false; + let flag: Result<_, style_traits::ParseError<'i>> = input.try_parse(|input| { + Ok(try_match_ident_ignore_ascii_case! { input, + #match_arms + }) + }); + + let flag = match flag { + Ok(flag) => flag, + Err(..) => break, + }; + + if single_flag { + return Ok(flag); + } + + if result.intersects(flag) { + return Err(input.new_custom_error(StyleParseErrorKind::UnspecifiedError)); + } + + result.insert(flag); + } + if #validate_condition { + Ok(result) + } else { + Err(input.new_custom_error(style_traits::StyleParseErrorKind::UnspecifiedError)) + } + } +} + fn parse_non_keyword_variant( where_clause: &mut Option, name: &syn::Ident, @@ -42,6 +107,13 @@ fn parse_non_keyword_variant( let binding_ast = &bindings[0].ast(); let ty = &binding_ast.ty; + if let Some(ref bitflags) = variant_attrs.bitflags { + assert!(skip_try, "Should be the only variant"); + assert!(parse_attrs.condition.is_none(), "Should be the only variant"); + assert!(where_clause.is_none(), "Generic bitflags?"); + return parse_bitflags(bitflags) + } + let field_attrs = cg::parse_field_attrs::(binding_ast); if field_attrs.field_bound { cg::add_predicate(where_clause, parse_quote!(#ty: crate::parser::Parse)); diff --git a/servo/components/style_derive/specified_value_info.rs b/servo/components/style_derive/specified_value_info.rs index c477f4a7cf9a..e29f2bc416ee 100644 --- a/servo/components/style_derive/specified_value_info.rs +++ b/servo/components/style_derive/specified_value_info.rs @@ -67,7 +67,14 @@ pub fn derive(mut input: DeriveInput) -> TokenStream { } }, Data::Struct(ref s) => { - if !derive_struct_fields(&s.fields, &mut types, &mut values) { + if let Some(ref bitflags) = css_attrs.bitflags { + for (_rust_name, css_name) in bitflags.single_flags() { + values.push(css_name) + } + for (_rust_name, css_name) in bitflags.mixed_flags() { + values.push(css_name) + } + } else if !derive_struct_fields(&s.fields, &mut types, &mut values) { values.push(input_name()); } }, diff --git a/servo/components/style_derive/to_css.rs b/servo/components/style_derive/to_css.rs index 1fd47510f63a..d9db85101830 100644 --- a/servo/components/style_derive/to_css.rs +++ b/servo/components/style_derive/to_css.rs @@ -4,11 +4,62 @@ use darling::util::Override; use derive_common::cg; -use proc_macro2::TokenStream; +use proc_macro2::{Span, TokenStream}; use quote::{ToTokens, TokenStreamExt}; -use syn::{self, Data, Path, WhereClause}; +use syn::{self, Data, Ident, Path, WhereClause}; use synstructure::{BindingInfo, Structure, VariantInfo}; +fn derive_bitflags(input: &syn::DeriveInput, bitflags: &CssBitflagAttrs) -> TokenStream { + let name = &input.ident; + let mut body = TokenStream::new(); + for (rust_name, css_name) in bitflags.single_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + body.append_all(quote! { + if *self == Self::#rust_ident { + return dest.write_str(#css_name); + } + }); + } + + body.append_all(quote! { + let mut has_any = false; + }); + + for (rust_name, css_name) in bitflags.mixed_flags() { + let rust_ident = Ident::new(&rust_name, Span::call_site()); + body.append_all(quote! { + if self.intersects(Self::#rust_ident) { + if has_any { + dest.write_char(' ')?; + } + has_any = true; + dest.write_str(#css_name)?; + } + }); + } + + body.append_all(quote! { + debug_assert!(has_any, "Shouldn't have parsed empty"); + Ok(()) + }); + + quote! { + impl style_traits::ToCss for #name { + #[allow(unused_variables)] + #[inline] + fn to_css( + &self, + dest: &mut style_traits::CssWriter, + ) -> std::fmt::Result + where + W: std::fmt::Write, + { + #body + } + } + } +} + pub fn derive(mut input: syn::DeriveInput) -> TokenStream { let mut where_clause = input.generics.where_clause.take(); for param in input.generics.type_params() { @@ -16,12 +67,21 @@ pub fn derive(mut input: syn::DeriveInput) -> TokenStream { } let input_attrs = cg::parse_input_attrs::(&input); - if let Data::Enum(_) = input.data { + if matches!(input.data, Data::Enum(..)) || input_attrs.bitflags.is_some() { assert!( input_attrs.function.is_none(), - "#[css(function)] is not allowed on enums" + "#[css(function)] is not allowed on enums or bitflags" ); - assert!(!input_attrs.comma, "#[css(comma)] is not allowed on enums"); + assert!( + !input_attrs.comma, + "#[css(comma)] is not allowed on enums or bitflags" + ); + } + + if let Some(ref bitflags) = input_attrs.bitflags { + assert!(!input_attrs.derive_debug, "Bitflags can derive debug on their own"); + assert!(where_clause.is_none(), "Generic bitflags?"); + return derive_bitflags(&input, bitflags); } let match_body = { @@ -244,6 +304,32 @@ fn derive_single_field_expr( expr } +#[derive(Default, FromMeta)] +pub struct CssBitflagAttrs { + /// Flags that can only go on their own, comma-separated. + pub single: String, + /// Flags that can go mixed with each other, comma-separated. + pub mixed: String, + /// Extra validation of the resulting mixed flags. + #[darling(default)] + pub validate_mixed: Option, +} + +impl CssBitflagAttrs { + /// Returns a vector of (rust_name, css_name) of a given flag list. + fn names(s: &str) -> Vec<(String, String)> { + s.split(',').map(|css_name| (cg::to_scream_case(css_name), css_name.to_owned())).collect() + } + + pub fn single_flags(&self) -> Vec<(String, String)> { + Self::names(&self.single) + } + + pub fn mixed_flags(&self) -> Vec<(String, String)> { + Self::names(&self.mixed) + } +} + #[derive(Default, FromDeriveInput)] #[darling(attributes(css), default)] pub struct CssInputAttrs { @@ -252,6 +338,7 @@ pub struct CssInputAttrs { pub function: Option>, // Here because structs variants are also their whole type definition. pub comma: bool, + pub bitflags: Option, } #[derive(Default, FromVariant)] @@ -261,6 +348,7 @@ pub struct CssVariantAttrs { // Here because structs variants are also their whole type definition. pub derive_debug: bool, pub comma: bool, + pub bitflags: Option, pub dimension: bool, pub keyword: Option, pub skip: bool, diff --git a/servo/components/style_traits/values.rs b/servo/components/style_traits/values.rs index 05ba0b5f2a52..c2de9cb71241 100644 --- a/servo/components/style_traits/values.rs +++ b/servo/components/style_traits/values.rs @@ -44,6 +44,29 @@ use std::fmt::{self, Write}; /// * `#[css(represents_keyword)]` can be used on bool fields in order to /// serialize the field name if the field is true, or nothing otherwise. It /// also collects those keywords for `SpecifiedValueInfo`. +/// * `#[css(bitflags(single="", mixed="", validate="")]` can be used to derive +/// parse / serialize / etc on bitflags. The rules for parsing bitflags are +/// the following: +/// +/// * `single` flags can only appear on their own. It's common that bitflags +/// properties at least have one such value like `none` or `auto`. +/// * `mixed` properties can appear mixed together, but not along any other +/// flag that shares a bit with itself. For example, if you have three +/// bitflags like: +/// +/// FOO = 1 << 0; +/// BAR = 1 << 1; +/// BAZ = 1 << 2; +/// BAZZ = BAR | BAZ; +/// +/// Then the following combinations won't be valid: +/// +/// * foo foo: (every flag shares a bit with itself) +/// * bar bazz: (bazz shares a bit with bar) +/// +/// But `bar baz` will be valid, as they don't share bits, and so would +/// `foo` with any other flag, or `bazz` on its own. +/// /// * finally, one can put `#[css(derive_debug)]` on the whole type, to /// implement `Debug` by a single call to `ToCss::to_css`. pub trait ToCss {