Skip to main content

css_feature_data/
browser_version.rs

1#[cfg(feature = "browserslist")]
2use browserslist::Distrib;
3use core::num::ParseIntError;
4
5#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord)]
6pub struct BrowserVersion(pub u16, pub u16);
7
8impl BrowserVersion {
9	pub fn from_string(version: &str) -> Result<Self, ParseIntError> {
10		let parts: Vec<&str> = version.split('.').collect();
11		if parts.is_empty() {
12			return Ok(BrowserVersion(version.parse()?, 0));
13		}
14		let major: u16 = parts[0].parse()?;
15		let minor: u16 = if parts.len() > 1 { parts[1].parse()? } else { 0 };
16		Ok(Self(major, minor))
17	}
18
19	pub fn from_string_as_range(version: &str) -> Result<(Self, Self), ParseIntError> {
20		if version == "all" {
21			return Ok((BrowserVersion(0, 0), BrowserVersion(u16::MAX, u16::MAX)));
22		}
23		let parts: Vec<&str> = version.split('-').collect();
24		if parts.is_empty() || parts.len() < 2 {
25			let res = Self::from_string(version)?;
26			return Ok((res, res));
27		}
28		let first = Self::from_string(parts[0])?;
29		let second = Self::from_string(parts[1])?;
30		if first > second {
31			return Ok((second, first));
32		}
33		Ok((first, second))
34	}
35
36	pub fn in_range(self, range: (Self, Self)) -> bool {
37		range.0 < self && self < range.1
38	}
39}
40
41#[derive(Debug, Copy, Clone, PartialEq)]
42pub enum NamedBrowserVersion {
43	Chrome(BrowserVersion),
44	ChromeAndroid(BrowserVersion),
45	Edge(BrowserVersion),
46	Firefox(BrowserVersion),
47	FirefoxAndroid(BrowserVersion),
48	Safari(BrowserVersion),
49	SafariIos(BrowserVersion),
50	Samsung(BrowserVersion),
51	Opera(BrowserVersion),
52	OperaMini(BrowserVersion),
53	OperaMobile(BrowserVersion),
54	QQ(BrowserVersion),
55	QQAndroid(BrowserVersion),
56	UCBrowser(BrowserVersion),
57	KaiOS(BrowserVersion),
58	AndroidWebView(BrowserVersion),
59	InternetExplorer(BrowserVersion),
60}
61
62#[derive(Debug, Clone, PartialEq)]
63pub enum NamedBrowserVersionErr {
64	ParseIntError(ParseIntError),
65	UnknownNameError(String),
66}
67
68#[cfg(feature = "browserslist")]
69impl TryFrom<Distrib> for NamedBrowserVersion {
70	type Error = NamedBrowserVersionErr;
71
72	fn try_from(value: Distrib) -> Result<Self, Self::Error> {
73		let ver =
74			BrowserVersion::from_string_as_range(value.version()).map_err(NamedBrowserVersionErr::ParseIntError)?.0;
75		match value.name() {
76			"chrome" => Ok(Self::Chrome(ver)),
77			"chrome_android" | "and_chr" => Ok(Self::ChromeAndroid(ver)),
78			"edge" => Ok(Self::Edge(ver)),
79			"firefox" => Ok(Self::Firefox(ver)),
80			"firefox_android" | "and_ff" => Ok(Self::FirefoxAndroid(ver)),
81			"safari" => Ok(Self::Safari(ver)),
82			"safari_ios" | "ios_saf" => Ok(Self::SafariIos(ver)),
83			"samsung" => Ok(Self::Samsung(ver)),
84			"opera" => Ok(Self::Opera(ver)),
85			"op_mini" => Ok(Self::OperaMini(ver)),
86			"op_mob" => Ok(Self::OperaMobile(ver)),
87			"qq" => Ok(Self::QQ(ver)),
88			"and_qq" => Ok(Self::QQAndroid(ver)),
89			"kaios" => Ok(Self::KaiOS(ver)),
90			"android" => Ok(Self::AndroidWebView(ver)),
91			"and_uc" => Ok(Self::UCBrowser(ver)),
92			"ie" => Ok(Self::InternetExplorer(ver)),
93			name => Err(NamedBrowserVersionErr::UnknownNameError(name.to_owned())),
94		}
95	}
96}
97
98impl NamedBrowserVersion {
99	/// Reports whether [crate::BrowserSupport] holds data for this browser.
100	pub fn has_support_data(self) -> bool {
101		matches!(
102			self,
103			Self::Chrome(_)
104				| Self::ChromeAndroid(_)
105				| Self::Edge(_)
106				| Self::Firefox(_)
107				| Self::FirefoxAndroid(_)
108				| Self::Safari(_)
109				| Self::SafariIos(_)
110		)
111	}
112
113	/// Converts resolved browserslist targets, keeping the browsers that [crate::BrowserSupport] holds data for.
114	#[cfg(feature = "browserslist")]
115	pub fn from_distribs(distribs: impl IntoIterator<Item = Distrib>) -> Vec<Self> {
116		distribs
117			.into_iter()
118			.filter_map(|distrib| Self::try_from(distrib).ok())
119			.filter(|browser| browser.has_support_data())
120			.collect()
121	}
122}
123
124#[cfg(test)]
125mod tests {
126	use super::*;
127
128	#[test]
129	fn test_browserversion_from_string() {
130		assert_eq!(BrowserVersion::from_string("29").unwrap(), BrowserVersion(29, 0));
131		assert_eq!(BrowserVersion::from_string("29.1").unwrap(), BrowserVersion(29, 1));
132		assert_eq!(BrowserVersion::from_string("29.12").unwrap(), BrowserVersion(29, 12));
133		assert_eq!(BrowserVersion::from_string("120").unwrap(), BrowserVersion(120, 0));
134		assert_eq!(BrowserVersion::from_string("10.0").unwrap(), BrowserVersion(10, 0));
135		assert_eq!(BrowserVersion::from_string("1.99").unwrap(), BrowserVersion(1, 99));
136	}
137
138	#[test]
139	fn test_browserversion_from_string_errors() {
140		assert!(BrowserVersion::from_string("15.2-15.3").is_err());
141		assert!(BrowserVersion::from_string("").is_err());
142		assert!(BrowserVersion::from_string("not_a_number").is_err());
143	}
144
145	#[test]
146	fn test_browserversion_from_string_as_range() {
147		assert_eq!(
148			BrowserVersion::from_string_as_range("15.2-15.3").unwrap(),
149			(BrowserVersion(15, 2), BrowserVersion(15, 3))
150		);
151		assert_eq!(
152			BrowserVersion::from_string_as_range("110.5-108.0").unwrap(),
153			(BrowserVersion(108, 0), BrowserVersion(110, 5))
154		);
155		assert!(BrowserVersion::from_string_as_range("").is_err());
156		assert!(BrowserVersion::from_string_as_range("not_a_number").is_err());
157	}
158
159	#[test]
160	fn test_browserversion_comparison() {
161		assert!(BrowserVersion(29, 0) > BrowserVersion(28, 0));
162		assert!(BrowserVersion(29, 0) < BrowserVersion(30, 0));
163		assert!(BrowserVersion(29, 1) > BrowserVersion(29, 0));
164		assert!(BrowserVersion(29, 1) < BrowserVersion(30, 1));
165		assert!(BrowserVersion(29, 1) < BrowserVersion(29, 2));
166		assert!(BrowserVersion(0, 0) < BrowserVersion(0, 1));
167		assert!(BrowserVersion(0, 1) < BrowserVersion(0, 2));
168		assert!(BrowserVersion(0, 2) > BrowserVersion(0, 1));
169	}
170
171	#[test]
172	fn test_browserversion_in_range() {
173		assert!(BrowserVersion(0, 2).in_range((BrowserVersion(0, 0), BrowserVersion(1, 0))));
174	}
175}