css_feature_data/
css_feature.rs

1use 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	/// The ID of the feature.
12	pub id: &'static str,
13
14	/// A friendly, human-readable name of the feature.
15	pub name: &'static str,
16
17	/// A description for this feature.
18	pub description: &'static str,
19
20	/// A URL to the CSS specification that includes this feature.
21	pub spec: &'static str,
22
23	/// The groups this feature is part of.
24	pub groups: &'static [&'static str],
25
26	/// The CanIUse URLs available for this feature.
27	pub caniuse: &'static [&'static str],
28
29	/// The current BaselineStatus of this feature
30	pub baseline_status: BaselineStatus,
31
32	/// The browsers that support this feature.
33	pub browser_support: BrowserSupport,
34
35	/// The percentage of web pages which use this feature, as reported by Chrome usage data.
36	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	/// Check if a feature has Baseline support.
56	///
57	/// ```rust
58	/// use css_feature_data::CSSFeature;
59	/// assert_eq!(CSSFeature::by_property_name("word-break").is_some_and(|f| f.has_baseline_support()), true)
60	/// ```
61	pub fn has_baseline_support(&self) -> bool {
62		matches!(self.baseline_status, BaselineStatus::High { .. } | BaselineStatus::Low(_))
63	}
64
65	/// Check the earliest date this feature was supported as Baseline.
66	///
67	/// If BaselineStatus::Low, then that date will be returned.
68	/// If BaselineStatus::High, then the low date will be returned.
69	///
70	/// ```rust
71	/// use css_feature_data::CSSFeature;
72	/// use chrono::NaiveDate;
73	/// assert_eq!(CSSFeature::by_property_name("word-break").map(|f| f.baseline_supported_since()), Some(NaiveDate::from_ymd_opt(2015,07,29)));
74	/// ```
75	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	/// Get all CSS properties in the same groups as this one
83	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	/// Get all CSS properties in the same specification as this one
91	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	/// Check if a CSS property is supported across browsers specified by a browserslist query
100	#[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		// flex-wrap should be fully supported in these modern browsers
185		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		// flex-wrap should be fully supported in these browsers
199		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}