lightningcss/
targets.rs

1//! Browser target options.
2
3#![allow(missing_docs)]
4
5use crate::vendor_prefix::VendorPrefix;
6use bitflags::bitflags;
7#[cfg(any(feature = "serde", feature = "nodejs"))]
8use serde::{Deserialize, Serialize};
9
10/// Browser versions to compile CSS for.
11///
12/// Versions are represented as a single 24-bit integer, with one byte
13/// per `major.minor.patch` component.
14///
15/// # Example
16///
17/// This example represents a target of Safari 13.2.0.
18///
19/// ```
20/// use lightningcss::targets::Browsers;
21///
22/// let targets = Browsers {
23///   safari: Some((13 << 16) | (2 << 8)),
24///   ..Browsers::default()
25/// };
26/// ```
27#[derive(Debug, Clone, Copy, Default)]
28#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize, Deserialize))]
29#[allow(missing_docs)]
30pub struct Browsers {
31  pub android: Option<u32>,
32  pub chrome: Option<u32>,
33  pub edge: Option<u32>,
34  pub firefox: Option<u32>,
35  pub ie: Option<u32>,
36  pub ios_saf: Option<u32>,
37  pub opera: Option<u32>,
38  pub safari: Option<u32>,
39  pub samsung: Option<u32>,
40}
41
42#[cfg(feature = "browserslist")]
43#[cfg_attr(docsrs, doc(cfg(feature = "browserslist")))]
44impl Browsers {
45  /// Parses a list of browserslist queries into Lightning CSS targets.
46  pub fn from_browserslist<S: AsRef<str>, I: IntoIterator<Item = S>>(
47    query: I,
48  ) -> Result<Option<Browsers>, browserslist::Error> {
49    use browserslist::{resolve, Opts};
50
51    Self::from_distribs(resolve(query, &Opts::default())?)
52  }
53
54  /// Finds browserslist configuration, selects queries by environment and loads the resulting queries into LightningCSS targets.
55  ///
56  /// Configuration resolution is modeled after the original `browserslist` nodeJS package.
57  /// The configuration is resolved in the following order:
58  ///
59  /// - If a `BROWSERSLIST` environment variable is present, then load targets from its value. This is analog to the `--targets` CLI option.
60  ///   Example: `BROWSERSLIST="firefox ESR" lightningcss [OPTIONS] <INPUT_FILE>`
61  /// - If a `BROWSERSLIST_CONFIG` environment variable is present, then resolve the file at the provided path.
62  ///   Then parse and use targets from `package.json` or any browserslist configuration file pointed to by the environment variable.
63  ///   Example: `BROWSERSLIST_CONFIG="../config/browserslist" lightningcss [OPTIONS] <INPUT_FILE>`
64  /// - If none of the above apply, then find, parse and use targets from the first `browserslist`, `.browserslistrc`
65  ///   or `package.json` configuration file in any parent directory.
66  ///
67  /// When using parsed configuration from `browserslist`, `.browserslistrc` or `package.json` configuration files,
68  /// the environment determined by:
69  ///
70  /// - the `BROWSERSLIST_ENV` environment variable if present,
71  /// - otherwise the `NODE_ENV` environment varialbe if present,
72  /// - otherwise `production` is used.
73  ///
74  /// If no targets are found for the resulting environment, then the `defaults` configuration section is used.
75  pub fn load_browserslist() -> Result<Option<Browsers>, browserslist::Error> {
76    use browserslist::{execute, Opts};
77
78    Self::from_distribs(execute(&Opts::default())?)
79  }
80
81  fn from_distribs(distribs: Vec<browserslist::Distrib>) -> Result<Option<Browsers>, browserslist::Error> {
82    let mut browsers = Browsers::default();
83    let mut has_any = false;
84    for distrib in distribs {
85      macro_rules! browser {
86        ($browser: ident) => {{
87          if let Some(v) = parse_version(distrib.version()) {
88            if browsers.$browser.is_none() || v < browsers.$browser.unwrap() {
89              browsers.$browser = Some(v);
90              has_any = true;
91            }
92          }
93        }};
94      }
95
96      match distrib.name() {
97        "android" => browser!(android),
98        "chrome" | "and_chr" => browser!(chrome),
99        "edge" => browser!(edge),
100        "firefox" | "and_ff" => browser!(firefox),
101        "ie" => browser!(ie),
102        "ios_saf" => browser!(ios_saf),
103        "opera" | "op_mob" => browser!(opera),
104        "safari" => browser!(safari),
105        "samsung" => browser!(samsung),
106        _ => {}
107      }
108    }
109
110    if !has_any {
111      return Ok(None);
112    }
113
114    Ok(Some(browsers))
115  }
116}
117
118#[cfg(feature = "browserslist")]
119fn parse_version(version: &str) -> Option<u32> {
120  let version = version.split('-').next();
121  if version.is_none() {
122    return None;
123  }
124
125  let mut version = version.unwrap().split('.');
126  let major = version.next().and_then(|v| v.parse::<u32>().ok());
127  if let Some(major) = major {
128    let minor = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
129    let patch = version.next().and_then(|v| v.parse::<u32>().ok()).unwrap_or(0);
130    let v: u32 = (major & 0xff) << 16 | (minor & 0xff) << 8 | (patch & 0xff);
131    return Some(v);
132  }
133
134  None
135}
136
137bitflags! {
138  /// Features to explicitly enable or disable.
139  #[derive(Debug, Default, Clone, Copy)]
140  pub struct Features: u32 {
141    const Nesting = 1 << 0;
142    const NotSelectorList = 1 << 1;
143    const DirSelector = 1 << 2;
144    const LangSelectorList = 1 << 3;
145    const IsSelector = 1 << 4;
146    const TextDecorationThicknessPercent = 1 << 5;
147    const MediaIntervalSyntax = 1 << 6;
148    const MediaRangeSyntax = 1 << 7;
149    const CustomMediaQueries = 1 << 8;
150    const ClampFunction = 1 << 9;
151    const ColorFunction = 1 << 10;
152    const OklabColors = 1 << 11;
153    const LabColors = 1 << 12;
154    const P3Colors = 1 << 13;
155    const HexAlphaColors = 1 << 14;
156    const SpaceSeparatedColorNotation = 1 << 15;
157    const FontFamilySystemUi = 1 << 16;
158    const DoublePositionGradients = 1 << 17;
159    const VendorPrefixes = 1 << 18;
160    const LogicalProperties = 1 << 19;
161    const Selectors = Self::Nesting.bits() | Self::NotSelectorList.bits() | Self::DirSelector.bits() | Self::LangSelectorList.bits() | Self::IsSelector.bits();
162    const MediaQueries = Self::MediaIntervalSyntax.bits() | Self::MediaRangeSyntax.bits() | Self::CustomMediaQueries.bits();
163    const Colors = Self::ColorFunction.bits() | Self::OklabColors.bits() | Self::LabColors.bits() | Self::P3Colors.bits() | Self::HexAlphaColors.bits() | Self::SpaceSeparatedColorNotation.bits();
164  }
165}
166
167/// Target browsers and features to compile.
168#[derive(Debug, Clone, Copy, Default)]
169pub struct Targets {
170  /// Browser targets to compile the CSS for.
171  pub browsers: Option<Browsers>,
172  /// Features that should always be compiled, even when supported by targets.
173  pub include: Features,
174  /// Features that should never be compiled, even when unsupported by targets.
175  pub exclude: Features,
176}
177
178impl From<Browsers> for Targets {
179  fn from(browsers: Browsers) -> Self {
180    Self {
181      browsers: Some(browsers),
182      ..Default::default()
183    }
184  }
185}
186
187impl From<Option<Browsers>> for Targets {
188  fn from(browsers: Option<Browsers>) -> Self {
189    Self {
190      browsers,
191      ..Default::default()
192    }
193  }
194}
195
196impl Targets {
197  pub(crate) fn is_compatible(&self, feature: crate::compat::Feature) -> bool {
198    self.browsers.map(|targets| feature.is_compatible(targets)).unwrap_or(true)
199  }
200
201  pub(crate) fn should_compile(&self, feature: crate::compat::Feature, flag: Features) -> bool {
202    self.include.contains(flag) || (!self.exclude.contains(flag) && !self.is_compatible(feature))
203  }
204
205  pub(crate) fn should_compile_logical(&self, feature: crate::compat::Feature) -> bool {
206    self.should_compile(feature, Features::LogicalProperties)
207  }
208
209  pub(crate) fn should_compile_selectors(&self) -> bool {
210    self.include.intersects(Features::Selectors)
211      || (!self.exclude.intersects(Features::Selectors) && self.browsers.is_some())
212  }
213
214  pub(crate) fn prefixes(&self, prefix: VendorPrefix, feature: crate::prefixes::Feature) -> VendorPrefix {
215    if prefix.contains(VendorPrefix::None) && !self.exclude.contains(Features::VendorPrefixes) {
216      if self.include.contains(Features::VendorPrefixes) {
217        VendorPrefix::all()
218      } else {
219        self.browsers.map(|browsers| feature.prefixes_for(browsers)).unwrap_or(prefix)
220      }
221    } else {
222      prefix
223    }
224  }
225}
226
227macro_rules! should_compile {
228  ($targets: expr, $feature: ident) => {
229    $targets.should_compile(
230      crate::compat::Feature::$feature,
231      crate::targets::Features::$feature,
232    )
233  };
234}
235
236pub(crate) use should_compile;