browserslist_data/
caniuse.rs

1use ahash::AHashMap;
2use std::{borrow::Cow, sync::LazyLock};
3
4pub mod features;
5pub mod region;
6
7use crate::utils::{BinMap, PooledStr};
8
9pub const ANDROID_EVERGREEN_FIRST: f32 = 37.0;
10pub const OP_MOB_BLINK_FIRST: u32 = 14;
11
12#[derive(Clone, Debug)]
13pub struct BrowserStat(u32, u32);
14
15#[derive(Clone, Debug)]
16pub struct VersionDetail {
17    version: PooledStr,
18    pub release_date: i64,
19    // Use bool instead of Option to use pad space
20    pub released: bool,
21    pub global_usage: f32,
22}
23
24include!("generated/caniuse-browsers.rs");
25
26static CANIUSE_BROWSERS: BinMap<PooledStr, BrowserStat> = BinMap(BROWSERS_STATS);
27
28static CANIUSE_GLOBAL_USAGE: &[(PooledStr, PooledStr, f32)] =
29    include!("generated/caniuse-global-usage.rs");
30
31static BROWSER_VERSION_ALIASES: LazyLock<
32    AHashMap<&'static str, AHashMap<&'static str, &'static str>>,
33> = LazyLock::new(|| {
34    let mut aliases = CANIUSE_BROWSERS
35        .iter()
36        .filter_map(|(name, stat)| {
37            let name = name.as_str();
38            let aliases = stat
39                .version_list()
40                .iter()
41                .filter_map(|version| {
42                    version
43                        .version
44                        .as_str()
45                        .split_once('-')
46                        .map(|(bottom, top)| (bottom, top, version.version))
47                })
48                .fold(
49                    AHashMap::<&str, &str>::new(),
50                    move |mut aliases, (bottom, top, version)| {
51                        let _ = aliases.insert(bottom, version.as_str());
52                        let _ = aliases.insert(top, version.as_str());
53                        aliases
54                    },
55                );
56            if aliases.is_empty() {
57                None
58            } else {
59                Some((name, aliases))
60            }
61        })
62        .collect::<AHashMap<&'static str, _>>();
63    let _ = aliases.insert("op_mob", {
64        let mut aliases = AHashMap::new();
65        let _ = aliases.insert("59", "58");
66        aliases
67    });
68    aliases
69});
70
71static ANDROID_TO_DESKTOP: LazyLock<Vec<VersionDetail>> = LazyLock::new(|| {
72    let chrome = CANIUSE_BROWSERS.get("chrome").unwrap();
73    let android = CANIUSE_BROWSERS.get("android").unwrap();
74
75    let chrome_point = chrome
76        .version_list()
77        .binary_search_by_key(&(ANDROID_EVERGREEN_FIRST as usize), |probe| {
78            probe.version.as_str().parse::<usize>().unwrap()
79        })
80        .unwrap();
81
82    android
83        .version_list()
84        .iter()
85        .filter(|version| {
86            let version = version.version.as_str();
87            version.starts_with("2.")
88                || version.starts_with("3.")
89                || version.starts_with("4.")
90                || version == "3"
91                || version == "4"
92        })
93        .chain(chrome.version_list().iter().skip(chrome_point))
94        .cloned()
95        .collect()
96});
97
98pub fn get_browser_stat(
99    name: &str,
100    mobile_to_desktop: bool,
101) -> Option<(&'static str, &'static [VersionDetail])> {
102    let name = if name.bytes().all(|b| b.is_ascii_lowercase()) {
103        Cow::from(name)
104    } else {
105        Cow::from(name.to_ascii_lowercase())
106    };
107    let name = get_browser_alias(&name);
108
109    if mobile_to_desktop {
110        if let Some(desktop_name) = to_desktop_name(name) {
111            match name {
112                "android" => Some(("android", &*ANDROID_TO_DESKTOP)),
113                "op_mob" => {
114                    let stat = CANIUSE_BROWSERS.get("opera").unwrap();
115                    Some(("op_mob", stat.version_list()))
116                }
117                _ => CANIUSE_BROWSERS.get(desktop_name).map(|stat| {
118                    (
119                        get_mobile_by_desktop_name(desktop_name),
120                        stat.version_list(),
121                    )
122                }),
123            }
124        } else {
125            CANIUSE_BROWSERS
126                .get_key_value(name)
127                .map(|(k, v)| (k.as_str(), v.version_list()))
128        }
129    } else {
130        CANIUSE_BROWSERS
131            .get_key_value(name)
132            .map(|(k, v)| (k.as_str(), v.version_list()))
133    }
134}
135
136pub fn iter_browser_stat(
137    mobile_to_desktop: bool,
138) -> impl Iterator<Item = (&'static str, &'static [VersionDetail])> {
139    CANIUSE_BROWSERS.iter().filter_map(move |(name, stat)| {
140        match (
141            mobile_to_desktop,
142            to_desktop_name(name.as_str()),
143            name.as_str(),
144        ) {
145            (false, _, _) | (true, None, _) => Some((name.as_str(), stat.version_list())),
146            (true, Some(_), "android") => Some(("android", &*ANDROID_TO_DESKTOP)),
147            (true, Some(_), "op_mob") => {
148                let stat = CANIUSE_BROWSERS.get("opera").unwrap();
149                Some(("op_mob", stat.version_list()))
150            }
151            (true, Some(desktop_name), _) => CANIUSE_BROWSERS.get(desktop_name).map(|stat| {
152                (
153                    get_mobile_by_desktop_name(desktop_name),
154                    stat.version_list(),
155                )
156            }),
157        }
158    })
159}
160
161pub fn iter_global_usage() -> impl ExactSizeIterator<Item = (&'static str, &'static str, f32)> {
162    CANIUSE_GLOBAL_USAGE
163        .iter()
164        .copied()
165        .map(|(name, version, usage)| (name.as_str(), version.as_str(), usage))
166}
167
168pub fn get_browser_version_alias(name: &str, version: &str) -> Option<&'static str> {
169    BROWSER_VERSION_ALIASES.get(name)?.get(version).copied()
170}
171
172fn get_browser_alias(name: &str) -> &str {
173    match name {
174        "fx" | "ff" => "firefox",
175        "ios" => "ios_saf",
176        "explorer" => "ie",
177        "blackberry" => "bb",
178        "explorermobile" => "ie_mob",
179        "operamini" => "op_mini",
180        "operamobile" => "op_mob",
181        "chromeandroid" => "and_chr",
182        "firefoxandroid" => "and_ff",
183        "ucandroid" => "and_uc",
184        "qqandroid" => "and_qq",
185        _ => name,
186    }
187}
188
189pub fn to_desktop_name(name: &str) -> Option<&'static str> {
190    match name {
191        "and_chr" | "android" => Some("chrome"),
192        "and_ff" => Some("firefox"),
193        "ie_mob" => Some("ie"),
194        _ => None,
195    }
196}
197
198fn get_mobile_by_desktop_name(name: &str) -> &'static str {
199    match name {
200        "chrome" => "and_chr", // "android" has been handled as a special case
201        "firefox" => "and_ff",
202        "ie" => "ie_mob",
203        "opera" => "op_mob",
204        _ => unreachable!(),
205    }
206}
207
208pub fn normalize_version<'a>(
209    name: &'a str,
210    version_list: &'static [VersionDetail],
211    version: &'a str,
212) -> Option<&'a str> {
213    if version_list.iter().any(|v| v.version.as_str() == version) {
214        Some(version)
215    } else if let Some(version) = get_browser_version_alias(name, version) {
216        Some(version)
217    } else if version_list.len() == 1 {
218        version_list.first().map(|s| s.version.as_str())
219    } else {
220        None
221    }
222}
223
224impl BrowserStat {
225    pub fn version_list(&self) -> &'static [VersionDetail] {
226        let range = (self.0 as usize)..(self.1 as usize);
227        &VERSION_LIST[range]
228    }
229}
230
231impl VersionDetail {
232    pub fn version(&self) -> &'static str {
233        self.version.as_str()
234    }
235}