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 dbg!(&named_browser);
114 if named_browser.is_ok_and(|ver| self.browser_support.supports(ver)) {
115 supported_browsers.push(str);
116 } else {
117 unsupported_browsers.push(str);
118 }
119 }
120 Ok(CompatibilityResult {
121 is_supported: unsupported_browsers.is_empty(),
122 unsupported_browsers,
123 supported_browsers,
124 })
125 }
126
127 pub fn supports(&self, browser: NamedBrowserVersion) -> bool {
128 self.browser_support.supports(browser)
129 }
130}
131
132pub trait ToCSSFeature {
133 fn to_css_feature(&self) -> Option<&'static CSSFeature>;
134}
135
136#[cfg(test)]
137mod tests {
138 use super::*;
139
140 #[test]
141 fn test_is_baseline_supported() {
142 let flex_wrap = CSSFeature::by_feature_name("css.properties.flex-wrap");
143 assert!(flex_wrap.is_some_and(|f| f.has_baseline_support()));
144 }
145
146 #[test]
147 fn test_group_siblings() {
148 let flex_wrap = CSSFeature::by_property_name("speak").unwrap();
149 assert_eq!(
150 flex_wrap.group_siblings().map(|f| f.id).collect::<Vec<_>>(),
151 vec![
152 "css.properties.speak",
153 "css.properties.speak-as",
154 "css.properties.speak-as.digits",
155 "css.properties.speak-as.literal-punctuation",
156 "css.properties.speak-as.no-punctuation",
157 "css.properties.speak-as.normal",
158 "css.properties.speak-as.spell-out"
159 ]
160 );
161 }
162
163 #[test]
164 fn test_spec_siblings() {
165 let flex_wrap = CSSFeature::by_property_name("display").unwrap();
166 assert_eq!(
167 flex_wrap.spec_siblings().map(|f| &f.id).collect::<Vec<_>>(),
168 SPECS
169 .get("https://drafts.csswg.org/css-display-3/#the-display-properties")
170 .unwrap()
171 .iter()
172 .collect::<Vec<_>>()
173 );
174 }
175
176 #[test]
177 #[cfg(feature = "browserslist")]
178 fn test_supports_browserslist_flex_wrap() {
179 let compat = CSSFeature::by_property_name("flex-wrap")
180 .unwrap()
181 .supports_browserslist(&["Chrome >= 30", "Firefox >= 21", "Safari >= 9.1"], &Default::default())
182 .unwrap();
183
184 assert!(compat.is_supported, "flex-wrap should be supported");
186 assert!(!compat.supported_browsers.is_empty(), "Should have supported browsers");
187 assert!(compat.unsupported_browsers.is_empty(), "Should have no unsupported browsers for this query");
188 }
189
190 #[test]
191 #[cfg(feature = "browserslist")]
192 fn test_supports_browserslist_ranged() {
193 let compat = CSSFeature::by_property_name("flex-wrap")
194 .unwrap()
195 .supports_browserslist(&["> 1%", "last 2 versions", "not dead", "ie 6"], &Default::default())
196 .unwrap();
197
198 assert!(!compat.is_supported, "flex-wrap should be supported");
200 assert!(!compat.supported_browsers.is_empty(), "Should have supported browsers");
201 assert!(!compat.unsupported_browsers.is_empty(), "Should have unsupported browsers");
202 assert!(compat.unsupported_browsers.iter().any(|b| b == "ie 6"), "Includes IE6 in unsupported_browsers")
203 }
204
205 #[test]
206 #[cfg(feature = "browserslist")]
207 fn test_invalid_browserslist_query() {
208 let result = CSSFeature::by_property_name("flex-wrap")
209 .unwrap()
210 .supports_browserslist(&["invalid browser query !@#$%"], &Default::default());
211
212 assert!(result.is_err());
213 }
214}