blob: 9648a5d934aaab5c0734557b6421686528dd60c8 [file] [edit]
//! 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<String>,
/// A media query.
pub media: Option<String>,
/// 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<SourceLocation> 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,
},
}
}
}