blob: 5981a42e6cd30e30586b9fc3583812bef4ec8db9 [file] [log] [blame]
//! CSS rules.
//!
//! The [CssRule](CssRule) enum includes all supported rules, and can be used to parse
//! and serialize rules from CSS. Lists of rules (i.e. within a stylesheet, or inside
//! another rule such as `@media`) are represented by [CssRuleList](CssRuleList).
//!
//! Each rule includes a source location, which indicates the line and column within
//! the source file where it was parsed. This is used when generating source maps.
//!
//! # Example
//!
//! This example shows how you could parse a single CSS rule, and serialize it to a string.
//!
//! ```
//! use lightningcss::{
//! rules::CssRule,
//! traits::ToCss,
//! stylesheet::{ParserOptions, PrinterOptions}
//! };
//!
//! let rule = CssRule::parse_string(
//! ".foo { color: red; }",
//! ParserOptions::default()
//! ).unwrap();
//!
//! assert_eq!(
//! rule.to_css_string(PrinterOptions::default()).unwrap(),
//! ".foo {\n color: red;\n}"
//! );
//! ```
//!
//! If you have a [cssparser::Parser](cssparser::Parser) already, you can also use the `parse` and `to_css`
//! methods instead, rather than parsing from a string.
//!
//! See [StyleSheet](super::stylesheet::StyleSheet) to parse an entire file of multiple rules.
#![deny(missing_docs)]
pub mod container;
pub mod counter_style;
pub mod custom_media;
pub mod document;
pub mod font_face;
pub mod font_feature_values;
pub mod font_palette_values;
pub mod import;
pub mod keyframes;
pub mod layer;
pub mod media;
pub mod namespace;
pub mod nesting;
pub mod page;
pub mod property;
pub mod scope;
pub mod starting_style;
pub mod style;
pub mod supports;
pub mod unknown;
pub mod view_transition;
pub mod viewport;
use self::font_feature_values::FontFeatureValuesRule;
use self::font_palette_values::FontPaletteValuesRule;
use self::layer::{LayerBlockRule, LayerStatementRule};
use self::property::PropertyRule;
use crate::context::PropertyHandlerContext;
use crate::declaration::{DeclarationBlock, DeclarationHandler};
use crate::dependencies::{Dependency, ImportDependency};
use crate::error::{MinifyError, ParserError, PrinterError, PrinterErrorKind};
use crate::parser::{parse_rule_list, parse_style_block, DefaultAtRule, DefaultAtRuleParser, TopLevelRuleParser};
use crate::prefixes::Feature;
use crate::printer::Printer;
use crate::rules::keyframes::KeyframesName;
use crate::selector::{is_compatible, is_equivalent, Component, Selector, SelectorList};
use crate::stylesheet::ParserOptions;
use crate::targets::TargetsWithSupportsScope;
use crate::traits::{AtRuleParser, ToCss};
use crate::values::string::CowArcStr;
use crate::vendor_prefix::VendorPrefix;
#[cfg(feature = "visitor")]
use crate::visitor::{Visit, VisitTypes, Visitor};
use container::ContainerRule;
use counter_style::CounterStyleRule;
use cssparser::{parse_one_rule, ParseError, Parser, ParserInput};
use custom_media::CustomMediaRule;
use document::MozDocumentRule;
use font_face::FontFaceRule;
use import::ImportRule;
use itertools::Itertools;
use keyframes::KeyframesRule;
use media::MediaRule;
use namespace::NamespaceRule;
use nesting::{NestedDeclarationsRule, NestingRule};
use page::PageRule;
use scope::ScopeRule;
use smallvec::{smallvec, SmallVec};
use starting_style::StartingStyleRule;
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasherDefault, Hasher};
use style::StyleRule;
use supports::SupportsRule;
use unknown::UnknownAtRule;
use view_transition::ViewTransitionRule;
use viewport::ViewportRule;
#[derive(Clone)]
pub(crate) struct StyleContext<'a, 'i> {
pub selectors: &'a SelectorList<'i>,
pub parent: Option<&'a StyleContext<'a, 'i>>,
}
/// A source location.
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
#[cfg_attr(any(feature = "serde", feature = "nodejs"), derive(serde::Serialize))]
#[cfg_attr(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 index of the source file within the source map.
pub source_index: u32,
/// The line number, starting at 0.
pub line: u32,
/// The column number within a line, starting at 1 for first the character of the line.
/// Column numbers are counted in UTF-16 code units.
pub column: u32,
}
/// A CSS rule.
#[derive(Debug, PartialEq, Clone)]
#[cfg_attr(feature = "visitor", derive(Visit))]
#[cfg_attr(feature = "visitor", visit(visit_rule, RULES))]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize),
serde(tag = "type", content = "value", rename_all = "kebab-case")
)]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema), schemars(rename = "Rule"))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub enum CssRule<'i, R = DefaultAtRule> {
/// A `@media` rule.
#[cfg_attr(feature = "serde", serde(borrow))]
Media(MediaRule<'i, R>),
/// An `@import` rule.
Import(ImportRule<'i>),
/// A style rule.
Style(StyleRule<'i, R>),
/// A `@keyframes` rule.
Keyframes(KeyframesRule<'i>),
/// A `@font-face` rule.
FontFace(FontFaceRule<'i>),
/// A `@font-palette-values` rule.
FontPaletteValues(FontPaletteValuesRule<'i>),
/// A `@font-feature-values` rule.
FontFeatureValues(FontFeatureValuesRule<'i>),
/// A `@page` rule.
Page(PageRule<'i>),
/// A `@supports` rule.
Supports(SupportsRule<'i, R>),
/// A `@counter-style` rule.
CounterStyle(CounterStyleRule<'i>),
/// A `@namespace` rule.
Namespace(NamespaceRule<'i>),
/// A `@-moz-document` rule.
MozDocument(MozDocumentRule<'i, R>),
/// A `@nest` rule.
Nesting(NestingRule<'i, R>),
/// A nested declarations rule.
NestedDeclarations(NestedDeclarationsRule<'i>),
/// A `@viewport` rule.
Viewport(ViewportRule<'i>),
/// A `@custom-media` rule.
CustomMedia(CustomMediaRule<'i>),
/// A `@layer` statement rule.
LayerStatement(LayerStatementRule<'i>),
/// A `@layer` block rule.
LayerBlock(LayerBlockRule<'i, R>),
/// A `@property` rule.
Property(PropertyRule<'i>),
/// A `@container` rule.
Container(ContainerRule<'i, R>),
/// A `@scope` rule.
Scope(ScopeRule<'i, R>),
/// A `@starting-style` rule.
StartingStyle(StartingStyleRule<'i, R>),
/// A `@view-transition` rule.
ViewTransition(ViewTransitionRule<'i>),
/// A placeholder for a rule that was removed.
Ignored,
/// An unknown at-rule.
Unknown(UnknownAtRule<'i>),
/// A custom at-rule.
Custom(R),
}
// Manually implemented deserialize to reduce binary size.
#[cfg(feature = "serde")]
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
impl<'i, 'de: 'i, R: serde::Deserialize<'de>> serde::Deserialize<'de> for CssRule<'i, R> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(serde::Deserialize)]
#[serde(field_identifier, rename_all = "snake_case")]
enum Field {
Type,
Value,
}
struct PartialRule<'de> {
rule_type: CowArcStr<'de>,
content: serde::__private::de::Content<'de>,
}
struct CssRuleVisitor;
impl<'de> serde::de::Visitor<'de> for CssRuleVisitor {
type Value = PartialRule<'de>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a CssRule")
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut rule_type: Option<CowArcStr<'de>> = None;
let mut value: Option<serde::__private::de::Content> = None;
while let Some(key) = map.next_key()? {
match key {
Field::Type => {
rule_type = Some(map.next_value()?);
}
Field::Value => {
value = Some(map.next_value()?);
}
}
}
let rule_type = rule_type.ok_or_else(|| serde::de::Error::missing_field("type"))?;
let content = value.ok_or_else(|| serde::de::Error::missing_field("value"))?;
Ok(PartialRule { rule_type, content })
}
}
let partial = deserializer.deserialize_map(CssRuleVisitor)?;
let deserializer = serde::__private::de::ContentDeserializer::new(partial.content);
match partial.rule_type.as_ref() {
"media" => {
let rule = MediaRule::deserialize(deserializer)?;
Ok(CssRule::Media(rule))
}
"import" => {
let rule = ImportRule::deserialize(deserializer)?;
Ok(CssRule::Import(rule))
}
"style" => {
let rule = StyleRule::deserialize(deserializer)?;
Ok(CssRule::Style(rule))
}
"keyframes" => {
let rule = KeyframesRule::deserialize(deserializer)?;
Ok(CssRule::Keyframes(rule))
}
"font-face" => {
let rule = FontFaceRule::deserialize(deserializer)?;
Ok(CssRule::FontFace(rule))
}
"font-palette-values" => {
let rule = FontPaletteValuesRule::deserialize(deserializer)?;
Ok(CssRule::FontPaletteValues(rule))
}
"font-feature-values" => {
let rule = FontFeatureValuesRule::deserialize(deserializer)?;
Ok(CssRule::FontFeatureValues(rule))
}
"page" => {
let rule = PageRule::deserialize(deserializer)?;
Ok(CssRule::Page(rule))
}
"supports" => {
let rule = SupportsRule::deserialize(deserializer)?;
Ok(CssRule::Supports(rule))
}
"counter-style" => {
let rule = CounterStyleRule::deserialize(deserializer)?;
Ok(CssRule::CounterStyle(rule))
}
"namespace" => {
let rule = NamespaceRule::deserialize(deserializer)?;
Ok(CssRule::Namespace(rule))
}
"moz-document" => {
let rule = MozDocumentRule::deserialize(deserializer)?;
Ok(CssRule::MozDocument(rule))
}
"nesting" => {
let rule = NestingRule::deserialize(deserializer)?;
Ok(CssRule::Nesting(rule))
}
"nested-declarations" => {
let rule = NestedDeclarationsRule::deserialize(deserializer)?;
Ok(CssRule::NestedDeclarations(rule))
}
"viewport" => {
let rule = ViewportRule::deserialize(deserializer)?;
Ok(CssRule::Viewport(rule))
}
"custom-media" => {
let rule = CustomMediaRule::deserialize(deserializer)?;
Ok(CssRule::CustomMedia(rule))
}
"layer-statement" => {
let rule = LayerStatementRule::deserialize(deserializer)?;
Ok(CssRule::LayerStatement(rule))
}
"layer-block" => {
let rule = LayerBlockRule::deserialize(deserializer)?;
Ok(CssRule::LayerBlock(rule))
}
"property" => {
let rule = PropertyRule::deserialize(deserializer)?;
Ok(CssRule::Property(rule))
}
"container" => {
let rule = ContainerRule::deserialize(deserializer)?;
Ok(CssRule::Container(rule))
}
"scope" => {
let rule = ScopeRule::deserialize(deserializer)?;
Ok(CssRule::Scope(rule))
}
"starting-style" => {
let rule = StartingStyleRule::deserialize(deserializer)?;
Ok(CssRule::StartingStyle(rule))
}
"view-transition" => {
let rule = ViewTransitionRule::deserialize(deserializer)?;
Ok(CssRule::ViewTransition(rule))
}
"ignored" => Ok(CssRule::Ignored),
"unknown" => {
let rule = UnknownAtRule::deserialize(deserializer)?;
Ok(CssRule::Unknown(rule))
}
"custom" => {
let rule = R::deserialize(deserializer)?;
Ok(CssRule::Custom(rule))
}
t => Err(serde::de::Error::unknown_variant(t, &[])),
}
}
}
impl<'a, 'i, T: ToCss> ToCss for CssRule<'i, T> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
match self {
CssRule::Media(media) => media.to_css(dest),
CssRule::Import(import) => import.to_css(dest),
CssRule::Style(style) => style.to_css(dest),
CssRule::Keyframes(keyframes) => keyframes.to_css(dest),
CssRule::FontFace(font_face) => font_face.to_css(dest),
CssRule::FontPaletteValues(f) => f.to_css(dest),
CssRule::FontFeatureValues(font_feature_values) => font_feature_values.to_css(dest),
CssRule::Page(font_face) => font_face.to_css(dest),
CssRule::Supports(supports) => supports.to_css(dest),
CssRule::CounterStyle(counter_style) => counter_style.to_css(dest),
CssRule::Namespace(namespace) => namespace.to_css(dest),
CssRule::MozDocument(document) => document.to_css(dest),
CssRule::Nesting(nesting) => nesting.to_css(dest),
CssRule::NestedDeclarations(nested) => nested.to_css(dest),
CssRule::Viewport(viewport) => viewport.to_css(dest),
CssRule::CustomMedia(custom_media) => custom_media.to_css(dest),
CssRule::LayerStatement(layer) => layer.to_css(dest),
CssRule::LayerBlock(layer) => layer.to_css(dest),
CssRule::Property(property) => property.to_css(dest),
CssRule::StartingStyle(rule) => rule.to_css(dest),
CssRule::Container(container) => container.to_css(dest),
CssRule::Scope(scope) => scope.to_css(dest),
CssRule::ViewTransition(rule) => rule.to_css(dest),
CssRule::Unknown(unknown) => unknown.to_css(dest),
CssRule::Custom(rule) => rule.to_css(dest).map_err(|_| PrinterError {
kind: PrinterErrorKind::FmtError,
loc: None,
}),
CssRule::Ignored => Ok(()),
}
}
}
impl<'i> CssRule<'i, DefaultAtRule> {
/// Parse a single rule.
pub fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Self::parse_with(input, options, &mut DefaultAtRuleParser)
}
/// Parse a single rule from a string.
pub fn parse_string(
input: &'i str,
options: ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Self::parse_string_with(input, options, &mut DefaultAtRuleParser)
}
}
impl<'i, T> CssRule<'i, T> {
/// Parse a single rule.
pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
at_rule_parser: &mut P,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut rules = CssRuleList(Vec::new());
parse_one_rule(input, &mut TopLevelRuleParser::new(options, at_rule_parser, &mut rules))?;
Ok(rules.0.pop().unwrap())
}
/// Parse a single rule from a string.
pub fn parse_string_with<P: AtRuleParser<'i, AtRule = T>>(
input: &'i str,
options: ParserOptions<'_, 'i>,
at_rule_parser: &mut P,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
let mut input = ParserInput::new(input);
let mut parser = Parser::new(&mut input);
Self::parse_with(&mut parser, &options, at_rule_parser)
}
}
/// A list of CSS rules.
#[derive(Debug, PartialEq, Clone, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(transparent))]
#[cfg_attr(feature = "jsonschema", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "into_owned", derive(static_self::IntoOwned))]
pub struct CssRuleList<'i, R = DefaultAtRule>(
#[cfg_attr(feature = "serde", serde(borrow))] pub Vec<CssRule<'i, R>>,
);
impl<'i> CssRuleList<'i, DefaultAtRule> {
/// Parse a rule list.
pub fn parse<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Self::parse_with(input, options, &mut DefaultAtRuleParser)
}
/// Parse a style block, with both declarations and rules.
/// Resulting declarations are returned in a nested style rule.
pub fn parse_style_block<'t>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
is_nested: bool,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
Self::parse_style_block_with(input, options, &mut DefaultAtRuleParser, is_nested)
}
}
impl<'i, T> CssRuleList<'i, T> {
/// Parse a rule list with a custom at rule parser.
pub fn parse_with<'t, P: AtRuleParser<'i, AtRule = T>>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
at_rule_parser: &mut P,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
parse_rule_list(input, options, at_rule_parser)
}
/// Parse a style block, with both declarations and rules.
/// Resulting declarations are returned in a nested style rule.
pub fn parse_style_block_with<'t, P: AtRuleParser<'i, AtRule = T>>(
input: &mut Parser<'i, 't>,
options: &ParserOptions<'_, 'i>,
at_rule_parser: &mut P,
is_nested: bool,
) -> Result<Self, ParseError<'i, ParserError<'i>>> {
parse_style_block(input, options, at_rule_parser, is_nested)
}
}
// Manually implemented to avoid circular child types.
#[cfg(feature = "visitor")]
#[cfg_attr(docsrs, doc(cfg(feature = "visitor")))]
impl<'i, T: Visit<'i, T, V>, V: ?Sized + Visitor<'i, T>> Visit<'i, T, V> for CssRuleList<'i, T> {
const CHILD_TYPES: VisitTypes = VisitTypes::all();
fn visit(&mut self, visitor: &mut V) -> Result<(), V::Error> {
if visitor.visit_types().contains(VisitTypes::RULES) {
visitor.visit_rule_list(self)
} else {
self.0.visit(visitor)
}
}
fn visit_children(&mut self, visitor: &mut V) -> Result<(), V::Error> {
self.0.visit(visitor)
}
}
pub(crate) struct MinifyContext<'a, 'i> {
pub targets: TargetsWithSupportsScope,
pub handler: &'a mut DeclarationHandler<'i>,
pub important_handler: &'a mut DeclarationHandler<'i>,
pub handler_context: PropertyHandlerContext<'i, 'a>,
pub unused_symbols: &'a HashSet<String>,
pub custom_media: Option<HashMap<CowArcStr<'i>, CustomMediaRule<'i>>>,
pub css_modules: bool,
pub pure_css_modules: bool,
}
impl<'i, T: Clone> CssRuleList<'i, T> {
pub(crate) fn minify(
&mut self,
context: &mut MinifyContext<'_, 'i>,
parent_is_unused: bool,
) -> Result<(), MinifyError> {
let mut keyframe_rules = HashMap::new();
let mut layer_rules = HashMap::new();
let mut has_layers = false;
let mut property_rules = HashMap::new();
let mut font_feature_values_rules = Vec::new();
let mut style_rules =
HashMap::with_capacity_and_hasher(self.0.len(), BuildHasherDefault::<PrecomputedHasher>::default());
let mut rules = Vec::new();
for mut rule in self.0.drain(..) {
match &mut rule {
CssRule::Keyframes(keyframes) => {
if context.unused_symbols.contains(match &keyframes.name {
KeyframesName::Ident(ident) => ident.0.as_ref(),
KeyframesName::Custom(string) => string.as_ref(),
}) {
continue;
}
keyframes.minify(context);
macro_rules! set_prefix {
($keyframes: ident) => {
$keyframes.vendor_prefix =
context.targets.current.prefixes($keyframes.vendor_prefix, Feature::AtKeyframes);
};
}
// Merge @keyframes rules with the same name.
if let Some(existing_idx) = keyframe_rules.get(&keyframes.name) {
if let Some(CssRule::Keyframes(existing)) = &mut rules.get_mut(*existing_idx) {
// If the existing rule has the same vendor prefixes, replace it with this rule.
if existing.vendor_prefix == keyframes.vendor_prefix {
*existing = keyframes.clone();
continue;
}
// Otherwise, if the keyframes are identical, merge the prefixes.
if existing.keyframes == keyframes.keyframes {
existing.vendor_prefix |= keyframes.vendor_prefix;
set_prefix!(existing);
continue;
}
}
}
set_prefix!(keyframes);
keyframe_rules.insert(keyframes.name.clone(), rules.len());
let fallbacks = keyframes.get_fallbacks(&context.targets.current);
rules.push(rule);
rules.extend(fallbacks);
continue;
}
CssRule::CustomMedia(_) => {
if context.custom_media.is_some() {
continue;
}
}
CssRule::Media(media) => {
if let Some(CssRule::Media(last_rule)) = rules.last_mut() {
if last_rule.query == media.query {
last_rule.rules.0.extend(media.rules.0.drain(..));
last_rule.minify(context, parent_is_unused)?;
continue;
}
}
if media.minify(context, parent_is_unused)? {
continue;
}
}
CssRule::Supports(supports) => {
if let Some(CssRule::Supports(last_rule)) = rules.last_mut() {
if last_rule.condition == supports.condition {
last_rule.rules.0.extend(supports.rules.0.drain(..));
last_rule.minify(context, parent_is_unused)?;
continue;
}
}
supports.minify(context, parent_is_unused)?;
if supports.rules.0.is_empty() {
continue;
}
}
CssRule::Container(container) => {
if let Some(CssRule::Container(last_rule)) = rules.last_mut() {
if last_rule.name == container.name && last_rule.condition == container.condition {
last_rule.rules.0.extend(container.rules.0.drain(..));
last_rule.minify(context, parent_is_unused)?;
continue;
}
}
if container.minify(context, parent_is_unused)? {
continue;
}
}
CssRule::LayerBlock(layer) => {
// Merging non-adjacent layer rules is safe because they are applied
// in the order they are first defined.
if let Some(name) = &layer.name {
if let Some(idx) = layer_rules.get(name) {
if let Some(CssRule::LayerBlock(last_rule)) = rules.get_mut(*idx) {
last_rule.rules.0.extend(layer.rules.0.drain(..));
continue;
}
}
layer_rules.insert(name.clone(), rules.len());
has_layers = true;
}
}
CssRule::LayerStatement(layer) => {
// Create @layer block rules for each declared layer name,
// so we can merge other blocks into it later on.
for name in &layer.names {
if !layer_rules.contains_key(name) {
layer_rules.insert(name.clone(), rules.len());
has_layers = true;
rules.push(CssRule::LayerBlock(LayerBlockRule {
name: Some(name.clone()),
rules: CssRuleList(vec![]),
loc: layer.loc.clone(),
}));
}
}
continue;
}
CssRule::MozDocument(document) => document.minify(context)?,
CssRule::Style(style) => {
if parent_is_unused || style.minify(context, parent_is_unused)? {
continue;
}
// If some of the selectors in this rule are not compatible with the targets,
// we need to either wrap in :is() or split them into multiple rules.
let incompatible = if style.selectors.0.len() > 1
&& context.targets.current.should_compile_selectors()
&& !style.is_compatible(context.targets.current)
{
// The :is() selector accepts a forgiving selector list, so use that if possible.
// Note that :is() does not allow pseudo elements, so we need to check for that.
// In addition, :is() takes the highest specificity of its arguments, so if the selectors
// have different weights, we need to split them into separate rules as well.
if context.targets.current.is_compatible(crate::compat::Feature::IsSelector)
&& !style.selectors.0.iter().any(|selector| selector.has_pseudo_element())
&& style.selectors.0.iter().map(|selector| selector.specificity()).all_equal()
{
style.selectors =
SelectorList::new(smallvec![
Component::Is(style.selectors.0.clone().into_boxed_slice()).into()
]);
smallvec![]
} else {
// Otherwise, partition the selectors and keep the compatible ones in this rule.
// We will generate additional rules for incompatible selectors later.
let (compatible, incompatible) = style
.selectors
.0
.iter()
.cloned()
.partition::<SmallVec<[Selector; 1]>, _>(|selector| {
let list = SelectorList::new(smallvec![selector.clone()]);
is_compatible(&list.0, context.targets.current)
});
style.selectors = SelectorList::new(compatible);
incompatible
}
} else {
smallvec![]
};
style.update_prefix(context);
// Attempt to merge the new rule with the last rule we added.
let mut merged = false;
if let Some(CssRule::Style(last_style_rule)) = rules.last_mut() {
if merge_style_rules(style, last_style_rule, context) {
// If that was successful, then the last rule has been updated to include the
// selectors/declarations of the new rule. This might mean that we can merge it
// with the previous rule, so continue trying while we have style rules available.
while rules.len() >= 2 {
let len = rules.len();
let (a, b) = rules.split_at_mut(len - 1);
if let (CssRule::Style(last), CssRule::Style(prev)) = (&mut b[0], &mut a[len - 2]) {
if merge_style_rules(last, prev, context) {
// If we were able to merge the last rule into the previous one, remove the last.
rules.pop();
continue;
}
}
// If we didn't see a style rule, or were unable to merge, stop.
break;
}
merged = true;
}
}
// Create additional rules for logical properties, @supports overrides, and incompatible selectors.
let supports = context.handler_context.get_supports_rules(&style);
let logical = context.handler_context.get_additional_rules(&style);
let incompatible_rules = incompatible
.into_iter()
.map(|selector| {
// Create a clone of the rule with only the one incompatible selector.
let list = SelectorList::new(smallvec![selector]);
let mut clone = style.clone();
clone.selectors = list;
clone.update_prefix(context);
// Also add rules for logical properties and @supports overrides.
let supports = context.handler_context.get_supports_rules(&clone);
let logical = context.handler_context.get_additional_rules(&clone);
(clone, logical, supports)
})
.collect::<Vec<_>>();
context.handler_context.reset();
// If the rule has nested rules, and we have extra rules to insert such as for logical properties,
// we need to split the rule in two so we can insert the extra rules in between the declarations from
// the main rule and the nested rules.
let nested_rule = if !style.rules.0.is_empty()
// can happen if there are no compatible rules, above.
&& !style.selectors.0.is_empty()
&& (!logical.is_empty() || !supports.is_empty() || !incompatible_rules.is_empty())
{
let mut rules = CssRuleList(vec![]);
std::mem::swap(&mut style.rules, &mut rules);
Some(StyleRule {
selectors: style.selectors.clone(),
declarations: DeclarationBlock::default(),
rules,
vendor_prefix: style.vendor_prefix,
loc: style.loc,
})
} else {
None
};
if !merged && !style.is_empty() {
let source_index = style.loc.source_index;
let has_no_rules = style.rules.0.is_empty();
let idx = rules.len();
rules.push(rule);
// Check if this rule is a duplicate of an earlier rule, meaning it has
// the same selectors and defines the same properties. If so, remove the
// earlier rule because this one completely overrides it.
if has_no_rules {
// SAFETY: StyleRuleKeys never live beyond this method.
let key = StyleRuleKey::new(unsafe { &*(&rules as *const _) }, idx);
if idx > 0 {
if let Some(i) = style_rules.remove(&key) {
if let CssRule::Style(other) = &rules[i] {
// Don't remove the rule if this is a CSS module and the other rule came from a different file.
if !context.css_modules || source_index == other.loc.source_index {
// Only mark the rule as ignored so we don't need to change all of the indices.
rules[i] = CssRule::Ignored;
}
}
}
}
style_rules.insert(key, idx);
}
}
if !logical.is_empty() {
let mut logical = CssRuleList(logical);
logical.minify(context, parent_is_unused)?;
rules.extend(logical.0)
}
rules.extend(supports);
for (rule, logical, supports) in incompatible_rules {
if !rule.is_empty() {
rules.push(CssRule::Style(rule));
}
if !logical.is_empty() {
let mut logical = CssRuleList(logical);
logical.minify(context, parent_is_unused)?;
rules.extend(logical.0)
}
rules.extend(supports);
}
if let Some(nested_rule) = nested_rule {
rules.push(CssRule::Style(nested_rule));
}
continue;
}
CssRule::CounterStyle(counter_style) => {
if context.unused_symbols.contains(counter_style.name.0.as_ref()) {
continue;
}
}
CssRule::Scope(scope) => scope.minify(context)?,
CssRule::Nesting(nesting) => {
if nesting.minify(context, parent_is_unused)? {
continue;
}
}
CssRule::NestedDeclarations(nested) => {
if nested.minify(context, parent_is_unused) {
continue;
}
}
CssRule::StartingStyle(rule) => {
if rule.minify(context, parent_is_unused)? {
continue;
}
}
CssRule::FontPaletteValues(f) => {
if context.unused_symbols.contains(f.name.0.as_ref()) {
continue;
}
f.minify(context, parent_is_unused);
let fallbacks = f.get_fallbacks(context.targets.current);
rules.push(rule);
rules.extend(fallbacks);
continue;
}
CssRule::FontFeatureValues(rule) => {
if let Some(index) = font_feature_values_rules
.iter()
.find(|index| matches!(&rules[**index], CssRule::FontFeatureValues(r) if r.name == rule.name))
{
if let CssRule::FontFeatureValues(existing) = &mut rules[*index] {
existing.merge(rule);
}
continue;
} else {
font_feature_values_rules.push(rules.len());
}
}
CssRule::Property(property) => {
if context.unused_symbols.contains(property.name.0.as_ref()) {
continue;
}
if let Some(index) = property_rules.get(&property.name) {
rules[*index] = rule;
continue;
} else {
property_rules.insert(property.name.clone(), rules.len());
}
}
CssRule::Import(_) => {
// @layer blocks can't be inlined into layers declared before imports.
layer_rules.clear();
}
_ => {}
}
rules.push(rule)
}
// Optimize @layer rules. Combine subsequent empty layer blocks into a single @layer statement
// so that layers are declared in the correct order.
if has_layers {
let mut declared_layers = HashSet::new();
let mut layer_statement = None;
for index in 0..rules.len() {
match &mut rules[index] {
CssRule::LayerBlock(layer) => {
if layer.minify(context, parent_is_unused)? {
if let Some(name) = &layer.name {
if declared_layers.contains(name) {
// Remove empty layer that has already been declared.
rules[index] = CssRule::Ignored;
continue;
}
let name = name.clone();
declared_layers.insert(name.clone());
if let Some(layer_index) = layer_statement {
if let CssRule::LayerStatement(layer) = &mut rules[layer_index] {
// Add name to previous layer statement rule and remove this one.
layer.names.push(name);
rules[index] = CssRule::Ignored;
}
} else {
// Create a new layer statement rule to declare the name.
rules[index] = CssRule::LayerStatement(LayerStatementRule {
names: vec![name],
loc: layer.loc,
});
layer_statement = Some(index);
}
} else {
// Remove empty anonymous layer.
rules[index] = CssRule::Ignored;
}
} else {
// Non-empty @layer block. Start a new statement.
layer_statement = None;
}
}
CssRule::Import(import) => {
if let Some(layer) = &import.layer {
// Start a new @layer statement so the import layer is in the right order.
layer_statement = None;
if let Some(name) = layer {
declared_layers.insert(name.clone());
}
}
}
_ => {}
}
}
}
self.0 = rules;
Ok(())
}
}
fn merge_style_rules<'i, T>(
style: &mut StyleRule<'i, T>,
last_style_rule: &mut StyleRule<'i, T>,
context: &mut MinifyContext<'_, 'i>,
) -> bool {
// Merge declarations if the selectors are equivalent, and both are compatible with all targets.
if style.selectors == last_style_rule.selectors
&& style.is_compatible(context.targets.current)
&& last_style_rule.is_compatible(context.targets.current)
&& style.rules.0.is_empty()
&& last_style_rule.rules.0.is_empty()
&& (!context.css_modules || style.loc.source_index == last_style_rule.loc.source_index)
{
last_style_rule
.declarations
.declarations
.extend(style.declarations.declarations.drain(..));
last_style_rule
.declarations
.important_declarations
.extend(style.declarations.important_declarations.drain(..));
last_style_rule
.declarations
.minify(context.handler, context.important_handler, &mut context.handler_context);
return true;
} else if style.declarations == last_style_rule.declarations
&& style.rules.0.is_empty()
&& last_style_rule.rules.0.is_empty()
{
// If both selectors are potentially vendor prefixable, and they are
// equivalent minus prefixes, add the prefix to the last rule.
if !style.vendor_prefix.is_empty()
&& !last_style_rule.vendor_prefix.is_empty()
&& is_equivalent(&style.selectors.0, &last_style_rule.selectors.0)
{
// If the new rule is unprefixed, replace the prefixes of the last rule.
// Otherwise, add the new prefix.
if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() {
last_style_rule.vendor_prefix = style.vendor_prefix;
} else {
last_style_rule.vendor_prefix |= style.vendor_prefix;
}
return true;
}
// Append the selectors to the last rule if the declarations are the same, and all selectors are compatible.
if style.is_compatible(context.targets.current) && last_style_rule.is_compatible(context.targets.current) {
last_style_rule.selectors.0.extend(style.selectors.0.drain(..));
if style.vendor_prefix.contains(VendorPrefix::None) && context.targets.current.should_compile_selectors() {
last_style_rule.vendor_prefix = style.vendor_prefix;
} else {
last_style_rule.vendor_prefix |= style.vendor_prefix;
}
return true;
}
}
false
}
impl<'a, 'i, T: ToCss> ToCss for CssRuleList<'i, T> {
fn to_css<W>(&self, dest: &mut Printer<W>) -> Result<(), PrinterError>
where
W: std::fmt::Write,
{
let mut first = true;
let mut last_without_block = false;
for rule in &self.0 {
if let CssRule::Ignored = &rule {
continue;
}
// Skip @import rules if collecting dependencies.
if let CssRule::Import(rule) = &rule {
if dest.remove_imports {
let dep = if dest.dependencies.is_some() {
Some(Dependency::Import(ImportDependency::new(&rule, dest.filename())))
} else {
None
};
if let Some(dependencies) = &mut dest.dependencies {
dependencies.push(dep.unwrap());
continue;
}
}
}
if first {
first = false;
} else {
if !dest.minify
&& !(last_without_block
&& matches!(
rule,
CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..)
))
{
dest.write_char('\n')?;
}
dest.newline()?;
}
rule.to_css(dest)?;
last_without_block = matches!(
rule,
CssRule::Import(..) | CssRule::Namespace(..) | CssRule::LayerStatement(..)
);
}
Ok(())
}
}
impl<'i, T> std::ops::Index<usize> for CssRuleList<'i, T> {
type Output = CssRule<'i, T>;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index]
}
}
impl<'i, T> std::ops::IndexMut<usize> for CssRuleList<'i, T> {
fn index_mut(&mut self, index: usize) -> &mut Self::Output {
&mut self.0[index]
}
}
/// A hasher that expects to be called with a single u64, which is already a hash.
#[derive(Default)]
struct PrecomputedHasher {
hash: Option<u64>,
}
impl Hasher for PrecomputedHasher {
#[inline]
fn write(&mut self, _: &[u8]) {
unreachable!()
}
#[inline]
fn write_u64(&mut self, i: u64) {
debug_assert!(self.hash.is_none());
self.hash = Some(i);
}
#[inline]
fn finish(&self) -> u64 {
self.hash.unwrap()
}
}
/// A key to a StyleRule meant for use in a HashMap for quickly detecting duplicates.
/// It stores a reference to a list and an index so it can access items without cloning
/// even when the list is reallocated. A hash is also pre-computed for fast lookups.
#[derive(Clone)]
pub(crate) struct StyleRuleKey<'a, 'i, R> {
list: &'a Vec<CssRule<'i, R>>,
index: usize,
hash: u64,
}
impl<'a, 'i, R> StyleRuleKey<'a, 'i, R> {
fn new(list: &'a Vec<CssRule<'i, R>>, index: usize) -> Self {
let rule = match &list[index] {
CssRule::Style(style) => style,
_ => unreachable!(),
};
Self {
list,
index,
hash: rule.hash_key(),
}
}
}
impl<'a, 'i, R> PartialEq for StyleRuleKey<'a, 'i, R> {
fn eq(&self, other: &Self) -> bool {
let rule = match self.list.get(self.index) {
Some(CssRule::Style(style)) => style,
_ => return false,
};
let other_rule = match other.list.get(other.index) {
Some(CssRule::Style(style)) => style,
_ => return false,
};
rule.is_duplicate(other_rule)
}
}
impl<'a, 'i, R> Eq for StyleRuleKey<'a, 'i, R> {}
impl<'a, 'i, R> std::hash::Hash for StyleRuleKey<'a, 'i, R> {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
state.write_u64(self.hash);
}
}