//! Dependency analysis. //! //! Dependencies in CSS can be analyzed using the `analyze_dependencies` option //! when printing a style sheet. These include other style sheets referenved via //! the `@import` rule, as well as `url()` references. See [PrinterOptions](PrinterOptions). //! //! When dependency analysis is enabled, `@import` rules are removed, and `url()` //! dependencies are replaced with hashed placeholders that can be substituted with //! the final urls later (e.g. after bundling and content hashing). use crate::css_modules::hash; use crate::printer::PrinterOptions; use crate::rules::import::ImportRule; use crate::traits::ToCss; use crate::values::url::Url; #[cfg(feature = "visitor")] use crate::visitor::Visit; use cssparser::SourceLocation; #[cfg(any(feature = "serde", feature = "nodejs"))] use serde::Serialize; /// Options for `analyze_dependencies` in `PrinterOptions`. #[derive(Default)] pub struct DependencyOptions { /// Whether to remove `@import` rules. pub remove_imports: bool, } /// A dependency. #[derive(Debug)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] #[cfg_attr( any(feature = "serde", feature = "nodejs"), serde(tag = "type", rename_all = "lowercase") )] pub enum Dependency { /// An `@import` dependency. Import(ImportDependency), /// A `url()` dependency. Url(UrlDependency), } /// An `@import` dependency. #[derive(Debug)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] pub struct ImportDependency { /// The url to import. pub url: String, /// The placeholder that the URL was replaced with. pub placeholder: String, /// An optional `supports()` condition. pub supports: Option, /// A media query. pub media: Option, /// The location of the dependency in the source file. pub loc: SourceRange, } impl ImportDependency { /// Creates a new dependency from an `@import` rule. pub fn new(rule: &ImportRule, filename: &str) -> ImportDependency { let supports = if let Some(supports) = &rule.supports { let s = supports.to_css_string(PrinterOptions::default()).unwrap(); Some(s) } else { None }; let media = if !rule.media.media_queries.is_empty() { let s = rule.media.to_css_string(PrinterOptions::default()).unwrap(); Some(s) } else { None }; let placeholder = hash(&format!("{}_{}", filename, rule.url), false); ImportDependency { url: rule.url.as_ref().to_owned(), placeholder, supports, media, loc: SourceRange::new( filename, Location { line: rule.loc.line + 1, column: rule.loc.column, }, 8, rule.url.len() + 2, ), // TODO: what about @import url(...)? } } } /// A `url()` dependency. #[derive(Debug)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] pub struct UrlDependency { /// The url of the dependency. pub url: String, /// The placeholder that the URL was replaced with. pub placeholder: String, /// The location of the dependency in the source file. pub loc: SourceRange, } impl UrlDependency { /// Creates a new url dependency. pub fn new(url: &Url, filename: &str) -> UrlDependency { let placeholder = hash(&format!("{}_{}", filename, url.url), false); UrlDependency { url: url.url.to_string(), placeholder, loc: SourceRange::new(filename, url.loc, 4, url.url.len()), } } } /// Represents the range of source code where a dependency was found. #[derive(Debug)] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(Serialize))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), serde(rename_all = "camelCase"))] pub struct SourceRange { /// The filename in which the dependency was found. pub file_path: String, /// The starting line and column position of the dependency. pub start: Location, /// The ending line and column position of the dependency. pub end: Location, } /// A line and column position within a source file. #[derive(Debug, Clone, Copy, PartialEq)] #[cfg_attr(feature = "visitor", derive(Visit))] #[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))] #[cfg_attr(any(feature = "serde"), derive(serde::Deserialize))] #[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))] #[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))] pub struct Location { /// The line number, starting from 1. pub line: u32, /// The column number, starting from 1. pub column: u32, } impl From for Location { fn from(loc: SourceLocation) -> Location { Location { line: loc.line + 1, column: loc.column, } } } impl SourceRange { fn new(filename: &str, loc: Location, offset: u32, len: usize) -> SourceRange { SourceRange { file_path: filename.into(), start: Location { line: loc.line, column: loc.column + offset, }, end: Location { line: loc.line, column: loc.column + offset + (len as u32) - 1, }, } } }