diff --git a/Cargo.toml b/Cargo.toml index 30f13090..211221fe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "cssparser" -version = "0.10.0" +version = "0.11.0" authors = [ "Simon Sapin " ] description = "Rust implementation of CSS Syntax Level 3" -documentation = "http://servo.github.io/rust-cssparser/cssparser/index.html" +documentation = "https://docs.rs/cssparser/" repository = "https://github.com/servo/rust-cssparser" readme = "README.md" keywords = ["css", "syntax", "parser"] @@ -14,23 +14,25 @@ build = "build.rs" exclude = ["src/css-parsing-tests"] -[lib] -doctest = false - [dev-dependencies] rustc-serialize = "0.3" tempdir = "0.3" -encoding_rs = "0.3.2" +encoding_rs = "0.5" [dependencies] +cssparser-macros = {path = "./macros", version = "0.1"} heapsize = {version = "0.3", optional = true} matches = "0.1" +phf = "0.7" serde = {version = "0.9", optional = true} [build-dependencies] -syn = { version = "0.10.6", features = ["full", "visit"]} +syn = "0.11" quote = "0.3" [features] bench = [] dummy_match_byte = [] + +[workspace] +members = [".", "./macros"] diff --git a/macros/Cargo.toml b/macros/Cargo.toml new file mode 100644 index 00000000..2ca733c4 --- /dev/null +++ b/macros/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "cssparser-macros" +version = "0.1.0" +authors = ["Simon Sapin "] +description = "Procedural macros for cssparser" +documentation = "https://docs.rs/cssparser-macros/" +repository = "https://github.com/servo/rust-cssparser" +license = "MPL-2.0" + +[lib] +path = "lib.rs" +proc-macro = true + +[dependencies] +phf_codegen = "0.7" +quote = "0.3" +syn = "0.11" + diff --git a/macros/lib.rs b/macros/lib.rs new file mode 100644 index 00000000..a783147a --- /dev/null +++ b/macros/lib.rs @@ -0,0 +1,122 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * 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/. */ + +extern crate phf_codegen; +extern crate proc_macro; +#[macro_use] extern crate quote; +extern crate syn; + +use std::ascii::AsciiExt; + +/// Find a `#[cssparser__assert_ascii_lowercase__data(string = "…", string = "…")]` attribute, +/// and panic if any string contains ASCII uppercase letters. +#[proc_macro_derive(cssparser__assert_ascii_lowercase, + attributes(cssparser__assert_ascii_lowercase__data))] +pub fn assert_ascii_lowercase(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = syn::parse_macro_input(&input.to_string()).unwrap(); + let data = list_attr(&input, "cssparser__assert_ascii_lowercase__data"); + + for sub_attr in data { + let string = sub_attr_value(sub_attr, "string"); + assert_eq!(*string, string.to_ascii_lowercase(), + "the expected strings must be given in ASCII lowercase"); + } + + "".parse().unwrap() +} + +/// Find a `#[cssparser__max_len__data(string = "…", string = "…")]` attribute, +/// and emit a `MAX_LENGTH` constant with the length of the longest string. +#[proc_macro_derive(cssparser__max_len, + attributes(cssparser__max_len__data))] +pub fn max_len(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = syn::parse_macro_input(&input.to_string()).unwrap(); + let data = list_attr(&input, "cssparser__max_len__data"); + + let lengths = data.iter().map(|sub_attr| sub_attr_value(sub_attr, "string").len()); + let max_length = lengths.max().expect("expected at least one string"); + + let tokens = quote! { + const MAX_LENGTH: usize = #max_length; + }; + + tokens.as_str().parse().unwrap() +} + +/// On `struct $Name($ValueType)`, add a new static method +/// `fn map() -> &'static ::phf::Map<&'static str, $ValueType>`. +/// The map’s content is given as: +/// `#[cssparser__phf_map__kv_pairs(key = "…", value = "…", key = "…", value = "…")]`. +/// Keys are ASCII-lowercased. +#[proc_macro_derive(cssparser__phf_map, + attributes(cssparser__phf_map__kv_pairs))] +pub fn phf_map(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input = syn::parse_macro_input(&input.to_string()).unwrap(); + let name = &input.ident; + let value_type = match input.body { + syn::Body::Struct(syn::VariantData::Tuple(ref fields)) if fields.len() == 1 => { + &fields[0].ty + } + _ => panic!("expected tuple struct newtype, got {:?}", input.body) + }; + + let pairs: Vec<_> = list_attr(&input, "cssparser__phf_map__kv_pairs").chunks(2).map(|chunk| { + let key = sub_attr_value(&chunk[0], "key"); + let value = sub_attr_value(&chunk[1], "value"); + (key.to_ascii_lowercase(), value) + }).collect(); + + let mut map = phf_codegen::Map::new(); + for &(ref key, value) in &pairs { + map.entry(&**key, value); + } + + let mut initializer_bytes = Vec::::new(); + let mut initializer_tokens = quote::Tokens::new(); + map.build(&mut initializer_bytes).unwrap(); + initializer_tokens.append(::std::str::from_utf8(&initializer_bytes).unwrap()); + + let tokens = quote! { + impl #name { + #[inline] + fn map() -> &'static ::phf::Map<&'static str, #value_type> { + static MAP: ::phf::Map<&'static str, #value_type> = #initializer_tokens; + &MAP + } + } + }; + + tokens.as_str().parse().unwrap() +} + +/// Panic if the first attribute isn’t `#[foo(…)]` with the given name, +/// or return the parameters. +fn list_attr<'a>(input: &'a syn::DeriveInput, expected_name: &str) -> &'a [syn::NestedMetaItem] { + for attr in &input.attrs { + match attr.value { + syn::MetaItem::List(ref name, ref nested) if name == expected_name => { + return nested + } + _ => {} + } + } + panic!("expected a {} attribute", expected_name) +} + +/// Panic if `sub_attr` is not a name-value like `foo = "…"` with the given name, +/// or return the value. +fn sub_attr_value<'a>(sub_attr: &'a syn::NestedMetaItem, expected_name: &str) -> &'a str { + match *sub_attr { + syn::NestedMetaItem::MetaItem( + syn::MetaItem::NameValue(ref name, syn::Lit::Str(ref value, _)) + ) + if name == expected_name => { + value + } + _ => { + panic!("expected a `{} = \"…\"` parameter to the attribute, got {:?}", + expected_name, sub_attr) + } + } +} diff --git a/src/color.rs b/src/color.rs index 6f3d886b..85ae102a 100644 --- a/src/color.rs +++ b/src/color.rs @@ -169,161 +169,173 @@ fn rgba(red: u8, green: u8, blue: u8, alpha: u8) -> Result { /// (For example, the value of an `Ident` token is fine.) #[inline] pub fn parse_color_keyword(ident: &str) -> Result { - match_ignore_ascii_case! { ident, - "black" => rgb(0, 0, 0), - "silver" => rgb(192, 192, 192), - "gray" => rgb(128, 128, 128), - "white" => rgb(255, 255, 255), - "maroon" => rgb(128, 0, 0), - "red" => rgb(255, 0, 0), - "purple" => rgb(128, 0, 128), - "fuchsia" => rgb(255, 0, 255), - "green" => rgb(0, 128, 0), - "lime" => rgb(0, 255, 0), - "olive" => rgb(128, 128, 0), - "yellow" => rgb(255, 255, 0), - "navy" => rgb(0, 0, 128), - "blue" => rgb(0, 0, 255), - "teal" => rgb(0, 128, 128), - "aqua" => rgb(0, 255, 255), - - "aliceblue" => rgb(240, 248, 255), - "antiquewhite" => rgb(250, 235, 215), - "aquamarine" => rgb(127, 255, 212), - "azure" => rgb(240, 255, 255), - "beige" => rgb(245, 245, 220), - "bisque" => rgb(255, 228, 196), - "blanchedalmond" => rgb(255, 235, 205), - "blueviolet" => rgb(138, 43, 226), - "brown" => rgb(165, 42, 42), - "burlywood" => rgb(222, 184, 135), - "cadetblue" => rgb(95, 158, 160), - "chartreuse" => rgb(127, 255, 0), - "chocolate" => rgb(210, 105, 30), - "coral" => rgb(255, 127, 80), - "cornflowerblue" => rgb(100, 149, 237), - "cornsilk" => rgb(255, 248, 220), - "crimson" => rgb(220, 20, 60), - "cyan" => rgb(0, 255, 255), - "darkblue" => rgb(0, 0, 139), - "darkcyan" => rgb(0, 139, 139), - "darkgoldenrod" => rgb(184, 134, 11), - "darkgray" => rgb(169, 169, 169), - "darkgreen" => rgb(0, 100, 0), - "darkgrey" => rgb(169, 169, 169), - "darkkhaki" => rgb(189, 183, 107), - "darkmagenta" => rgb(139, 0, 139), - "darkolivegreen" => rgb(85, 107, 47), - "darkorange" => rgb(255, 140, 0), - "darkorchid" => rgb(153, 50, 204), - "darkred" => rgb(139, 0, 0), - "darksalmon" => rgb(233, 150, 122), - "darkseagreen" => rgb(143, 188, 143), - "darkslateblue" => rgb(72, 61, 139), - "darkslategray" => rgb(47, 79, 79), - "darkslategrey" => rgb(47, 79, 79), - "darkturquoise" => rgb(0, 206, 209), - "darkviolet" => rgb(148, 0, 211), - "deeppink" => rgb(255, 20, 147), - "deepskyblue" => rgb(0, 191, 255), - "dimgray" => rgb(105, 105, 105), - "dimgrey" => rgb(105, 105, 105), - "dodgerblue" => rgb(30, 144, 255), - "firebrick" => rgb(178, 34, 34), - "floralwhite" => rgb(255, 250, 240), - "forestgreen" => rgb(34, 139, 34), - "gainsboro" => rgb(220, 220, 220), - "ghostwhite" => rgb(248, 248, 255), - "gold" => rgb(255, 215, 0), - "goldenrod" => rgb(218, 165, 32), - "greenyellow" => rgb(173, 255, 47), - "grey" => rgb(128, 128, 128), - "honeydew" => rgb(240, 255, 240), - "hotpink" => rgb(255, 105, 180), - "indianred" => rgb(205, 92, 92), - "indigo" => rgb(75, 0, 130), - "ivory" => rgb(255, 255, 240), - "khaki" => rgb(240, 230, 140), - "lavender" => rgb(230, 230, 250), - "lavenderblush" => rgb(255, 240, 245), - "lawngreen" => rgb(124, 252, 0), - "lemonchiffon" => rgb(255, 250, 205), - "lightblue" => rgb(173, 216, 230), - "lightcoral" => rgb(240, 128, 128), - "lightcyan" => rgb(224, 255, 255), - "lightgoldenrodyellow" => rgb(250, 250, 210), - "lightgray" => rgb(211, 211, 211), - "lightgreen" => rgb(144, 238, 144), - "lightgrey" => rgb(211, 211, 211), - "lightpink" => rgb(255, 182, 193), - "lightsalmon" => rgb(255, 160, 122), - "lightseagreen" => rgb(32, 178, 170), - "lightskyblue" => rgb(135, 206, 250), - "lightslategray" => rgb(119, 136, 153), - "lightslategrey" => rgb(119, 136, 153), - "lightsteelblue" => rgb(176, 196, 222), - "lightyellow" => rgb(255, 255, 224), - "limegreen" => rgb(50, 205, 50), - "linen" => rgb(250, 240, 230), - "magenta" => rgb(255, 0, 255), - "mediumaquamarine" => rgb(102, 205, 170), - "mediumblue" => rgb(0, 0, 205), - "mediumorchid" => rgb(186, 85, 211), - "mediumpurple" => rgb(147, 112, 219), - "mediumseagreen" => rgb(60, 179, 113), - "mediumslateblue" => rgb(123, 104, 238), - "mediumspringgreen" => rgb(0, 250, 154), - "mediumturquoise" => rgb(72, 209, 204), - "mediumvioletred" => rgb(199, 21, 133), - "midnightblue" => rgb(25, 25, 112), - "mintcream" => rgb(245, 255, 250), - "mistyrose" => rgb(255, 228, 225), - "moccasin" => rgb(255, 228, 181), - "navajowhite" => rgb(255, 222, 173), - "oldlace" => rgb(253, 245, 230), - "olivedrab" => rgb(107, 142, 35), - "orange" => rgb(255, 165, 0), - "orangered" => rgb(255, 69, 0), - "orchid" => rgb(218, 112, 214), - "palegoldenrod" => rgb(238, 232, 170), - "palegreen" => rgb(152, 251, 152), - "paleturquoise" => rgb(175, 238, 238), - "palevioletred" => rgb(219, 112, 147), - "papayawhip" => rgb(255, 239, 213), - "peachpuff" => rgb(255, 218, 185), - "peru" => rgb(205, 133, 63), - "pink" => rgb(255, 192, 203), - "plum" => rgb(221, 160, 221), - "powderblue" => rgb(176, 224, 230), - "rebeccapurple" => rgb(102, 51, 153), - "rosybrown" => rgb(188, 143, 143), - "royalblue" => rgb(65, 105, 225), - "saddlebrown" => rgb(139, 69, 19), - "salmon" => rgb(250, 128, 114), - "sandybrown" => rgb(244, 164, 96), - "seagreen" => rgb(46, 139, 87), - "seashell" => rgb(255, 245, 238), - "sienna" => rgb(160, 82, 45), - "skyblue" => rgb(135, 206, 235), - "slateblue" => rgb(106, 90, 205), - "slategray" => rgb(112, 128, 144), - "slategrey" => rgb(112, 128, 144), - "snow" => rgb(255, 250, 250), - "springgreen" => rgb(0, 255, 127), - "steelblue" => rgb(70, 130, 180), - "tan" => rgb(210, 180, 140), - "thistle" => rgb(216, 191, 216), - "tomato" => rgb(255, 99, 71), - "turquoise" => rgb(64, 224, 208), - "violet" => rgb(238, 130, 238), - "wheat" => rgb(245, 222, 179), - "whitesmoke" => rgb(245, 245, 245), - "yellowgreen" => rgb(154, 205, 50), - - "transparent" => rgba(0, 0, 0, 0), - "currentcolor" => Ok(Color::CurrentColor), - _ => Err(()) + macro_rules! rgb { + ($red: expr, $green: expr, $blue: expr) => { + Color::RGBA(RGBA { + red: $red, + green: $green, + blue: $blue, + alpha: 255, + }) + } + } + ascii_case_insensitive_phf_map! { + KEYWORDS: Map = { + "black" => "rgb!(0, 0, 0)", + "silver" => "rgb!(192, 192, 192)", + "gray" => "rgb!(128, 128, 128)", + "white" => "rgb!(255, 255, 255)", + "maroon" => "rgb!(128, 0, 0)", + "red" => "rgb!(255, 0, 0)", + "purple" => "rgb!(128, 0, 128)", + "fuchsia" => "rgb!(255, 0, 255)", + "green" => "rgb!(0, 128, 0)", + "lime" => "rgb!(0, 255, 0)", + "olive" => "rgb!(128, 128, 0)", + "yellow" => "rgb!(255, 255, 0)", + "navy" => "rgb!(0, 0, 128)", + "blue" => "rgb!(0, 0, 255)", + "teal" => "rgb!(0, 128, 128)", + "aqua" => "rgb!(0, 255, 255)", + + "aliceblue" => "rgb!(240, 248, 255)", + "antiquewhite" => "rgb!(250, 235, 215)", + "aquamarine" => "rgb!(127, 255, 212)", + "azure" => "rgb!(240, 255, 255)", + "beige" => "rgb!(245, 245, 220)", + "bisque" => "rgb!(255, 228, 196)", + "blanchedalmond" => "rgb!(255, 235, 205)", + "blueviolet" => "rgb!(138, 43, 226)", + "brown" => "rgb!(165, 42, 42)", + "burlywood" => "rgb!(222, 184, 135)", + "cadetblue" => "rgb!(95, 158, 160)", + "chartreuse" => "rgb!(127, 255, 0)", + "chocolate" => "rgb!(210, 105, 30)", + "coral" => "rgb!(255, 127, 80)", + "cornflowerblue" => "rgb!(100, 149, 237)", + "cornsilk" => "rgb!(255, 248, 220)", + "crimson" => "rgb!(220, 20, 60)", + "cyan" => "rgb!(0, 255, 255)", + "darkblue" => "rgb!(0, 0, 139)", + "darkcyan" => "rgb!(0, 139, 139)", + "darkgoldenrod" => "rgb!(184, 134, 11)", + "darkgray" => "rgb!(169, 169, 169)", + "darkgreen" => "rgb!(0, 100, 0)", + "darkgrey" => "rgb!(169, 169, 169)", + "darkkhaki" => "rgb!(189, 183, 107)", + "darkmagenta" => "rgb!(139, 0, 139)", + "darkolivegreen" => "rgb!(85, 107, 47)", + "darkorange" => "rgb!(255, 140, 0)", + "darkorchid" => "rgb!(153, 50, 204)", + "darkred" => "rgb!(139, 0, 0)", + "darksalmon" => "rgb!(233, 150, 122)", + "darkseagreen" => "rgb!(143, 188, 143)", + "darkslateblue" => "rgb!(72, 61, 139)", + "darkslategray" => "rgb!(47, 79, 79)", + "darkslategrey" => "rgb!(47, 79, 79)", + "darkturquoise" => "rgb!(0, 206, 209)", + "darkviolet" => "rgb!(148, 0, 211)", + "deeppink" => "rgb!(255, 20, 147)", + "deepskyblue" => "rgb!(0, 191, 255)", + "dimgray" => "rgb!(105, 105, 105)", + "dimgrey" => "rgb!(105, 105, 105)", + "dodgerblue" => "rgb!(30, 144, 255)", + "firebrick" => "rgb!(178, 34, 34)", + "floralwhite" => "rgb!(255, 250, 240)", + "forestgreen" => "rgb!(34, 139, 34)", + "gainsboro" => "rgb!(220, 220, 220)", + "ghostwhite" => "rgb!(248, 248, 255)", + "gold" => "rgb!(255, 215, 0)", + "goldenrod" => "rgb!(218, 165, 32)", + "greenyellow" => "rgb!(173, 255, 47)", + "grey" => "rgb!(128, 128, 128)", + "honeydew" => "rgb!(240, 255, 240)", + "hotpink" => "rgb!(255, 105, 180)", + "indianred" => "rgb!(205, 92, 92)", + "indigo" => "rgb!(75, 0, 130)", + "ivory" => "rgb!(255, 255, 240)", + "khaki" => "rgb!(240, 230, 140)", + "lavender" => "rgb!(230, 230, 250)", + "lavenderblush" => "rgb!(255, 240, 245)", + "lawngreen" => "rgb!(124, 252, 0)", + "lemonchiffon" => "rgb!(255, 250, 205)", + "lightblue" => "rgb!(173, 216, 230)", + "lightcoral" => "rgb!(240, 128, 128)", + "lightcyan" => "rgb!(224, 255, 255)", + "lightgoldenrodyellow" => "rgb!(250, 250, 210)", + "lightgray" => "rgb!(211, 211, 211)", + "lightgreen" => "rgb!(144, 238, 144)", + "lightgrey" => "rgb!(211, 211, 211)", + "lightpink" => "rgb!(255, 182, 193)", + "lightsalmon" => "rgb!(255, 160, 122)", + "lightseagreen" => "rgb!(32, 178, 170)", + "lightskyblue" => "rgb!(135, 206, 250)", + "lightslategray" => "rgb!(119, 136, 153)", + "lightslategrey" => "rgb!(119, 136, 153)", + "lightsteelblue" => "rgb!(176, 196, 222)", + "lightyellow" => "rgb!(255, 255, 224)", + "limegreen" => "rgb!(50, 205, 50)", + "linen" => "rgb!(250, 240, 230)", + "magenta" => "rgb!(255, 0, 255)", + "mediumaquamarine" => "rgb!(102, 205, 170)", + "mediumblue" => "rgb!(0, 0, 205)", + "mediumorchid" => "rgb!(186, 85, 211)", + "mediumpurple" => "rgb!(147, 112, 219)", + "mediumseagreen" => "rgb!(60, 179, 113)", + "mediumslateblue" => "rgb!(123, 104, 238)", + "mediumspringgreen" => "rgb!(0, 250, 154)", + "mediumturquoise" => "rgb!(72, 209, 204)", + "mediumvioletred" => "rgb!(199, 21, 133)", + "midnightblue" => "rgb!(25, 25, 112)", + "mintcream" => "rgb!(245, 255, 250)", + "mistyrose" => "rgb!(255, 228, 225)", + "moccasin" => "rgb!(255, 228, 181)", + "navajowhite" => "rgb!(255, 222, 173)", + "oldlace" => "rgb!(253, 245, 230)", + "olivedrab" => "rgb!(107, 142, 35)", + "orange" => "rgb!(255, 165, 0)", + "orangered" => "rgb!(255, 69, 0)", + "orchid" => "rgb!(218, 112, 214)", + "palegoldenrod" => "rgb!(238, 232, 170)", + "palegreen" => "rgb!(152, 251, 152)", + "paleturquoise" => "rgb!(175, 238, 238)", + "palevioletred" => "rgb!(219, 112, 147)", + "papayawhip" => "rgb!(255, 239, 213)", + "peachpuff" => "rgb!(255, 218, 185)", + "peru" => "rgb!(205, 133, 63)", + "pink" => "rgb!(255, 192, 203)", + "plum" => "rgb!(221, 160, 221)", + "powderblue" => "rgb!(176, 224, 230)", + "rebeccapurple" => "rgb!(102, 51, 153)", + "rosybrown" => "rgb!(188, 143, 143)", + "royalblue" => "rgb!(65, 105, 225)", + "saddlebrown" => "rgb!(139, 69, 19)", + "salmon" => "rgb!(250, 128, 114)", + "sandybrown" => "rgb!(244, 164, 96)", + "seagreen" => "rgb!(46, 139, 87)", + "seashell" => "rgb!(255, 245, 238)", + "sienna" => "rgb!(160, 82, 45)", + "skyblue" => "rgb!(135, 206, 235)", + "slateblue" => "rgb!(106, 90, 205)", + "slategray" => "rgb!(112, 128, 144)", + "slategrey" => "rgb!(112, 128, 144)", + "snow" => "rgb!(255, 250, 250)", + "springgreen" => "rgb!(0, 255, 127)", + "steelblue" => "rgb!(70, 130, 180)", + "tan" => "rgb!(210, 180, 140)", + "thistle" => "rgb!(216, 191, 216)", + "tomato" => "rgb!(255, 99, 71)", + "turquoise" => "rgb!(64, 224, 208)", + "violet" => "rgb!(238, 130, 238)", + "wheat" => "rgb!(245, 222, 179)", + "whitesmoke" => "rgb!(245, 245, 245)", + "yellowgreen" => "rgb!(154, 205, 50)", + + "transparent" => "Color::RGBA(RGBA { red: 0, green: 0, blue: 0, alpha: 0 })", + "currentcolor" => "Color::CurrentColor", + } } + KEYWORDS::get(ident).cloned().ok_or(()) } diff --git a/src/lib.rs b/src/lib.rs index 905f5066..08e7bd71 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -68,7 +68,9 @@ fn parse_border_spacing(_context: &ParserContext, input: &mut Parser) #![recursion_limit="200"] // For color::parse_color_keyword +#[macro_use] extern crate cssparser_macros; #[macro_use] extern crate matches; +extern crate phf; #[cfg(test)] extern crate encoding_rs; #[cfg(test)] extern crate tempdir; #[cfg(test)] extern crate rustc_serialize; @@ -87,27 +89,36 @@ pub use serializer::{ToCss, CssStringWriter, serialize_identifier, serialize_str pub use parser::{Parser, Delimiter, Delimiters, SourcePosition}; pub use unicode_range::UnicodeRange; - -/** - -This macro is equivalent to a `match` expression on an `&str` value, -but matching is case-insensitive in the ASCII range. - -Usage example: - -```{rust,ignore} -match_ignore_ascii_case! { string, - "foo" => Some(Foo), - "bar" => Some(Bar), - "baz" => Some(Baz), - _ => None -} -``` - -The macro also takes a slice of the value, -so that a `String` or `CowString` could be passed directly instead of a `&str`. - -*/ +/// Expands to an expression equivalent to a `match` with string patterns, +/// but matching is case-insensitive in the ASCII range. +/// +/// Requirements: +/// +/// * The `cssparser_macros` crate must also be imported at the crate root +/// * The patterns must not contain ASCII upper case letters. (They must be already be lower-cased.) +/// +/// # Example +/// +/// ```rust +/// #[macro_use] extern crate cssparser; +/// #[macro_use] extern crate cssparser_macros; +/// +/// # fn main() {} // Make doctest not wrap everythig in its own main +/// # fn dummy(function_name: &String) { let _ = +/// match_ignore_ascii_case! { &function_name, +/// "rgb" => parse_rgb(..), +/// "rgba" => parse_rgba(..), +/// "hsl" => parse_hsl(..), +/// "hsla" => parse_hsla(..), +/// _ => Err("unknown function") +/// } +/// # ;} +/// # use std::ops::RangeFull; +/// # fn parse_rgb(_: RangeFull) -> Result<(), &'static str> { Err("") } +/// # fn parse_rgba(_: RangeFull) -> Result<(), &'static str> { Err("") } +/// # fn parse_hsl(_: RangeFull) -> Result<(), &'static str> { Err("") } +/// # fn parse_hsla(_: RangeFull) -> Result<(), &'static str> { Err("") } +/// ``` #[macro_export] macro_rules! match_ignore_ascii_case { // parse the last case plus the fallback @@ -123,10 +134,15 @@ macro_rules! match_ignore_ascii_case { // finished parsing (@inner $value:expr, () -> ($(($string:expr => $result:expr))*) $fallback:expr ) => { { - use std::ascii::AsciiExt; - match &$value[..] { + #[derive(cssparser__assert_ascii_lowercase)] + #[cssparser__assert_ascii_lowercase__data($(string = $string),+)] + #[allow(dead_code)] + struct Dummy; + + _cssparser_internal__to_lowercase!($value => lowercase, $($string),+); + match lowercase { $( - s if s.eq_ignore_ascii_case($string) => $result, + Some($string) => $result, )+ _ => $fallback } @@ -139,6 +155,120 @@ macro_rules! match_ignore_ascii_case { }; } +/// Define a placeholder type `$Name` +/// with a method `fn get(input: &str) -> Option<&'static $ValueType>`. +/// +/// This method uses finds a match for the input string +/// in a [`phf` map](https://github.com/sfackler/rust-phf). +/// Matching is case-insensitive in the ASCII range. +/// +/// Requirements: +/// +/// * The `phf` and `cssparser_macros` crates must also be imported at the crate root +/// * The values must be given a strings that contain Rust syntax for a constant expression. +/// +/// ## Example: +/// +/// ```rust +/// extern crate phf; +/// #[macro_use] extern crate cssparser; +/// #[macro_use] extern crate cssparser_macros; +/// +/// # fn main() {} // Make doctest not wrap everythig in its own main +/// +/// fn color_rgb(input: &str) -> Option<(u8, u8, u8)> { +/// ascii_case_insensitive_phf_map! { +/// KEYWORDS: Map<(u8, u8, u8)> = { +/// "red" => "(255, 0, 0)", +/// "green" => "(0, 255, 0)", +/// "blue" => "(0, 0, 255)", +/// } +/// } +/// KEYWORDS::get(input).cloned() +/// } +#[macro_export] +macro_rules! ascii_case_insensitive_phf_map { + ($Name: ident : Map<$ValueType: ty> = { + $( $key: expr => $value: expr, )* + }) => { + #[derive(cssparser__phf_map)] + #[cssparser__phf_map__kv_pairs( + $( + key = $key, + value = $value + ),+ + )] + struct $Name($ValueType); + + impl $Name { + #[inline] + fn get(input: &str) -> Option<&'static $ValueType> { + _cssparser_internal__to_lowercase!(input => lowercase, $($key),+); + lowercase.and_then(|string| $Name::map().get(string)) + } + } + } +} + +/// Implementation detail of match_ignore_ascii_case! and ascii_case_insensitive_phf_map! macros. +/// +/// **This macro is not part of the public API. It can change or be removed between any versions.** +/// +/// * Check at compile-time that none of the `$string`s contain ASCII uppercase letters +/// * Define a local variable named `$output` to the result of calling `_internal__to_lowercase` +/// with a stack-allocated buffer as long as the longest `$string`. +#[macro_export] +#[doc(hidden)] +macro_rules! _cssparser_internal__to_lowercase { + ($input: expr => $output: ident, $($string: expr),+) => { + #[derive(cssparser__max_len)] + #[cssparser__max_len__data($(string = $string),+)] + #[allow(dead_code)] + struct Dummy2; + + // mem::uninitialized() is ok because `buffer` is only used in `_internal__to_lowercase`, + // which initializes with `copy_from_slice` the part of the buffer it uses, + // before it uses it. + #[allow(unsafe_code)] + // MAX_LENGTH is generated by cssparser__max_len + let mut buffer: [u8; MAX_LENGTH] = unsafe { + ::std::mem::uninitialized() + }; + let input: &str = $input; + let $output = $crate::_internal__to_lowercase(&mut buffer, input); + } +} + + +/// Implementation detail of match_ignore_ascii_case! and ascii_case_insensitive_phf_map! macros. +/// +/// **This function is not part of the public API. It can change or be removed between any verisons.** +/// +/// Return `input`, lower-cased, unless larger than `buffer` +/// which is used temporary space for lower-casing a copy of `input` if necessary. +#[doc(hidden)] +#[allow(non_snake_case)] +pub fn _internal__to_lowercase<'a>(buffer: &'a mut [u8], input: &'a str) -> Option<&'a str> { + if let Some(buffer) = buffer.get_mut(..input.len()) { + if let Some(first_uppercase) = input.bytes().position(|byte| matches!(byte, b'A'...b'Z')) { + buffer.copy_from_slice(input.as_bytes()); + std::ascii::AsciiExt::make_ascii_lowercase(&mut buffer[first_uppercase..]); + // `buffer` was initialized to a copy of `input` (which is &str so well-formed UTF-8) + // then lowercased (which preserves UTF-8 well-formedness) + unsafe { + Some(::std::str::from_utf8_unchecked(buffer)) + } + } else { + // Input is already lower-case + Some(input) + } + } else { + // Input is longer than buffer, which has the length of the longest expected string: + // none of the expected strings would match. + None + } +} + mod rules_and_declarations; #[cfg(feature = "dummy_match_byte")] diff --git a/src/nth.rs b/src/nth.rs index 675f4ba8..ec735bc2 100644 --- a/src/nth.rs +++ b/src/nth.rs @@ -16,14 +16,14 @@ pub fn parse_nth(input: &mut Parser) -> Result<(i32, i32), ()> { Token::Number(value) => Ok((0, try!(value.int_value.ok_or(())) as i32)), Token::Dimension(value, unit) => { let a = try!(value.int_value.ok_or(())) as i32; - match_ignore_ascii_case! { unit, + match_ignore_ascii_case! { &unit, "n" => parse_b(input, a), "n-" => parse_signless_b(input, a, -1), _ => Ok((a, try!(parse_n_dash_digits(&*unit)))) } } Token::Ident(value) => { - match_ignore_ascii_case! { value, + match_ignore_ascii_case! { &value, "even" => Ok((2, 0)), "odd" => Ok((2, 1)), "n" => parse_b(input, 1), @@ -39,7 +39,7 @@ pub fn parse_nth(input: &mut Parser) -> Result<(i32, i32), ()> { } Token::Delim('+') => match try!(input.next_including_whitespace()) { Token::Ident(value) => { - match_ignore_ascii_case! { value, + match_ignore_ascii_case! { &value, "n" => parse_b(input, 1), "n-" => parse_signless_b(input, 1, -1), _ => Ok((1, try!(parse_n_dash_digits(&*value))))