css_feature_data/
css_feature.rs1use crate::{
2 BaselineStatus, BrowserSupport, NamedBrowserVersion,
3 data::{CSS_FEATURES, GROUPS, SPECS},
4};
5#[cfg(feature = "browserslist")]
6use browserslist::{Error, Opts, resolve};
7use chrono::NaiveDate;
8
9#[derive(Debug, Clone, PartialEq)]
10pub struct CSSFeature {
11 pub id: &'static str,
13
14 pub name: &'static str,
16
17 pub description: &'static str,
19
20 pub spec: &'static str,
22
23 pub groups: &'static [&'static str],
25
26 pub caniuse: &'static [&'static str],
28
29 pub baseline_status: BaselineStatus,
31
32 pub browser_support: BrowserSupport,
34
35 pub popularity: f32,
37}
38
39#[derive(Debug, Clone)]
40pub struct CompatibilityResult {
41 pub is_supported: bool,
42 pub unsupported_browsers: Vec<String>,
43 pub supported_browsers: Vec<String>,
44}
45
46impl CSSFeature {
47 pub fn by_feature_name(name: &str) -> Option<&'static CSSFeature> {
48 CSS_FEATURES.get(name)
49 }
50
51 pub fn by_property_name(name: &str) -> Option<&'static CSSFeature> {
52 CSS_FEATURES.get(&format!("css.properties.{name}"))
53 }
54
55 pub fn has_baseline_support(&self) -> bool {
62 matches!(self.baseline_status, BaselineStatus::High { .. } | BaselineStatus::Low(_))
63 }
64
65 pub fn baseline_supported_since(&self) -> Option<NaiveDate> {
76 match self.baseline_status {
77 BaselineStatus::High { low_since, .. } | BaselineStatus::Low(low_since) => Some(low_since),
78 _ => None,
79 }
80 }
81
82 pub fn group_siblings(&self) -> impl Iterator<Item = &'static CSSFeature> {
84 self.groups
85 .iter()
86 .filter_map(|f| GROUPS.get(f).map(|names| names.iter().filter_map(|name| Self::by_feature_name(name))))
87 .flatten()
88 }
89
90 pub fn spec_siblings(&self) -> impl Iterator<Item = &'static CSSFeature> {
92 SPECS
93 .get(self.spec)
94 .map(|names| names.iter().filter_map(|name| Self::by_feature_name(name)))
95 .into_iter()
96 .flatten()
97 }
98
99 #[cfg(feature = "browserslist")]
101 pub fn supports_browserslist(
102 &self,
103 browserslist_query: &[&str],
104 opts: &Opts,
105 ) -> Result<CompatibilityResult, Error> {
106 let browsers = resolve(browserslist_query, opts)?;
107 let mut supported_browsers = Vec::new();
108 let mut unsupported_browsers = Vec::new();
109
110 for browser in browsers {
111 let str = format!("{} {}", browser.name(), browser.version());
112 let named_browser = NamedBrowserVersion::try_from(browser);
113 if named_browser.is_ok_and(|ver| self.browser_support.supports(ver)) {
114 supported_browsers.push(str);
115 } else {
116 unsupported_browsers.push(str);
117 }
118 }
119 Ok(CompatibilityResult {
120 is_supported: unsupported_browsers.is_empty(),
121 unsupported_browsers,
122 supported_browsers,
123 })
124 }
125
126 pub fn supports(&self, browser: NamedBrowserVersion) -> bool {
127 self.browser_support.supports(browser)
128 }
129}
130
131pub trait ToCSSFeature {
132 fn to_css_feature(&self) -> Option<&'static CSSFeature>;
133}
134
135#[cfg(test)]
136mod tests {
137 use super::*;
138
139 #[test]
140 fn test_is_baseline_supported() {
141 let flex_wrap = CSSFeature::by_feature_name("css.properties.flex-wrap");
142 assert!(flex_wrap.is_some_and(|f| f.has_baseline_support()));
143 }
144
145 #[test]
146 fn test_group_siblings() {
147 let flex_wrap = CSSFeature::by_property_name("speak").unwrap();
148 assert_eq!(
149 flex_wrap.group_siblings().map(|f| f.id).collect::<Vec<_>>(),
150 vec![
151 "css.properties.speak",
152 "css.properties.speak-as",
153 "css.properties.speak-as.digits",
154 "css.properties.speak-as.literal-punctuation",
155 "css.properties.speak-as.no-punctuation",
156 "css.properties.speak-as.normal",
157 "css.properties.speak-as.spell-out"
158 ]
159 );
160 }
161
162 #[test]
163 fn test_spec_siblings() {
164 let flex_wrap = CSSFeature::by_property_name("display").unwrap();
165 assert_eq!(
166 flex_wrap.spec_siblings().map(|f| &f.id).collect::<Vec<_>>(),
167 SPECS
168 .get("https://drafts.csswg.org/css-display-3/#the-display-properties")
169 .unwrap()
170 .iter()
171 .collect::<Vec<_>>()
172 );
173 }
174
175 #[test]
176 #[cfg(feature = "browserslist")]
177 fn test_supports_browserslist_flex_wrap() {
178 let compat = CSSFeature::by_property_name("flex-wrap")
179 .unwrap()
180 .supports_browserslist(&["Chrome >= 29", "Firefox >= 28", "Safari >= 9.1"], &Default::default())
181 .unwrap();
182
183 assert!(compat.is_supported, "flex-wrap should be supported");
185 assert!(!compat.supported_browsers.is_empty(), "Should have supported browsers");
186 assert!(compat.unsupported_browsers.is_empty(), "Should have no unsupported browsers for this query");
187 }
188
189 #[test]
190 #[cfg(feature = "browserslist")]
191 fn test_supports_browserslist_ranged() {
192 let compat = CSSFeature::by_property_name("flex-wrap")
193 .unwrap()
194 .supports_browserslist(&["> 1%", "last 2 versions", "not dead", "ie 6"], &Default::default())
195 .unwrap();
196
197 assert!(!compat.is_supported, "flex-wrap should be supported");
199 assert!(!compat.supported_browsers.is_empty(), "Should have supported browsers");
200 assert!(!compat.unsupported_browsers.is_empty(), "Should have unsupported browsers");
201 assert!(compat.unsupported_browsers.iter().any(|b| b == "ie 6"), "Includes IE6 in unsupported_browsers")
202 }
203
204 #[test]
205 #[cfg(feature = "browserslist")]
206 fn test_invalid_browserslist_query() {
207 let result = CSSFeature::by_property_name("flex-wrap")
208 .unwrap()
209 .supports_browserslist(&["invalid browser query !@#$%"], &Default::default());
210
211 assert!(result.is_err());
212 }
213}