Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/css/context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,14 @@ impl<'a> PropertyHandlerContext<'a> {
self.rtl.push(rtl);
}

/// Whether `get_supports_rules` or `get_additional_rules` would return anything.
pub(crate) fn has_fallback_rules(&self) -> bool {
!(self.supports.is_empty()
&& self.ltr.is_empty()
&& self.rtl.is_empty()
&& self.dark.is_empty())
}

pub(crate) fn should_compile_logical(&self, feature: css::compat::Feature) -> bool {
// Don't convert logical properties in style attributes because
// our fallbacks rely on extra rules to define --ltr and --rtl.
Expand Down
6 changes: 1 addition & 5 deletions src/css/css_modules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,13 +172,9 @@ impl<'a> CssModule<'a> {
}

pub(crate) fn handle_composes(
&mut self,
_dest: &mut css::Printer,
&self,
selectors: &css::selector::parser::SelectorList,
_composes: &css::css_properties::css_modules::Composes,
_source_index: u32,
) -> css::Maybe<(), css::PrinterErrorKind> {
// let bump = dest.arena;
for sel in selectors.v.slice() {
if sel.len() == 1
&& matches!(
Expand Down
84 changes: 57 additions & 27 deletions src/css/css_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub use crate::rules::{
import::{ImportConditions, ImportRule},
layer::{LayerBlockRule, LayerName, LayerStatementRule},
namespace::NamespaceRule,
nesting::NestedDeclarationsRule,
style::StyleRule,
supports::{SupportsCondition, SupportsRule},
unknown::UnknownAtRule,
Expand Down Expand Up @@ -1071,7 +1072,14 @@ pub struct NestedRuleParser<'a, T: CustomAtRuleParser> {

impl<'a, T: CustomAtRuleParser> NestedRuleParser<'a, T> {
pub(crate) fn get_loc(&self, start: &ParserState) -> Location {
let loc = start.source_location();
self.loc_at(start.source_location())
}

pub(crate) fn current_loc(&self, input: &Parser) -> Location {
self.loc_at(input.current_source_location())
}

fn loc_at(&self, loc: SourceLocation) -> Location {
Location {
source_index: self.options.source_index,
line: loc.line,
Expand Down Expand Up @@ -1520,32 +1528,15 @@ mod rule_parsers {
&mut self,
input: &mut Parser,
) -> CssResult<CssRuleList<T::AtRule>> {
let srcloc = input.current_source_location();
let loc = Location {
source_index: self.options.source_index,
line: srcloc.line,
column: srcloc.column,
};
let loc = self.current_loc(input);

// Declarations can be immediately within @media and @supports blocks
// that are nested within a parent style rule. These act the same way
// as if they were nested within a `& { ... }` block.
// Declarations directly inside a @media, @supports, ... block nested in a style rule.
let (declarations, mut rules) = self.parse_nested(input, false)?;

if declarations.len() > 0 {
rules.v.insert(
0,
CssRule::Style(StyleRule {
// Arena-backed: this StyleRule lands in arena AST; bulk-free won't run Drop.
selectors: SelectorList::from_selector(Selector::from_component_in(
Component::Nesting,
bun_alloc::ArenaPtr::new(input.arena()),
)),
declarations,
vendor_prefix: VendorPrefix::default(),
rules: CssRuleList::default(),
loc,
}),
CssRule::NestedDeclarations(NestedDeclarationsRule { declarations, loc }),
);
}

Expand Down Expand Up @@ -2081,6 +2072,16 @@ mod rule_parsers {
let mut usage = PropertyBitset::init_empty();
let mut custom_properties: Vec<&'static [u8]> = Vec::new();
fill_property_bit_set(&mut usage, &declarations, &mut custom_properties);
// Declarations written after a nested rule are still this rule's own.
for rule in rules.v.iter() {
if let CssRule::NestedDeclarations(nested) = rule {
fill_property_bit_set(
&mut usage,
&nested.declarations,
&mut custom_properties,
);
}
}

let custom_properties_slice = custom_properties.slice();

Expand Down Expand Up @@ -2126,25 +2127,54 @@ mod rule_parsers {
type Declaration = ();

fn parse_value(this: &mut Self, name: &[u8], input: &mut Parser) -> CssResult<()> {
// Note: split-borrow — see `NestedComposesCtx` above.
// SAFETY: `input.arena()` re-borrows the parser arena through `&self`;
// detach that borrow so `input` can be re-borrowed mutably below. The
// arena outlives the parser (it owns all parsed allocations).
let arena: &Bump = unsafe { bun_ptr::detach_lifetime_ref(input.arena()) };
let arena: &'static Bump = unsafe { bun_ptr::detach_lifetime_ref(input.arena()) };

// Past a nested rule, declarations go into a `NestedDeclarationsRule` ending the list.
let mut nested = match this.rules.v.pop() {
None => None,
Some(CssRule::NestedDeclarations(nested)) => Some(nested),
Some(rule) => {
this.rules.v.push(rule);
Some(NestedDeclarationsRule {
declarations: DeclarationBlock::new_in(arena),
loc: this.current_loc(input),
})
}
};
let (declarations, important_declarations) = match &mut nested {
Some(nested) => (
&mut nested.declarations.declarations,
&mut nested.declarations.important_declarations,
),
None => (&mut this.declarations, &mut this.important_declarations),
};

// Note: split-borrow — see `NestedComposesCtx` above.
let mut ctx = NestedComposesCtx {
state: this.composes_state,
arena,
composes: &mut *this.composes,
composes_refs: &mut *this.composes_refs,
};
declaration::parse_declaration_impl(
let result = declaration::parse_declaration_impl(
name,
input,
&mut this.declarations,
&mut this.important_declarations,
declarations,
important_declarations,
this.options,
&mut ctx,
)
);

// A failed declaration is retried as a nested rule; leave no empty rule ahead of it.
if let Some(nested) = nested
&& !nested.declarations.is_empty()
{
this.rules.v.push(CssRule::NestedDeclarations(nested));
}
result
}
}

Expand Down
3 changes: 3 additions & 0 deletions src/css/printer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,8 @@ pub struct Printer<'a> {
/// they are skipped while this is set and emitted once in the final pass,
/// keeping the output linear in nesting depth instead of exponential.
pub(crate) skip_prefixed_nested_rules: bool,
/// Set by `CssRuleList::to_css` per rule: whether more output follows it within its block.
pub(crate) more_rules_follow: bool,
pub(crate) in_calc: bool,
pub(crate) css_module: Option<css::CssModule<'a>>,
pub(crate) dependencies: Option<BumpVec<'a, css::Dependency>>,
Expand Down Expand Up @@ -306,6 +308,7 @@ impl<'a> Printer<'a> {
col: 0,
vendor_prefix: css::VendorPrefix::default(),
skip_prefixed_nested_rules: false,
more_rules_follow: false,
in_calc: false,
css_module: None,
ctx: None,
Expand Down
3 changes: 1 addition & 2 deletions src/css/rules/media.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,7 @@ pub struct MediaRule<R> {
impl<R> MediaRule<R> {
pub fn to_css(&self, dest: &mut Printer) -> Result<(), PrintErr> {
if dest.minify && self.query.always_matches() {
self.rules.to_css(dest)?;
return Ok(());
return self.rules.to_css_unwrapped(dest);
}
// #[cfg(feature = "sourcemap")]
// dest.add_mapping(self.loc);
Expand Down
104 changes: 96 additions & 8 deletions src/css/rules/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ pub mod viewport;
// lifetime-free here.

// ─── CssRule variant table ────────────────────────────────────────────────
// Single source of truth for the 20 typed at-rule payloads. Adding a new
// Single source of truth for the 21 typed rule payloads. Adding a new
// at-rule = one line here; the enum variant + `to_css` arm + `deep_clone`
// arm are generated. `Unknown`/`Custom`/`Ignored` stay a fixed tail because
// their `to_css` arms are special-cased (see the note on `Custom`).
Expand Down Expand Up @@ -118,6 +118,8 @@ css_rule_variants! {
MozDocument(document::MozDocumentRule<R>),
/// A `@nest` rule.
Nesting(nesting::NestingRule<R>),
/// Declarations nested after a rule; see `NestedDeclarationsRule`.
NestedDeclarations(nesting::NestedDeclarationsRule),
/// A `@viewport` rule.
Viewport(viewport::ViewportRule),
/// A `@custom-media` rule.
Expand Down Expand Up @@ -421,10 +423,29 @@ impl<R> CssRule<R> {

impl<R> CssRuleList<R> {
pub fn to_css(&self, dest: &mut Printer) -> Result<(), PrintErr> {
self.to_css_impl(dest, false)
}

/// For an owner printed without a block of its own: whatever follows it also follows this list.
pub(crate) fn to_css_unwrapped(&self, dest: &mut Printer) -> Result<(), PrintErr> {
let more_rules_follow = dest.more_rules_follow;
self.to_css_impl(dest, more_rules_follow)
}

fn to_css_impl(
&self,
dest: &mut Printer,
more_rules_follow_list: bool,
) -> Result<(), PrintErr> {
let mut first = true;
let mut last_without_block = false;
// Counting rules skipped below as followers only costs a nested declarations rule a `;`.
let last = self
.v
.iter()
.rposition(|rule| !matches!(rule, CssRule::Ignored));

for rule in self.v.iter() {
for (i, rule) in self.v.iter().enumerate() {
if matches!(rule, CssRule::Ignored) {
continue;
}
Expand Down Expand Up @@ -476,6 +497,7 @@ impl<R> CssRuleList<R> {
}
dest.newline()?;
}
dest.more_rules_follow = more_rules_follow_list || Some(i) != last;
rule.to_css(dest)?;
last_without_block = matches!(
rule,
Expand Down Expand Up @@ -572,6 +594,17 @@ impl<R> CssRuleList<R> {
)?;
break 'arm;
}
CssRule::NestedDeclarations(decls) => {
minify_nested_declarations_arm(
decls,
&mut rules,
&mut style_rules,
&mut merge_state,
context,
parent_is_unused,
)?;
break 'arm;
}
CssRule::CounterStyle(_) => {}
CssRule::Scope(scpe) => {
// See `Container` above: recurse so nested style rules
Expand Down Expand Up @@ -762,12 +795,7 @@ fn minify_style_arm<R: for<'b> css::generics::DeepClone<'b>>(
// re-minify re-runs the staging declarations and stages their rules
// again, and the per-merge re-minify this replaces also ran before
// collection, so the re-staged entries belong in this rule's extras.
if merge_state.pending_minify
&& !(context.handler_context.supports.is_empty()
&& context.handler_context.ltr.is_empty()
&& context.handler_context.rtl.is_empty()
&& context.handler_context.dark.is_empty())
{
if merge_state.pending_minify && context.handler_context.has_fallback_rules() {
flush_pending_style_merge(rules, merge_state, context);
}

Expand Down Expand Up @@ -898,6 +926,42 @@ fn minify_style_arm<R: for<'b> css::generics::DeepClone<'b>>(
Ok(())
}

fn minify_nested_declarations_arm<R: for<'b> css::generics::DeepClone<'b>>(
decls: &mut nesting::NestedDeclarationsRule,
rules: &mut Vec<CssRule<R>>,
style_rules: &mut StyleRuleKeyMap,
merge_state: &mut StyleRuleMergeState,
context: &mut MinifyContext<'_, '_>,
parent_is_unused: bool,
) -> Result<(), MinifyErr> {
if parent_is_unused {
return Ok(());
}

let fallbacks = decls.minify(context)?;
if decls.declarations.is_empty() && fallbacks.is_empty() {
return Ok(());
}

// Same bookkeeping as for the other non-style rules in `CssRuleList::minify`.
flush_pending_style_merge(rules, merge_state, context);
merge_state.last_compat = None;
if !decls.declarations.is_empty() {
rules.push(CssRule::NestedDeclarations(
nesting::NestedDeclarationsRule {
declarations: core::mem::replace(
&mut decls.declarations,
dc::decl_block_empty_static(context.arena),
),
loc: decls.loc,
},
));
}
rules.extend(fallbacks);
style_rules.clear();
Ok(())
}

// ─── StyleRuleKey ──────────────────────────────────────────────────────────
/// A key to a `StyleRule` meant for use in a hash map for quickly detecting
/// duplicates. It stores an index into the live `rules` Vec plus a
Expand Down Expand Up @@ -1248,3 +1312,27 @@ pub struct MinifyContext<'a, 'bump> {
/// will expand to, checked against [`MAX_SELECTOR_EXPANSION`].
pub(crate) selector_expansion_total: u32,
}

impl MinifyContext<'_, '_> {
/// Charges `selectors` selectors against [`MAX_SELECTOR_EXPANSION`], failing at `loc`.
pub(crate) fn charge_selector_expansion(
&mut self,
selectors: u32,
loc: Location,
) -> Result<(), MinifyErr> {
if self.selector_expansion_multiplier > 1 {
self.selector_expansion_total = self.selector_expansion_total.saturating_add(
self.selector_expansion_multiplier
.saturating_mul(selectors.max(1)),
);
if self.selector_expansion_total > MAX_SELECTOR_EXPANSION {
self.err = Some(crate::error::MinifyError {
kind: crate::error::MinifyErrorKind::selector_expansion_limit_exceeded,
loc,
});
return Err(MinifyErr::minify_err);
}
}
Ok(())
}
}
Loading
Loading