use crate::{
model::{
AnalysisStats, FileReport, Finding, PlanEntry, Proof, RuleId, Safety, SourceRange,
WorkspaceReport, WorkspaceSummary,
},
scanner::{
NodeKind, SourceNode, count_ascii_case_insensitive_outside_comments,
count_top_level_declarations, is_whitespace_only, scan_nodes,
},
};
use anyhow::{Context, Result};
use lightningcss::stylesheet::{ParserOptions, StyleSheet};
use similar::TextDiff;
use std::{
collections::{HashMap, HashSet},
fs,
ops::Range,
path::{Path, PathBuf},
};
const SPEC_BASELINE: &str = "2026-08-17";
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub struct Specificity {
pub ids: usize,
pub classes: usize,
pub elements: usize,
}
pub fn calculate_specificity(selector: &str) -> Specificity {
let mut ids = 0;
let mut classes = 0;
let mut elements = 0;
let bytes = selector.as_bytes();
let mut i = 0;
let mut in_attr = false;
while i < bytes.len() {
let b = bytes[i];
if b == b'[' {
in_attr = true;
classes += 1;
i += 1;
continue;
}
if b == b']' {
in_attr = false;
i += 1;
continue;
}
if in_attr {
i += 1;
continue;
}
if b == b'#' {
ids += 1;
i += 1;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'-' || bytes[i] == b'_')
{
i += 1;
}
continue;
}
if b == b'.' {
classes += 1;
i += 1;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'-' || bytes[i] == b'_')
{
i += 1;
}
continue;
}
if b == b':' {
if i + 1 < bytes.len() && bytes[i + 1] == b':' {
elements += 1;
i += 2;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'-' || bytes[i] == b'_')
{
i += 1;
}
} else {
let start_name = i + 1;
let mut end_name = start_name;
while end_name < bytes.len()
&& (bytes[end_name].is_ascii_alphanumeric() || bytes[end_name] == b'-')
{
end_name += 1;
}
let pseudo_name = &selector[start_name..end_name];
if pseudo_name == "where" {
if end_name < bytes.len()
&& bytes[end_name] == b'('
&& let Some(close_p) = find_matching_paren(selector, end_name)
{
i = close_p + 1;
continue;
}
} else if pseudo_name == "is" || pseudo_name == "not" || pseudo_name == "has" {
if end_name < bytes.len()
&& bytes[end_name] == b'('
&& let Some(close_p) = find_matching_paren(selector, end_name)
{
let inner = &selector[end_name + 1..close_p];
let max_inner = split_top_level_comma(inner)
.into_iter()
.map(|s| calculate_specificity(s.trim()))
.max()
.unwrap_or_default();
ids += max_inner.ids;
classes += max_inner.classes;
elements += max_inner.elements;
i = close_p + 1;
continue;
}
classes += 1;
} else {
classes += 1;
}
i = end_name;
}
continue;
}
if (b.is_ascii_alphabetic() || b == b'*')
&& (i == 0
|| bytes[i - 1].is_ascii_whitespace()
|| bytes[i - 1] == b'>'
|| bytes[i - 1] == b'+'
|| bytes[i - 1] == b'~'
|| bytes[i - 1] == b'|')
{
if b != b'*' {
elements += 1;
}
i += 1;
while i < bytes.len()
&& (bytes[i].is_ascii_alphanumeric() || bytes[i] == b'-' || bytes[i] == b'_')
{
i += 1;
}
continue;
}
i += 1;
}
Specificity {
ids,
classes,
elements,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RelationKind {
PseudoClass,
PseudoElement,
Attribute,
Compound,
Descendant,
Combinator,
}
impl RelationKind {
fn rule(self) -> RuleId {
match self {
Self::PseudoClass => RuleId::NestPseudoClass,
Self::PseudoElement => RuleId::NestPseudoElement,
Self::Attribute => RuleId::NestAttribute,
Self::Compound => RuleId::NestCompound,
Self::Descendant => RuleId::NestDescendant,
Self::Combinator => RuleId::NestCombinator,
}
}
}
#[derive(Debug, Clone)]
enum ConditionalInner {
Direct {
body_range: Range<usize>,
},
Nested {
nested_selector: String,
body_range: Range<usize>,
},
}
#[derive(Debug, Clone)]
enum ClusterChild {
Style {
node: SourceNode,
relation: RelationKind,
nested_selector: String,
},
Conditional {
node: SourceNode,
rule: RuleId,
inners: Vec<ConditionalInner>,
},
}
impl ClusterChild {
fn node(&self) -> &SourceNode {
match self {
Self::Style { node, .. } | Self::Conditional { node, .. } => node,
}
}
fn rule(&self) -> RuleId {
match self {
Self::Style { relation, .. } => relation.rule(),
Self::Conditional { rule, .. } => *rule,
}
}
}
pub fn analyze_file(path: &Path, enabled_rules: &[RuleId]) -> Result<FileReport> {
let source = fs::read_to_string(path)
.with_context(|| format!("failed to read CSS file {}", path.display()))?;
analyze_source(path.to_path_buf(), &source, enabled_rules)
}
pub fn analyze_workspace(
root: &Path,
files: &[PathBuf],
enabled_rules: &[RuleId],
) -> Result<WorkspaceReport> {
let mut reports = Vec::with_capacity(files.len());
let mut next_id = 1usize;
for path in files {
let mut report = analyze_file(path, enabled_rules)?;
for plan in &mut report.plans {
plan.id = format!("T-{next_id:06}");
next_id += 1;
}
reports.push(report);
}
let mut summary = WorkspaceSummary {
files: reports.len(),
..WorkspaceSummary::default()
};
for report in &reports {
if !report.parse_ok {
summary.parse_errors += 1;
}
summary.rules_analyzed +=
report.stats.top_level_style_rules + report.stats.top_level_at_rules;
for plan in &report.plans {
match plan.safety {
Safety::Safe => summary.safe += 1,
Safety::Review => summary.review += 1,
Safety::Unsafe => summary.unsafe_count += 1,
Safety::Unsupported => summary.unsupported += 1,
Safety::NoOp => summary.no_op += 1,
}
if plan
.warnings
.iter()
.any(|w| w.to_ascii_lowercase().contains("specificity"))
{
summary.specificity_sensitive += 1;
}
if plan.warnings.iter().any(|w| {
w.to_ascii_lowercase().contains("cascade")
|| w.to_ascii_lowercase().contains("source order")
}) {
summary.cascade_sensitive += 1;
}
if plan
.warnings
.iter()
.any(|w| w.to_ascii_lowercase().contains("layer"))
{
summary.layer_sensitive += 1;
}
if plan
.warnings
.iter()
.any(|w| w.to_ascii_lowercase().contains("scope"))
{
summary.scope_sensitive += 1;
}
}
}
Ok(WorkspaceReport {
tool_version: env!("CARGO_PKG_VERSION").to_string(),
spec_baseline: SPEC_BASELINE.to_string(),
root: root.to_path_buf(),
enabled_rules: enabled_rules.to_vec(),
files: reports,
summary,
})
}
fn analyze_source(path: PathBuf, source: &str, enabled_rules: &[RuleId]) -> Result<FileReport> {
let parse_result = StyleSheet::parse(
source,
ParserOptions {
filename: path.display().to_string(),
error_recovery: true,
..ParserOptions::default()
},
);
let parse_error = parse_result.err().map(|err| format!("{err:?}"));
let parse_ok = parse_error.is_none();
let nodes = scan_nodes(source, 0..source.len());
let stats = collect_stats(source, &nodes, parse_ok);
let mut findings = collect_findings(source, &nodes);
if let Some(error) = &parse_error {
findings.push(Finding {
safety: Safety::Unsupported,
title: "Semantic parse failed".into(),
detail: format!(
"{error} Planning continues from the structural scanner; review all plans."
),
});
}
let mut plans = if !enabled_rules.is_empty() {
build_plans_recursive(&path, source, &nodes, enabled_rules)
} else {
Vec::new()
};
if !parse_ok {
for plan in &mut plans {
if plan.safety == Safety::Safe {
plan.safety = Safety::Review;
}
plan.warnings.push(
"Planned despite a LightningCSS parse error on this file; review required.".into(),
);
}
}
Ok(FileReport {
path,
parse_ok,
parse_error,
stats,
findings,
plans,
})
}
fn collect_stats(source: &str, nodes: &[SourceNode], parse_ok: bool) -> AnalysisStats {
let mut stats = AnalysisStats {
bytes: source.len(),
parse_errors: usize::from(!parse_ok),
important_declarations: count_ascii_case_insensitive_outside_comments(source, "!important"),
..AnalysisStats::default()
};
let mut selector_counts: HashMap<String, usize> = HashMap::new();
for node in nodes {
match &node.kind {
NodeKind::Style => {
stats.top_level_style_rules += 1;
let selector = node.prelude(source).to_string();
*selector_counts.entry(selector).or_default() += 1;
if let Some(body) = node.body(source) {
stats.declarations += count_top_level_declarations(body);
stats.custom_properties += count_custom_properties(body);
}
}
NodeKind::AtBlock { name, .. } => {
stats.top_level_at_rules += 1;
match name.as_str() {
"media" => stats.media_rules += 1,
"supports" => stats.supports_rules += 1,
"container" => stats.container_rules += 1,
"layer" => stats.layer_rules += 1,
"scope" => stats.scope_rules += 1,
"starting-style" => stats.starting_style_rules += 1,
_ => {}
}
}
NodeKind::AtStatement { .. } => stats.top_level_at_rules += 1,
}
}
stats.duplicate_selectors = selector_counts.values().filter(|&&count| count > 1).count();
stats
}
fn count_custom_properties(body: &str) -> usize {
body.lines()
.filter(|line| {
let trimmed = line.trim_start();
trimmed.starts_with("--") && trimmed.contains(':')
})
.count()
}
fn collect_findings(source: &str, nodes: &[SourceNode]) -> Vec<Finding> {
let mut findings = Vec::new();
let selectors: HashSet<String> = nodes
.iter()
.filter(|n| matches!(&n.kind, NodeKind::Style))
.map(|n| n.prelude(source).to_string())
.collect();
let mut selector_occurrences: HashMap<String, usize> = HashMap::new();
for node in nodes {
match &node.kind {
NodeKind::Style => {
let selector = node.prelude(source);
*selector_occurrences.entry(selector.to_string()).or_default() += 1;
if contains_top_level_comma(selector) {
let branches = split_top_level_comma(selector);
let specs: Vec<Specificity> = branches.iter().map(|b| calculate_specificity(b.trim())).collect();
let has_mixed = specs.windows(2).any(|w| w[0] != w[1]);
if has_mixed {
findings.push(Finding {
safety: Safety::Review,
title: "Mixed-specificity selector list detected".into(),
detail: format!("{selector}: contains branches with differing specificities; factoring into :is() or parent nesting would raise lower-specificity branches."),
});
} else {
findings.push(Finding {
safety: Safety::Review,
title: "Selector list kept flat".into(),
detail: format!("{selector}: parent selector lists require per-branch specificity proof before native nesting."),
});
}
}
if let Some(base) = bem_base_candidate(selector)
&& selectors.contains(base) {
findings.push(Finding {
safety: Safety::Unsupported,
title: "BEM token concatenation is not native nesting".into(),
detail: format!("{selector} resembles {base} + a BEM suffix; CSS nesting cannot safely generate &__element or &--modifier."),
});
}
if let Some(body) = node.body(source) {
if body.trim().is_empty() {
findings.push(Finding {
safety: Safety::Review,
title: "Empty rule block detected".into(),
detail: format!("{selector} contains no declarations or nested rules."),
});
}
let mut seen_props: HashMap<String, String> = HashMap::new();
for line in body.lines() {
let trimmed = line.trim();
if trimmed.starts_with("/*") || trimmed.starts_with('*') || !trimmed.contains(':') {
continue;
}
if let Some((prop, val)) = trimmed.split_once(':') {
let prop = prop.trim().to_ascii_lowercase();
let val = val.trim().trim_end_matches(';').trim().to_string();
if let Some(prev_val) = seen_props.get(&prop) {
if prev_val == &val {
findings.push(Finding {
safety: Safety::Review,
title: "Exact duplicate declaration detected".into(),
detail: format!("In {selector}: property '{prop}: {val}' is declared multiple times with identical value."),
});
}
} else {
seen_props.insert(prop, val);
}
}
}
if selector.contains(" .") && !selector.contains(":has(") {
findings.push(Finding {
safety: Safety::Review,
title: "Potential :has() relational candidate".into(),
detail: format!("{selector}: parent-child descendant relationship could be expressed with :has() if container-targeting is intended (advisory)."),
});
}
}
}
NodeKind::AtBlock { name, .. } => match name.as_str() {
"layer" => findings.push(Finding {
safety: Safety::Review,
title: "Cascade layer context detected".into(),
detail: "@layer participates in cascade ordering and reverses layer precedence for !important; automatic layer architecture is not applied.".into(),
}),
"scope" => findings.push(Finding {
safety: Safety::Review,
title: "Scope boundary detected".into(),
detail: "@scope boundaries enforce doughnut scoping; scoping parameters require manual architect review.".into(),
}),
"container" => findings.push(Finding {
safety: Safety::Review,
title: "Container query context detected".into(),
detail: "@container depends on eligible ancestor containers; media-to-container conversion is not inferred from CSS alone.".into(),
}),
"starting-style" => findings.push(Finding {
safety: Safety::Review,
title: "Starting-style context detected".into(),
detail: "@starting-style is temporal transition state; this build never invents it from ordinary declarations.".into(),
}),
_ => {}
},
NodeKind::AtStatement { .. } => {}
}
}
for (selector, count) in selector_occurrences {
if count > 1 {
findings.push(Finding {
safety: Safety::Review,
title: "Duplicate selector in stylesheet".into(),
detail: format!("'{selector}' appears {count} times in the stylesheet; non-adjacent occurrences must not be merged across intervening rules."),
});
}
}
findings
}
fn bem_base_candidate(selector: &str) -> Option<&str> {
if let Some(pos) = selector.find("__") {
let base = &selector[..pos];
if !base.is_empty() && !base.contains(' ') {
return Some(base);
}
}
if let Some(pos) = selector.find("--") {
let base = &selector[..pos];
if !base.is_empty() && !base.contains(' ') {
return Some(base);
}
}
None
}
const TRANSPARENT_AT_RULES: &[&str] = &["layer", "scope", "media", "supports", "container"];
fn build_plans_recursive(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled_rules: &[RuleId],
) -> Vec<PlanEntry> {
let mut plans = build_plans(path, source, nodes, enabled_rules);
for node in nodes {
if let NodeKind::AtBlock { name, .. } = &node.kind
&& TRANSPARENT_AT_RULES.contains(&name.as_str())
{
let is_covered = plans
.iter()
.any(|p| p.source_range.start <= node.start && node.end <= p.source_range.end);
if !is_covered && let Some(body_range) = &node.body_range {
let inner_nodes = scan_nodes(source, body_range.clone());
if !inner_nodes.is_empty() {
let inner_plans =
build_plans_recursive(path, source, &inner_nodes, enabled_rules);
plans.extend(inner_plans);
}
}
}
}
let keep = select_disjoint_plan_indices(&plans);
keep.into_iter().map(|i| plans[i].clone()).collect()
}
fn build_plans(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled_rules: &[RuleId],
) -> Vec<PlanEntry> {
let enabled: HashSet<RuleId> = enabled_rules.iter().copied().collect();
let mut plans = Vec::new();
plan_merge_same_named_layers(path, source, nodes, &enabled, &mut plans);
plan_merge_adjacent_at_blocks(path, source, nodes, &enabled, &mut plans);
plan_gather_consecutive_conditions_by_selector(path, source, nodes, &enabled, &mut plans);
plan_merge_adjacent_identical_selectors(path, source, nodes, &enabled, &mut plans);
plan_gather_related_selector_rules(path, source, nodes, &enabled, &mut plans);
plan_nest_layer_by_selector(path, source, nodes, &enabled, &mut plans);
plan_merge_identical_rule_bodies(path, source, nodes, &enabled, &mut plans);
plan_factor_identical_states_with_is(path, source, nodes, &enabled, &mut plans);
plan_factor_multi_selector_cluster_with_is(path, source, nodes, &enabled, &mut plans);
plan_nest_in_place_adjacent_states(path, source, nodes, &enabled, &mut plans);
let mut i = 0usize;
while i < nodes.len() {
let parent = &nodes[i];
if enabled.contains(&RuleId::ModernizeMediaRange)
&& let NodeKind::AtBlock { name, .. } = &parent.kind
&& (name == "media" || name == "container")
{
let prelude = parent.prelude(source);
if let Some(modernized) = modernize_media_query_str(prelude) {
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::ModernizeMediaRange],
safety: Safety::Safe,
source_range: SourceRange {
start: parent.prelude_range.start,
end: parent.prelude_range.end,
},
original: source[parent.prelude_range.clone()].to_string(),
proposed: modernized,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: "Modernize legacy media/container feature syntax to CSS Range Syntax (e.g. (width >= 800px)).".to_string(),
selected: true,
});
}
}
if !matches!(&parent.kind, NodeKind::Style) {
i += 1;
continue;
}
let parent_selector = parent.prelude(source);
if contains_top_level_comma(parent_selector) {
let parent_indent = line_indent(source, parent.start);
let unit = relative_indent_unit(source, parent);
if enabled.contains(&RuleId::FactorSelectorList)
&& let Some(body_range) = &parent.body_range
{
let body = &source[body_range.clone()];
if let Some(mut factored) =
factor_selector_list(parent_selector, body, &parent_indent, &unit)
{
let branches: Vec<&str> = split_top_level_comma(parent_selector)
.into_iter()
.map(|s| s.trim())
.collect();
let base = branches[0];
let mut cursor = i + 1;
let mut prev_end = parent.end;
let mut extra_children = Vec::new();
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style)
&& let Some((rel, nested_sel)) =
selector_relation(base, next.prelude(source))
&& enabled.contains(&rel.rule())
{
extra_children.push(ClusterChild::Style {
node: next.clone(),
relation: rel,
nested_selector: nested_sel,
});
prev_end = next.end;
cursor += 1;
continue;
}
break;
}
let end_offset = if extra_children.is_empty() {
parent.end
} else {
let nested_indent = format!("{parent_indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let mut extra_rendered = String::new();
for ch in &extra_children {
if let ClusterChild::Style {
node: ch_node,
nested_selector,
..
} = ch
{
extra_rendered.push('\n');
extra_rendered.push_str(&nested_indent);
extra_rendered.push_str(nested_selector.trim());
extra_rendered.push_str(" {\n");
if let Some(ch_body_range) = &ch_node.body_range {
for line in source[ch_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
extra_rendered.push_str(&inner_decl_indent);
extra_rendered.push_str(&ensure_semicolon(trimmed));
extra_rendered.push('\n');
}
}
}
extra_rendered.push_str(&nested_indent);
extra_rendered.push_str("}\n");
}
}
if let Some(close_brace_pos) = factored.rfind('}') {
factored.insert_str(close_brace_pos, &extra_rendered);
}
prev_end
};
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::FactorSelectorList],
safety: Safety::Safe,
source_range: SourceRange {
start: parent.start,
end: end_offset,
},
original: source[parent.start..end_offset].to_string(),
proposed: factored,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: "Factor comma-separated selectors sharing a common base element into nested form.".to_string(),
selected: true,
});
i = cursor;
continue;
}
}
if enabled.contains(&RuleId::ModernizeIs)
&& let Some((factored_sel, uniform)) = factor_with_is(parent_selector)
{
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::ModernizeIs],
safety: if uniform { Safety::Safe } else { Safety::Review },
source_range: SourceRange {
start: parent.prelude_range.start,
end: parent.prelude_range.end,
},
original: source[parent.prelude_range.clone()].to_string(),
proposed: factored_sel,
proof: Proof {
specificity_equivalent: uniform,
..Proof::safe_local()
},
warnings: if uniform { Vec::new() } else { vec!["Mixed branch specificity: :is() takes the specificity of its most specific argument.".into()] },
reason: "Factor common selector prefix/suffix into :is(...) grouping.".to_string(),
selected: true,
});
i += 1;
continue;
}
if enabled.contains(&RuleId::ModernizeWhere)
&& let Some(factored_where) = factor_with_where(parent_selector)
{
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::ModernizeWhere],
safety: Safety::Review,
source_range: SourceRange {
start: parent.prelude_range.start,
end: parent.prelude_range.end,
},
original: source[parent.prelude_range.clone()].to_string(),
proposed: factored_where,
proof: Proof {
specificity_equivalent: false,
..Proof::safe_local()
},
warnings: vec!["Specificity zeroed to 0-0-0 by :where()".into()],
reason: "Convert selector list to :where(...) for zero-specificity defaults (review required).".to_string(),
selected: true,
});
i += 1;
continue;
}
i += 1;
continue;
}
if parent_selector.contains("::") {
i += 1;
continue;
}
let mut children = Vec::new();
let mut cursor = i + 1;
let mut previous_end = parent.end;
while cursor < nodes.len() {
let node = &nodes[cursor];
if !is_whitespace_only(source, previous_end..node.start) {
break;
}
if matches!(&node.kind, NodeKind::Style)
&& let Some((relation, nested_selector)) =
selector_relation(parent_selector, node.prelude(source))
&& enabled.contains(&relation.rule())
{
children.push(ClusterChild::Style {
node: node.clone(),
relation,
nested_selector,
});
previous_end = node.end;
cursor += 1;
continue;
}
if let Some(child) = conditional_child(source, parent_selector, node, &enabled) {
previous_end = node.end;
children.push(child);
cursor += 1;
continue;
}
break;
}
if !children.is_empty() {
let last_end = children.last().expect("non-empty cluster").node().end;
let proposed = render_cluster(source, parent, &children);
let mut rules = Vec::new();
for child in &children {
let rule = child.rule();
if !rules.contains(&rule) {
rules.push(rule);
}
}
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules,
safety: Safety::Safe,
source_range: SourceRange {
start: parent.start,
end: last_end,
},
original: source[parent.start..last_end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"{} immediately adjacent rule(s) share the exact parent selector and can be nested without crossing comments or unrelated rules.",
children.len()
),
selected: true,
});
i = cursor;
} else {
if enabled.contains(&RuleId::ConsolidateNot) && matches!(&parent.kind, NodeKind::Style)
{
let prelude = parent.prelude(source);
if let Some((consolidated, _uniform)) = consolidate_not_in_selector(prelude) {
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::ConsolidateNot],
safety: Safety::Review,
source_range: SourceRange {
start: parent.prelude_range.start,
end: parent.prelude_range.end,
},
original: source[parent.prelude_range.clone()].to_string(),
proposed: consolidated,
proof: Proof {
specificity_equivalent: false,
..Proof::safe_local()
},
warnings: vec!["Specificity reduced: chained :not() has additive specificity; comma-separated :not() takes only the maximum argument specificity.".into()],
reason: "Consolidate chained :not() selectors into a single comma-separated :not() list (review required for specificity drop).".to_string(),
selected: true,
});
}
}
i += 1;
}
}
plans
}
fn plan_merge_same_named_layers(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::MergeSameNamedLayer) {
return;
}
let mut layer_groups: HashMap<String, Vec<&SourceNode>> = HashMap::new();
for node in nodes {
if let NodeKind::AtBlock { name, .. } = &node.kind
&& name == "layer"
{
let prelude = node.prelude(source).trim();
if let Some(layer_name) = prelude.strip_prefix("@layer") {
let layer_name = layer_name.trim();
if !layer_name.is_empty() && !layer_name.contains('{') {
layer_groups
.entry(layer_name.to_string())
.or_default()
.push(node);
}
}
}
}
let enabled_rules_vec: Vec<RuleId> = enabled.iter().copied().collect();
for (layer_name, blocks) in layer_groups {
if blocks.len() > 1 {
let first = blocks[0];
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let mut merged_body = String::new();
for b in &blocks {
if let Some(body_range) = &b.body_range {
let inner_nodes = scan_nodes(source, body_range.clone());
let inner_plans =
build_plans_recursive(path, source, &inner_nodes, &enabled_rules_vec);
let body_text = &source[body_range.clone()];
let modernized_body = if inner_plans.is_empty() {
body_text.to_string()
} else {
let mut local_plans = Vec::new();
for p in inner_plans {
if p.source_range.start >= body_range.start
&& p.source_range.end <= body_range.end
{
let mut local_p = p.clone();
local_p.source_range.start -= body_range.start;
local_p.source_range.end -= body_range.start;
local_plans.push(local_p);
}
}
apply_selected_plans(body_text, &local_plans, true)
.unwrap_or_else(|_| body_text.to_string())
};
for line in modernized_body.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
merged_body.push_str(&nested_indent);
merged_body.push_str(&ensure_semicolon(trimmed));
merged_body.push('\n');
}
}
}
}
let proposed_first =
format!("{parent_indent}@layer {layer_name} {{\n{merged_body}{parent_indent}}}");
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::MergeSameNamedLayer],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: first.end,
},
original: source[first.start..first.end].to_string(),
proposed: proposed_first,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Consolidate {} separated blocks of @layer {} into first occurrence.",
blocks.len(),
layer_name
),
selected: true,
});
for subsequent in &blocks[1..] {
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::MergeSameNamedLayer],
safety: Safety::Safe,
source_range: SourceRange {
start: subsequent.start,
end: subsequent.end,
},
original: source[subsequent.start..subsequent.end].to_string(),
proposed: String::new(),
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Remove consolidated subsequent block of @layer {}.",
layer_name
),
selected: true,
});
}
}
}
}
fn plan_merge_adjacent_at_blocks(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if let NodeKind::AtBlock { name, .. } = &first.kind {
let rule = match name.as_str() {
"media" => RuleId::MergeAdjacentMedia,
"supports" => RuleId::MergeAdjacentSupports,
"container" => RuleId::MergeAdjacentContainer,
"scope" => RuleId::MergeIdenticalScope,
"starting-style" => RuleId::MergeIdenticalStartingStyle,
_ => {
i += 1;
continue;
}
};
if !enabled.contains(&rule) {
i += 1;
continue;
}
let first_prelude = first.prelude(source).trim();
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if let NodeKind::AtBlock {
name: next_name, ..
} = &next.kind
&& next_name == name
&& next.prelude(source).trim() == first_prelude
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let mut merged_body = String::new();
for c in &cluster {
if let Some(body_range) = &c.body_range {
let body_text = &source[body_range.clone()];
for line in body_text.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
merged_body.push_str(&nested_indent);
merged_body.push_str(&ensure_semicolon(trimmed));
merged_body.push('\n');
}
}
}
}
let proposed =
format!("{parent_indent}{first_prelude} {{\n{merged_body}{parent_indent}}}");
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![rule],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Merge {} adjacent identical {} blocks into a single block.",
cluster.len(),
first_prelude
),
selected: true,
});
i = cursor;
continue;
}
}
i += 1;
}
}
fn plan_gather_consecutive_conditions_by_selector(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::NestMedia) && !enabled.contains(&RuleId::NestSupports) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if let NodeKind::AtBlock { name, .. } = &first.kind
&& (name == "media" || name == "supports")
&& let Some(target_sel) = extract_single_style_selector(source, first)
{
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if let NodeKind::AtBlock {
name: next_name, ..
} = &next.kind
&& (next_name == "media" || next_name == "supports")
&& let Some(next_sel) = extract_single_style_selector(source, next)
&& next_sel == target_sel
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let mut body_out = String::new();
for (idx, &c) in cluster.iter().enumerate() {
if idx > 0 {
body_out.push('\n');
}
let at_header = c.prelude(source).trim();
body_out.push_str(&nested_indent);
body_out.push_str(at_header);
body_out.push_str(" {\n");
let c_body_range = c.body_range.as_ref().unwrap();
let inner_nodes = scan_nodes(source, c_body_range.clone());
for in_node in &inner_nodes {
if let Some(in_body_range) = &in_node.body_range {
for line in source[in_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
body_out.push_str(&inner_decl_indent);
body_out.push_str(&ensure_semicolon(trimmed));
body_out.push('\n');
}
}
}
}
body_out.push_str(&nested_indent);
body_out.push_str("}\n");
}
let proposed =
format!("{parent_indent}{target_sel} {{\n{body_out}{parent_indent}}}");
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::NestMedia, RuleId::NestSupports],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Gather {} consecutive condition blocks targeting '{}' into a single component rule.",
cluster.len(),
target_sel
),
selected: true,
});
i = cursor;
continue;
}
}
i += 1;
}
}
fn extract_single_style_selector<'a>(source: &'a str, at_node: &SourceNode) -> Option<&'a str> {
let body_range = at_node.body_range.as_ref()?;
let inner_nodes = scan_nodes(source, body_range.clone());
if inner_nodes.len() == 1 && matches!(&inner_nodes[0].kind, NodeKind::Style) {
Some(inner_nodes[0].prelude(source).trim())
} else {
None
}
}
fn plan_nest_in_place_adjacent_states(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::NestPseudoClass) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if matches!(&first.kind, NodeKind::Style) {
let first_sel = first.prelude(source).trim();
if let Some(base) = extract_base_target(first_sel) {
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style) {
let next_sel = next.prelude(source).trim();
if let Some(next_base) = extract_base_target(next_sel)
&& next_base == base
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let mut out = format!("{parent_indent}{base} {{\n");
for (idx, &c) in cluster.iter().enumerate() {
if idx > 0 {
out.push('\n');
}
let c_sel = c.prelude(source).trim();
let remainder = &c_sel[base.len()..];
let nested_sel = if remainder.starts_with(':')
|| remainder.starts_with('[')
|| remainder.starts_with('.')
|| remainder.starts_with('#')
{
format!("&{remainder}")
} else {
remainder.trim().to_string()
};
out.push_str(&nested_indent);
out.push_str(&nested_sel);
out.push_str(" {\n");
if let Some(c_body_range) = &c.body_range {
for line in source[c_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.push_str(&inner_decl_indent);
out.push_str(&ensure_semicolon(trimmed));
out.push('\n');
}
}
}
out.push_str(&nested_indent);
out.push_str("}\n");
}
out.push_str(&parent_indent);
out.push('}');
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::NestPseudoClass, RuleId::NestAttribute],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed: out,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Nest {} adjacent state rules for '{}' in place without moving.",
cluster.len(),
base
),
selected: true,
});
i = cursor;
continue;
}
}
}
i += 1;
}
}
fn extract_base_target(selector: &str) -> Option<&str> {
if contains_top_level_comma(selector) {
return None;
}
if let Some(pos) = selector.find(':')
&& pos > 0
&& !selector[pos..].starts_with("::")
{
let base = &selector[..pos];
if !base.is_empty() {
return Some(base);
}
}
if let Some(pos) = selector.find('[')
&& pos > 0
{
let base = &selector[..pos];
if !base.is_empty() {
return Some(base);
}
}
None
}
fn parse_rule_body_items(body_str: &str) -> (Vec<String>, Vec<String>) {
let mut declarations = Vec::new();
let mut nested_rules = Vec::new();
let mut depth = 0usize;
let mut current_block = String::new();
let mut current_decl = String::new();
let mut in_comment = false;
let bytes = body_str.as_bytes();
let mut i = 0;
while i < bytes.len() {
if in_comment {
current_decl.push(bytes[i] as char);
current_block.push(bytes[i] as char);
if bytes[i] == b'*' && i + 1 < bytes.len() && bytes[i + 1] == b'/' {
current_decl.push('/');
current_block.push('/');
i += 2;
in_comment = false;
continue;
}
i += 1;
continue;
}
if bytes[i] == b'/' && i + 1 < bytes.len() && bytes[i + 1] == b'*' {
in_comment = true;
current_decl.push('/');
current_decl.push('*');
current_block.push('/');
current_block.push('*');
i += 2;
continue;
}
let b = bytes[i];
if b == b'{' {
depth += 1;
if depth == 1 {
current_block = current_decl.clone();
current_decl.clear();
}
current_block.push('{');
i += 1;
continue;
} else if b == b'}' {
if depth > 0 {
depth -= 1;
current_block.push('}');
if depth == 0 {
let trimmed = current_block.trim().to_string();
if !trimmed.is_empty() {
nested_rules.push(trimmed);
}
current_block.clear();
current_decl.clear();
}
}
i += 1;
continue;
}
if depth > 0 {
current_block.push(b as char);
} else {
if b == b';' {
current_decl.push(';');
let trimmed = current_decl.trim().to_string();
if !trimmed.is_empty() {
declarations.push(trimmed);
}
current_decl.clear();
} else if b == b'\n' {
let trimmed = current_decl.trim();
if !trimmed.is_empty()
&& trimmed.contains(':')
&& !trimmed.ends_with('{')
&& !trimmed.ends_with(',')
{
let rest = body_str[i + 1..].trim_start();
if !rest.starts_with('{') {
declarations.push(trimmed.to_string());
current_decl.clear();
} else {
current_decl.push('\n');
}
} else {
current_decl.push('\n');
}
} else {
current_decl.push(b as char);
}
}
i += 1;
}
let trailing_decl = current_decl.trim().to_string();
if !trailing_decl.is_empty() && trailing_decl.contains(':') {
declarations.push(ensure_semicolon(&trailing_decl).into_owned());
}
(declarations, nested_rules)
}
fn is_ident_continue(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '-' || c == '_' || c == '\\'
}
fn appended_nesting_selector_raw(base: &str, candidate_sel: &str) -> Option<String> {
let base = base.trim();
let candidate_sel = candidate_sel.trim();
if base.is_empty() || candidate_sel == base || contains_top_level_comma(candidate_sel) {
return None;
}
const FNS: &[&str] = &[":not(", ":is(", ":where(", ":has("];
for fn_name in FNS {
let wrapped = format!("{fn_name}{base})");
if candidate_sel == wrapped {
return Some(format!("{fn_name}&)"));
}
if let Some(prefix) = candidate_sel.strip_suffix(wrapped.as_str())
&& !prefix.is_empty()
{
let prev = prefix.chars().last()?;
if is_ident_continue(prev)
|| prev == '.'
|| prev == '#'
|| prev == ']'
|| prev == ')'
|| prev == '*'
{
return Some(format!("{prefix}{fn_name}&)"));
}
}
}
if !candidate_sel.ends_with(base) {
return None;
}
let prefix = &candidate_sel[..candidate_sel.len() - base.len()];
if prefix.is_empty() {
return None;
}
let prev = prefix.chars().last()?;
let first_of_base = base.chars().next()?;
if prev.is_whitespace() || matches!(prev, '>' | '+' | '~') {
return Some(format!("{prefix}&"));
}
if matches!(first_of_base, '.' | '#' | '[' | ':')
&& (is_ident_continue(prev) || prev == ']' || prev == ')' || prev == '*')
{
return Some(format!("{prefix}&"));
}
None
}
fn appended_nesting_selector(base: &str, candidate_sel: &str) -> Option<String> {
let nested = appended_nesting_selector_raw(base, candidate_sel)?;
selector_contains_nesting_amp(&nested).then_some(nested)
}
fn appended_selector_relation(parent: &str, child: &str) -> Option<(RelationKind, String)> {
let nested = appended_nesting_selector(parent, child)?;
let before_amp = nested.strip_suffix('&').unwrap_or(nested.as_str());
let kind = if nested.contains(":not(")
|| nested.contains(":is(")
|| nested.contains(":where(")
|| nested.contains(":has(")
{
RelationKind::PseudoClass
} else if before_amp.contains('>') || before_amp.contains('+') || before_amp.contains('~') {
RelationKind::Combinator
} else if nested.ends_with('&')
&& before_amp
.chars()
.last()
.is_some_and(|c| !c.is_whitespace())
{
RelationKind::Compound
} else {
RelationKind::Descendant
};
Some((kind, nested))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum RelatedKind {
Prefix,
Appended,
}
fn prefix_related_nested_selector(base: &str, candidate_sel: &str) -> Option<String> {
if candidate_sel == base || !candidate_sel.starts_with(base) {
return None;
}
let rem_raw = &candidate_sel[base.len()..];
let rem = rem_raw.trim_start();
if rem.is_empty() {
return None;
}
let directly_attached = !rem_raw.starts_with(|c: char| c.is_whitespace());
if rem.starts_with(':') || rem.starts_with('[') || rem.starts_with('.') || rem.starts_with('#')
{
if directly_attached {
return Some(format!("&{rem}"));
} else {
return Some(rem.to_string());
}
}
if rem.starts_with('+') || rem.starts_with('>') || rem.starts_with('~') {
let first_char = &rem[..1];
let rest = rem[1..].trim_start();
return Some(format!("{first_char} {rest}"));
}
if rem_raw.starts_with(' ') {
return Some(rem.to_string());
}
None
}
fn classify_related_nested_one(base: &str, candidate_sel: &str) -> Option<(RelatedKind, String)> {
if candidate_sel == base || contains_top_level_comma(candidate_sel) {
return None;
}
if let Some(rel) = prefix_related_nested_selector(base, candidate_sel) {
return Some((RelatedKind::Prefix, rel));
}
appended_nesting_selector(base, candidate_sel).map(|rel| (RelatedKind::Appended, rel))
}
fn classify_related_comma_list(base: &str, candidate_sel: &str) -> Option<(RelatedKind, String)> {
let parts: Vec<&str> = split_top_level_comma(candidate_sel)
.into_iter()
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if parts.len() < 2 {
return None;
}
let mut rels = Vec::with_capacity(parts.len());
let mut all_prefix = true;
for part in parts {
if part == base {
rels.push("&".to_string());
continue;
}
let (kind, rel) = classify_related_nested_one(base, part)?;
if kind != RelatedKind::Prefix {
all_prefix = false;
}
rels.push(rel);
}
let kind = if all_prefix {
RelatedKind::Prefix
} else {
RelatedKind::Appended
};
Some((kind, rels.join(", ")))
}
fn classify_related_nested(base: &str, candidate_sel: &str) -> Option<(RelatedKind, String)> {
if candidate_sel == base {
return None;
}
if contains_top_level_comma(candidate_sel) {
return classify_related_comma_list(base, candidate_sel);
}
classify_related_nested_one(base, candidate_sel)
}
fn extract_related_nested_selector(base: &str, candidate_sel: &str) -> Option<String> {
classify_related_nested(base, candidate_sel).map(|(_, rel)| rel)
}
fn is_weak_gather_base(base: &str) -> bool {
matches!(base.trim(), "*" | "html" | "body" | ":root" | ":host")
}
fn selector_contains_nesting_amp(selector: &str) -> bool {
let bytes = selector.as_bytes();
let mut i = 0;
let mut quote: Option<u8> = None;
let mut escaped = false;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'\'' | b'"' => quote = Some(b),
b'&' => return true,
_ => {}
}
i += 1;
}
false
}
fn leading_block_comment(source: &str, node_start: usize) -> Option<(usize, &str)> {
let before = source.get(..node_start)?;
let start = before.rfind("/*")?;
let close_rel = source.get(start + 2..node_start)?.find("*/")?;
let end = start + 2 + close_rel + 2;
if !source[end..node_start]
.bytes()
.all(|b| b.is_ascii_whitespace())
{
return None;
}
Some((start, source[start..end].trim_end()))
}
fn is_simple_compound_selector(sel: &str) -> bool {
let sel = sel.trim();
if sel.is_empty()
|| sel.starts_with('&')
|| sel.starts_with('+')
|| sel.starts_with('>')
|| sel.starts_with('~')
|| contains_top_level_comma(sel)
{
return false;
}
let mut paren = 0usize;
let mut brack = 0usize;
for c in sel.chars() {
match c {
'(' => paren += 1,
')' => paren = paren.saturating_sub(1),
'[' => brack += 1,
']' => brack = brack.saturating_sub(1),
_ if paren == 0
&& brack == 0
&& (c.is_whitespace() || matches!(c, '+' | '>' | '~')) =>
{
return false;
}
_ => {}
}
}
true
}
fn first_compound_stripped(sel: &str) -> Option<&str> {
let sel = sel.trim();
if sel.is_empty() || sel.starts_with('&') {
return None;
}
let mut paren = 0usize;
let mut brack = 0usize;
let mut end = sel.len();
for (i, c) in sel.char_indices() {
match c {
'(' => paren += 1,
')' => paren = paren.saturating_sub(1),
'[' => brack += 1,
']' => brack = brack.saturating_sub(1),
_ if paren == 0
&& brack == 0
&& (c.is_whitespace() || matches!(c, '+' | '>' | '~' | ',')) =>
{
end = i;
break;
}
_ => {}
}
}
let head = sel[..end].trim();
if head.is_empty() || head.starts_with(':') || head.starts_with('[') {
return None;
}
paren = 0;
brack = 0;
for (i, c) in head.char_indices() {
match c {
'(' => paren += 1,
')' => paren = paren.saturating_sub(1),
'[' if paren == 0 && i > 0 => return Some(&head[..i]),
':' if paren == 0 && brack == 0 && i > 0 => return Some(&head[..i]),
_ => {}
}
}
Some(head)
}
fn style_body_weight(source: &str, node: &SourceNode) -> usize {
node.body(source)
.map(|b| b.lines().filter(|l| !l.trim().is_empty()).count())
.unwrap_or(0)
}
fn assign_gather_home<'a>(
sel: &str,
exact_homes: &[&'a str],
home_weight: &HashMap<&'a str, usize>,
) -> Option<&'a str> {
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
struct Rank {
spec: Specificity,
is_prefix: bool,
weight: usize,
len: usize,
}
let mut best: Option<(Rank, &'a str)> = None;
let consider =
|best: &mut Option<(Rank, &'a str)>, home: &'a str, kind: Option<RelatedKind>| {
let is_prefix = matches!(kind, None | Some(RelatedKind::Prefix));
if matches!(kind, Some(RelatedKind::Appended)) && is_weak_gather_base(home) {
return;
}
let rank = Rank {
spec: calculate_specificity(home),
is_prefix,
weight: home_weight.get(home).copied().unwrap_or(0),
len: home.len(),
};
if best.as_ref().is_none_or(|(cur, _)| rank > *cur) {
*best = Some((rank, home));
}
};
for &home in exact_homes {
if sel == home || home_weight.get(home).copied().unwrap_or(0) == 0 {
continue;
}
if let Some((kind, _)) = classify_related_nested(home, sel) {
consider(&mut best, home, Some(kind));
}
}
if best.is_some() {
return best.map(|(_, home)| home);
}
for &home in exact_homes {
if sel == home || home_weight.get(home).copied().unwrap_or(0) != 0 {
continue;
}
if let Some((kind, _)) = classify_related_nested(home, sel) {
consider(&mut best, home, Some(kind));
}
}
best.map(|(_, home)| home)
}
const GATHERABLE_CONDITIONALS: &[&str] = &["media", "supports", "container", "starting-style"];
const GATHER_SCAN_CONTAINERS: &[&str] = &[];
fn is_strong_gather_home(sel: &str) -> bool {
let sel = sel.trim();
sel.starts_with('.')
|| sel.starts_with('#')
|| sel.starts_with('[')
|| (sel.starts_with(':') && !sel.starts_with("::"))
}
fn is_absorbable_descendant(base: &str, sel: &str) -> bool {
let sel = sel.trim();
if sel.is_empty() || sel == base {
return false;
}
if contains_top_level_comma(sel) {
return split_top_level_comma(sel)
.into_iter()
.map(str::trim)
.filter(|s| !s.is_empty())
.all(|part| is_absorbable_descendant(base, part));
}
if extract_related_nested_selector(base, sel).is_some() {
return true;
}
let head = first_compound_stripped(sel).unwrap_or(sel);
if is_weak_gather_base(head) {
return false;
}
if is_strong_gather_home(head) && head != first_compound_stripped(base).unwrap_or(base) {
return false;
}
is_simple_compound_selector(sel) || first_compound_stripped(sel).is_some()
}
fn can_nest_under_gather_home(base: &str, sel: &str) -> bool {
sel == base
|| extract_related_nested_selector(base, sel).is_some()
|| is_absorbable_descendant(base, sel)
}
enum GatherMember {
Style(SourceNode),
Conditional {
at_node: SourceNode,
inner: SourceNode,
delete_whole_at_block: bool,
},
}
impl GatherMember {
fn outer_start(&self) -> usize {
match self {
Self::Style(n) => n.start,
Self::Conditional { at_node, .. } => at_node.start,
}
}
fn outer_end(&self) -> usize {
match self {
Self::Style(n) => n.end,
Self::Conditional { at_node, .. } => at_node.end,
}
}
}
fn extend_with_trailing_newline(source: &str, end: usize) -> usize {
if source[end..].starts_with("\r\n") {
end + 2
} else if source[end..].starts_with('\n') {
end + 1
} else {
end
}
}
fn push_relative_body_as_nest(rel_sel: &str, body_str: &str, all_nested_rules: &mut Vec<String>) {
let (decls, nested) = parse_rule_body_items(body_str);
if nested.is_empty() {
let mut rel_body = String::new();
for d in &decls {
rel_body.push_str(&format!("{}\n", ensure_semicolon(d)));
}
all_nested_rules.push(format!("{rel_sel} {{\n {rel_body}}}"));
} else {
let mut rel_body_lines = Vec::new();
for d in &decls {
rel_body_lines.push(format!(" {}", ensure_semicolon(d)));
}
for nr in &nested {
rel_body_lines.push(nr.clone());
}
let rel_body = rel_body_lines.join("\n");
all_nested_rules.push(format!("{rel_sel} {{\n{rel_body}\n}}"));
}
}
fn push_style_member_into_merge(
first_sel: &str,
cand_sel: &str,
body_str: &str,
all_decls: &mut Vec<String>,
all_nested_rules: &mut Vec<String>,
) {
if cand_sel == first_sel {
let (decls, nested) = parse_rule_body_items(body_str);
all_decls.extend(decls);
all_nested_rules.extend(nested);
} else if let Some(rel_sel) = extract_related_nested_selector(first_sel, cand_sel) {
push_relative_body_as_nest(&rel_sel, body_str, all_nested_rules);
} else if is_absorbable_descendant(first_sel, cand_sel) {
push_relative_body_as_nest(cand_sel, body_str, all_nested_rules);
}
}
fn format_conditional_group_into_lines(
first_sel: &str,
at_header: &str,
inners: &[&SourceNode],
source: &str,
nested_indent: &str,
unit: &str,
) -> Vec<String> {
let level2 = format!("{nested_indent}{unit}");
let mut direct_decls = Vec::new();
let mut nested = Vec::new();
for inner in inners {
let Some(body_range) = &inner.body_range else {
continue;
};
let inner_sel = inner.prelude(source).trim();
let inner_body = &source[body_range.clone()];
if inner_sel == first_sel {
let (decls, nrs) = parse_rule_body_items(inner_body);
direct_decls.extend(decls);
nested.extend(nrs);
} else if let Some(rel_sel) = extract_related_nested_selector(first_sel, inner_sel) {
push_relative_body_as_nest(&rel_sel, inner_body, &mut nested);
}
}
let nested = factor_related_nested_rules(merge_same_prelude_nests(nested));
let mut lines = Vec::new();
lines.push(format!("{nested_indent}{at_header} {{"));
for d in &direct_decls {
lines.push(format!("{level2}{}", ensure_semicolon(d)));
}
for nr in &nested {
if let Some(sel) = nested_rule_prelude(nr).map(str::to_string) {
let item = NestedRuleItem {
comment: nested_rule_comment_prefix(nr),
sel,
body: nested_rule_inner(nr),
};
for line in render_item_indented(&item, &level2, unit).lines() {
lines.push(line.to_string());
}
} else {
for line in nr.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
lines.push(format!("{level2}{}", ensure_semicolon(trimmed)));
}
}
}
}
lines.push(format!("{nested_indent}}}"));
lines
}
fn format_conditional_into_lines(
first_sel: &str,
at_header: &str,
inner_sel: &str,
inner_body: &str,
nested_indent: &str,
unit: &str,
) -> Vec<String> {
if inner_sel != first_sel {
return Vec::new();
}
let (decls, nested) = parse_rule_body_items(inner_body);
let level2 = format!("{nested_indent}{unit}");
let mut lines = Vec::new();
lines.push(format!("{nested_indent}{at_header} {{"));
for d in &decls {
lines.push(format!("{level2}{}", ensure_semicolon(d)));
}
for nr in &nested {
if let Some(sel) = nested_rule_prelude(nr).map(str::to_string) {
let item = NestedRuleItem {
comment: nested_rule_comment_prefix(nr),
sel,
body: nested_rule_inner(nr),
};
for line in render_item_indented(&item, &level2, unit).lines() {
lines.push(line.to_string());
}
} else {
for line in nr.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
lines.push(String::new());
} else {
lines.push(format!("{level2}{}", ensure_semicolon(trimmed)));
}
}
}
}
lines.push(format!("{nested_indent}}}"));
lines
}
fn nested_rule_prelude(nr: &str) -> Option<&str> {
let mut i = 0usize;
while i < nr.len() {
while i < nr.len() && nr.as_bytes()[i].is_ascii_whitespace() {
i += 1;
}
if nr[i..].starts_with("/*") {
i += nr[i..].find("*/")? + 2;
continue;
}
if nr[i..].starts_with("//") {
i += nr[i..].find('\n').map(|n| n + 1).unwrap_or(nr.len() - i);
continue;
}
break;
}
let rest = nr.get(i..)?;
let brace = rest.find('{')?;
let head = rest[..brace].trim();
if head.is_empty() { None } else { Some(head) }
}
fn nested_rule_inner(nr: &str) -> String {
let lines: Vec<&str> = nr.lines().collect();
let open = lines.iter().position(|line| {
let t = line.trim();
t.ends_with('{') && !t.starts_with("/*") && !t.starts_with("//")
});
let Some(open) = open else {
return String::new();
};
if lines.len() <= open + 2 {
return String::new();
}
lines[open + 1..lines.len() - 1]
.iter()
.map(|l| l.trim_end())
.collect::<Vec<_>>()
.join("\n")
}
fn nested_rule_comment_prefix(nr: &str) -> String {
let mut out = String::new();
for line in nr.lines() {
let t = line.trim();
if t.is_empty() || t.starts_with("/*") || t.starts_with("//") {
if !out.is_empty() {
out.push('\n');
}
out.push_str(t);
} else {
break;
}
}
out
}
fn merge_same_prelude_nests(rules: Vec<String>) -> Vec<String> {
let mut order: Vec<String> = Vec::new();
let mut merged: HashMap<String, String> = HashMap::new();
let mut comments: HashMap<String, String> = HashMap::new();
let mut leftovers = Vec::new();
for nr in rules {
let Some(prelude) = nested_rule_prelude(&nr).map(str::to_string) else {
leftovers.push(nr);
continue;
};
let inner = nested_rule_inner(&nr);
let prefix = nested_rule_comment_prefix(&nr);
if let Some(existing) = merged.get_mut(&prelude) {
if !existing.is_empty() && !inner.is_empty() {
existing.push('\n');
}
existing.push_str(&inner);
} else {
order.push(prelude.clone());
merged.insert(prelude.clone(), inner);
if !prefix.is_empty() {
comments.insert(prelude, prefix);
}
}
}
let mut out: Vec<String> = order
.into_iter()
.map(|prelude| {
let inner = merged.remove(&prelude).unwrap_or_default();
let comment = comments.remove(&prelude).unwrap_or_default();
let head = if comment.is_empty() {
String::new()
} else {
format!("{comment}\n")
};
if inner.is_empty() {
format!("{head}{prelude} {{}}")
} else {
format!("{head}{prelude} {{\n{inner}\n}}")
}
})
.collect();
out.extend(leftovers);
out
}
fn conditional_as_nested_rule(
first_sel: &str,
at_header: &str,
inner_sel: &str,
inner_body: &str,
) -> Option<String> {
if inner_sel == first_sel {
return None;
}
let rel_sel = extract_related_nested_selector(first_sel, inner_sel)?;
let (decls, nested) = parse_rule_body_items(inner_body);
let mut at_inner = String::new();
for d in &decls {
at_inner.push_str(" ");
at_inner.push_str(&ensure_semicolon(d));
at_inner.push('\n');
}
for nr in &nested {
for line in nr.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
at_inner.push_str(" ");
at_inner.push_str(&ensure_semicolon(trimmed));
at_inner.push('\n');
}
}
}
Some(format!(
"{rel_sel} {{\n {at_header} {{\n{at_inner} }}\n}}"
))
}
#[derive(Debug, Clone)]
struct NestedRuleItem {
comment: String,
sel: String,
body: String,
}
fn render_nested_rule_item(item: &NestedRuleItem) -> String {
let head = if item.comment.is_empty() {
String::new()
} else {
format!("{}\n", item.comment)
};
if item.body.trim().is_empty() {
format!("{head}{} {{}}", item.sel)
} else {
format!("{head}{} {{\n{}\n}}", item.sel, item.body)
}
}
fn append_child_rule(parent: &mut NestedRuleItem, rel_sel: &str, child_body: &str) {
let child = NestedRuleItem {
comment: String::new(),
sel: rel_sel.to_string(),
body: child_body.to_string(),
};
if !parent.body.is_empty() {
parent.body.push('\n');
}
parent.body.push_str(&render_nested_rule_item(&child));
}
fn split_relative_combinator(sel: &str) -> Option<(String, String)> {
let mut paren = 0usize;
let mut brack = 0usize;
for (i, c) in sel.char_indices() {
match c {
'(' => paren += 1,
')' => paren = paren.saturating_sub(1),
'[' => brack += 1,
']' => brack = brack.saturating_sub(1),
_ if paren == 0 && brack == 0 => {
if c.is_whitespace() {
let right = sel[i..].trim_start();
let left = sel[..i].trim();
if left.is_empty() || right.is_empty() {
return None;
}
return Some((left.to_string(), right.to_string()));
}
if matches!(c, '+' | '>' | '~') && i > 0 {
let right = sel[i + c.len_utf8()..].trim_start();
let left = sel[..i].trim();
if left.is_empty() || right.is_empty() {
return None;
}
return Some((left.to_string(), format!("{c} {right}")));
}
}
_ => {}
}
}
None
}
fn is_at_rule_prelude(sel: &str) -> bool {
sel.trim_start().starts_with('@')
}
fn synthesizable_parent(sel: &str) -> Option<(String, String)> {
let sel = sel.trim();
if sel.is_empty() || sel == "&" || is_at_rule_prelude(sel) {
return None;
}
if contains_top_level_comma(sel) {
let parts: Vec<&str> = split_top_level_comma(sel)
.into_iter()
.map(str::trim)
.filter(|s| !s.is_empty())
.collect();
if parts.len() < 2 {
return None;
}
let mut parent: Option<String> = None;
let mut children = Vec::new();
for part in parts {
let (p, c) = synthesizable_parent(part)?;
match &parent {
Some(existing) if existing != &p => return None,
None => parent = Some(p),
_ => {}
}
children.push(c);
}
return Some((parent?, children.join(", ")));
}
split_relative_combinator(sel)
}
fn nest_items_under_existing(items: Vec<NestedRuleItem>) -> Vec<NestedRuleItem> {
let n = items.len();
if n < 2 {
return items;
}
let mut parent_of: Vec<Option<usize>> = vec![None; n];
for i in 0..n {
let mut best: Option<(usize, usize)> = None;
for j in 0..n {
if i == j {
continue;
}
if matches!(
classify_related_nested(&items[j].sel, &items[i].sel),
Some((RelatedKind::Prefix, _))
) {
let len = items[j].sel.len();
if best.is_none_or(|(_, l)| len >= l) {
best = Some((j, len));
}
}
}
if let Some((j, _)) = best {
parent_of[i] = Some(j);
}
}
for i in 0..n {
if let Some(j) = parent_of[i]
&& parent_of[j].is_some()
{
parent_of[i] = None;
}
}
let mut out = Vec::new();
let mut out_idx = vec![None; n];
for i in 0..n {
if parent_of[i].is_none() {
out_idx[i] = Some(out.len());
out.push(items[i].clone());
}
}
for i in 0..n {
if let Some(j) = parent_of[i]
&& let Some(out_j) = out_idx[j]
&& let Some((RelatedKind::Prefix, rel)) =
classify_related_nested(&out[out_j].sel, &items[i].sel)
{
append_child_rule(&mut out[out_j], &rel, &items[i].body);
}
}
out
}
fn wrap_shared_virtual_parents(items: Vec<NestedRuleItem>) -> Vec<NestedRuleItem> {
#[derive(Clone)]
enum Bucket {
Atomic(NestedRuleItem),
Shared {
parent: String,
originals: Vec<NestedRuleItem>,
children: Vec<NestedRuleItem>,
},
}
let mut buckets: Vec<Bucket> = Vec::new();
let mut shared_at: HashMap<String, usize> = HashMap::new();
for item in items {
match synthesizable_parent(&item.sel) {
Some((parent, child)) => {
if let Some(&idx) = shared_at.get(&parent) {
if let Bucket::Shared {
originals,
children,
..
} = &mut buckets[idx]
{
originals.push(item.clone());
children.push(NestedRuleItem {
comment: item.comment.clone(),
sel: child,
body: item.body,
});
}
} else {
shared_at.insert(parent.clone(), buckets.len());
buckets.push(Bucket::Shared {
parent,
originals: vec![item.clone()],
children: vec![NestedRuleItem {
comment: item.comment.clone(),
sel: child,
body: item.body,
}],
});
}
}
None => buckets.push(Bucket::Atomic(item)),
}
}
let mut out = Vec::new();
for bucket in buckets {
match bucket {
Bucket::Atomic(item) => out.push(item),
Bucket::Shared {
parent,
originals,
children,
} => {
if children.len() >= 2 {
let body = children
.iter()
.map(render_nested_rule_item)
.collect::<Vec<_>>()
.join("\n");
out.push(NestedRuleItem {
comment: String::new(),
sel: parent,
body,
});
} else {
out.extend(originals);
}
}
}
}
out
}
fn factor_related_nested_rules(rules: Vec<String>) -> Vec<String> {
let mut items = Vec::new();
let mut leftovers = Vec::new();
for nr in rules {
if let Some(sel) = nested_rule_prelude(&nr).map(str::to_string) {
items.push(NestedRuleItem {
comment: nested_rule_comment_prefix(&nr),
sel,
body: nested_rule_inner(&nr),
});
} else {
leftovers.push(nr);
}
}
for _ in 0..8 {
let before = items.len();
items = nest_items_under_existing(items);
items = wrap_shared_virtual_parents(items);
if items.len() == before {
break;
}
}
let mut out: Vec<String> = items.iter().map(render_nested_rule_item).collect();
out.extend(leftovers);
out
}
fn line_start(source: &str, pos: usize) -> usize {
source[..pos.min(source.len())]
.rfind('\n')
.map(|i| i + 1)
.unwrap_or(0)
}
fn relative_indent_unit(source: &str, node: &SourceNode) -> String {
let parent = line_indent(source, node.start);
let Some(body) = node.body_range.clone() else {
return " ".to_string();
};
match detect_indent_unit(source, body) {
Some(raw) => raw
.strip_prefix(parent.as_str())
.filter(|rest| !rest.is_empty())
.unwrap_or(" ")
.to_string(),
None => " ".to_string(),
}
}
fn squeeze_excess_blank_lines(s: &str) -> String {
let ends_nl = s.ends_with('\n');
let mut out = String::new();
let mut blank_run = 0usize;
for line in s.lines() {
if line.trim().is_empty() {
blank_run += 1;
if blank_run > 1 {
continue;
}
out.push('\n');
} else {
blank_run = 0;
out.push_str(line);
out.push('\n');
}
}
if !ends_nl && out.ends_with('\n') {
out.pop();
}
out
}
fn render_item_indented(item: &NestedRuleItem, indent: &str, unit: &str) -> String {
let mut out = String::new();
if !item.comment.is_empty() {
for line in item.comment.lines() {
out.push_str(indent);
out.push_str(line.trim());
out.push('\n');
}
}
out.push_str(indent);
out.push_str(&item.sel);
out.push_str(" {\n");
let inner = format!("{indent}{unit}");
let (decls, nested) = parse_rule_body_items(&item.body);
for d in decls {
out.push_str(&inner);
out.push_str(&ensure_semicolon(&d));
out.push('\n');
}
for nr in nested {
if let Some(sel) = nested_rule_prelude(&nr).map(str::to_string) {
let child = NestedRuleItem {
comment: nested_rule_comment_prefix(&nr),
sel,
body: nested_rule_inner(&nr),
};
out.push_str(&render_item_indented(&child, &inner, unit));
out.push('\n');
}
}
out.push_str(indent);
out.push('}');
out
}
fn leftover_style_replacements(
source: &str,
nodes: &[SourceNode],
span_start: usize,
span_end: usize,
cluster_starts: &HashSet<usize>,
) -> Vec<(usize, usize, String)> {
let leftovers: Vec<&SourceNode> = nodes
.iter()
.filter(|n| {
matches!(n.kind, NodeKind::Style)
&& n.start >= span_start
&& n.end <= span_end
&& !cluster_starts.contains(&n.start)
})
.collect();
let mut reps = Vec::new();
let mut i = 0;
while i < leftovers.len() {
let parent = leftovers[i];
let parent_sel = parent.prelude(source);
if contains_top_level_comma(parent_sel) || parent_sel.contains("::") {
i += 1;
continue;
}
let mut children = Vec::new();
let mut j = i + 1;
let mut prev_end = parent.end;
while j < leftovers.len() {
let next = leftovers[j];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if let Some((relation, nested_selector)) =
selector_relation(parent_sel, next.prelude(source))
{
children.push(ClusterChild::Style {
node: (*next).clone(),
relation,
nested_selector,
});
prev_end = next.end;
j += 1;
continue;
}
break;
}
if !children.is_empty() {
let last_end = children.last().expect("non-empty").node().end;
reps.push((
parent.start,
last_end,
render_cluster(source, parent, &children),
));
i = j;
} else {
i += 1;
}
}
reps
}
fn apply_range_replacements(
source: &str,
start: usize,
end: usize,
replacements: &[(usize, usize, String)],
) -> String {
let mut reps: Vec<(usize, usize, &str)> = replacements
.iter()
.map(|(s, e, t)| (*s, *e, t.as_str()))
.filter(|(s, e, _)| *s >= start && *e <= end && *s <= *e)
.collect();
reps.sort_by(|a, b| a.0.cmp(&b.0).then_with(|| b.1.cmp(&a.1)));
let mut out = String::new();
let mut cur = start;
for (s, e, text) in reps {
if s < cur {
continue;
}
if s > cur {
out.push_str(&source[cur..s]);
}
out.push_str(text);
cur = e;
}
if cur < end {
out.push_str(&source[cur..end]);
}
out
}
fn is_gather_delete(plan: &PlanEntry) -> bool {
plan.proposed.is_empty() && plan.rules.contains(&RuleId::GatherRelatedSelectorRules)
}
fn gather_target_key(plan: &PlanEntry) -> Option<String> {
if !plan.rules.contains(&RuleId::GatherRelatedSelectorRules) {
return None;
}
let start = plan.reason.find("for '")? + 5;
let rest = plan.reason.get(start..)?;
let end = rest.find('\'')?;
Some(rest[..end].to_string())
}
fn ranges_overlap(a: &SourceRange, b: &SourceRange) -> bool {
a.start < b.end && b.start < a.end
}
fn select_disjoint_plan_indices(plans: &[PlanEntry]) -> Vec<usize> {
let mut primary: Vec<usize> = (0..plans.len())
.filter(|&i| !is_gather_delete(&plans[i]))
.collect();
primary.sort_by(|&i, &j| {
plans[i]
.source_range
.start
.cmp(&plans[j].source_range.start)
.then_with(|| plans[j].source_range.end.cmp(&plans[i].source_range.end))
});
let mut kept = Vec::new();
let mut last_end = 0usize;
for i in primary {
if plans[i].source_range.start >= last_end {
last_end = plans[i].source_range.end;
kept.push(i);
}
}
let mut kept_gather_keys = HashSet::new();
for &i in &kept {
if !plans[i].proposed.is_empty()
&& let Some(key) = gather_target_key(&plans[i])
{
kept_gather_keys.insert((plans[i].file.clone(), key));
}
}
let mut deletes: Vec<usize> = (0..plans.len())
.filter(|&i| is_gather_delete(&plans[i]))
.collect();
deletes.sort_by(|&i, &j| {
plans[i]
.source_range
.start
.cmp(&plans[j].source_range.start)
});
for i in deletes {
let Some(key) = gather_target_key(&plans[i]) else {
continue;
};
if !kept_gather_keys.contains(&(plans[i].file.clone(), key)) {
continue;
}
if kept
.iter()
.any(|&k| ranges_overlap(&plans[k].source_range, &plans[i].source_range))
{
continue;
}
kept.push(i);
}
kept.sort_by(|&i, &j| {
plans[i]
.source_range
.start
.cmp(&plans[j].source_range.start)
.then_with(|| plans[j].source_range.end.cmp(&plans[i].source_range.end))
});
kept
}
fn format_merged_rule(
first_sel: &str,
parent_indent: &str,
unit: &str,
cluster: &[GatherMember],
source: &str,
) -> String {
let nested_indent = format!("{parent_indent}{unit}");
let mut all_decls = Vec::new();
let mut all_nested_rules = Vec::new();
let mut absorbed_nests = Vec::new();
let mut conditional_lines = Vec::new();
for member in cluster {
let GatherMember::Style(c) = member else {
continue;
};
if let Some(body_range) = &c.body_range {
let cand_sel = c.prelude(source).trim();
let body_str = &source[body_range.clone()];
let before_len = all_nested_rules.len();
push_style_member_into_merge(
first_sel,
cand_sel,
body_str,
&mut all_decls,
&mut all_nested_rules,
);
if all_nested_rules.len() > before_len
&& let Some((_, cmt)) = leading_block_comment(source, c.start)
&& let Some(last) = all_nested_rules.last_mut()
{
*last = format!("{cmt}\n{last}");
}
}
}
let mut cond_order: Vec<usize> = Vec::new();
let mut cond_groups: HashMap<usize, (&SourceNode, Vec<&SourceNode>)> = HashMap::new();
for member in cluster {
let GatherMember::Conditional { at_node, inner, .. } = member else {
continue;
};
cond_groups
.entry(at_node.start)
.and_modify(|(_, inners)| inners.push(inner))
.or_insert_with(|| {
cond_order.push(at_node.start);
(at_node, vec![inner])
});
}
for key in cond_order {
let Some((at_node, inners)) = cond_groups.remove(&key) else {
continue;
};
let at_header = at_node.prelude(source).trim();
let mut related = Vec::new();
let mut absorbed = Vec::new();
for inner in inners {
let sel = inner.prelude(source).trim();
if sel == first_sel || extract_related_nested_selector(first_sel, sel).is_some() {
related.push(inner);
} else {
absorbed.push(inner);
}
}
if !related.is_empty() {
let invert_group = related.len() >= 2
|| related
.iter()
.any(|inner| inner.prelude(source).trim() == first_sel);
if invert_group {
let extra = format_conditional_group_into_lines(
first_sel,
at_header,
&related,
source,
&nested_indent,
unit,
);
if !conditional_lines.is_empty() && !extra.is_empty() {
conditional_lines.push(String::new());
}
conditional_lines.extend(extra);
} else {
for inner in &related {
if let Some(body_range) = &inner.body_range {
let inner_sel = inner.prelude(source).trim();
let inner_body = &source[body_range.clone()];
if let Some(nr) =
conditional_as_nested_rule(first_sel, at_header, inner_sel, inner_body)
{
all_nested_rules.push(nr);
} else {
let extra = format_conditional_into_lines(
first_sel,
at_header,
inner_sel,
inner_body,
&nested_indent,
unit,
);
if !conditional_lines.is_empty() && !extra.is_empty() {
conditional_lines.push(String::new());
}
conditional_lines.extend(extra);
}
}
}
}
}
for inner in absorbed {
if let Some(body_range) = &inner.body_range {
push_style_member_into_merge(
first_sel,
inner.prelude(source).trim(),
&source[body_range.clone()],
&mut all_decls,
&mut absorbed_nests,
);
}
}
}
let all_nested_rules = factor_related_nested_rules(merge_same_prelude_nests(all_nested_rules));
let absorbed_nests = factor_related_nested_rules(merge_same_prelude_nests(absorbed_nests));
let mut body_lines = Vec::new();
for d in &all_decls {
body_lines.push(format!("{nested_indent}{}", ensure_semicolon(d)));
}
if !all_decls.is_empty() && !all_nested_rules.is_empty() {
body_lines.push(String::new());
}
for (idx, nr) in all_nested_rules.iter().enumerate() {
if let Some(sel) = nested_rule_prelude(nr).map(str::to_string) {
let item = NestedRuleItem {
comment: nested_rule_comment_prefix(nr),
sel,
body: nested_rule_inner(nr),
};
body_lines.push(render_item_indented(&item, &nested_indent, unit));
} else {
for line in nr.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
body_lines.push(String::new());
} else {
body_lines.push(format!("{nested_indent}{}", ensure_semicolon(trimmed)));
}
}
}
if idx < all_nested_rules.len() - 1 {
body_lines.push(String::new());
}
}
if !conditional_lines.is_empty() {
if !body_lines.is_empty() {
body_lines.push(String::new());
}
body_lines.extend(conditional_lines);
}
if !absorbed_nests.is_empty() {
if !body_lines.is_empty() {
body_lines.push(String::new());
}
for (idx, nr) in absorbed_nests.iter().enumerate() {
if let Some(sel) = nested_rule_prelude(nr).map(str::to_string) {
let item = NestedRuleItem {
comment: nested_rule_comment_prefix(nr),
sel,
body: nested_rule_inner(nr),
};
body_lines.push(render_item_indented(&item, &nested_indent, unit));
} else {
for line in nr.lines() {
let trimmed = line.trim();
if trimmed.is_empty() {
body_lines.push(String::new());
} else {
body_lines.push(format!("{nested_indent}{}", ensure_semicolon(trimmed)));
}
}
}
if idx < absorbed_nests.len() - 1 {
body_lines.push(String::new());
}
}
}
let body_content = body_lines.join("\n");
format!("{parent_indent}{first_sel} {{\n{body_content}\n{parent_indent}}}")
}
fn plan_merge_adjacent_identical_selectors(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::MergeAdjacentIdenticalSelector) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if matches!(&first.kind, NodeKind::Style) {
let first_sel = first.prelude(source).trim();
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style) && next.prelude(source).trim() == first_sel
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let members: Vec<GatherMember> = cluster
.iter()
.map(|n| GatherMember::Style((*n).clone()))
.collect();
let proposed =
format_merged_rule(first_sel, &parent_indent, &unit, &members, source);
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::MergeAdjacentIdenticalSelector],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Merge {} adjacent identical selector rules for '{}' into a single block.",
cluster.len(),
first_sel
),
selected: true,
});
i = cursor;
continue;
}
}
i += 1;
}
}
fn note_gather_home<'s>(
source: &'s str,
node: &SourceNode,
exact_homes: &mut Vec<&'s str>,
home_weight: &mut HashMap<&'s str, usize>,
) {
let sel = node.prelude(source).trim();
let core = first_compound_stripped(sel).unwrap_or(sel);
if is_simple_compound_selector(sel) && core == sel && !sel.contains("::") {
if !exact_homes.contains(&sel) {
exact_homes.push(sel);
}
*home_weight.entry(sel).or_insert(0) += style_body_weight(source, node);
}
if let Some(stripped) = first_compound_stripped(sel)
&& !exact_homes.contains(&stripped)
&& is_simple_compound_selector(stripped)
&& !stripped.contains("::")
{
exact_homes.push(stripped);
home_weight.entry(stripped).or_insert(0);
}
}
fn collect_gather_homes<'s>(
source: &'s str,
nodes: &[SourceNode],
exact_homes: &mut Vec<&'s str>,
home_weight: &mut HashMap<&'s str, usize>,
) {
for node in nodes {
if matches!(&node.kind, NodeKind::Style) {
note_gather_home(source, node, exact_homes, home_weight);
} else if let NodeKind::AtBlock { name, .. } = &node.kind
&& GATHER_SCAN_CONTAINERS.contains(&name.as_str())
&& let Some(body_range) = &node.body_range
{
let inner = scan_nodes(source, body_range.clone());
collect_gather_homes(source, &inner, exact_homes, home_weight);
}
}
}
fn should_group_at_block(
name: &str,
source: &str,
style_inners: &[&SourceNode],
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
) -> bool {
if style_inners.len() < 3 {
return false;
}
let mut assigned: HashMap<&str, usize> = HashMap::new();
let mut unsafe_extract = false;
for inner in style_inners {
let sel = inner.prelude(source).trim();
match assign_gather_home(sel, exact_homes, home_weight) {
Some(home) => {
*assigned.entry(home).or_insert(0) += 1;
if home != sel
&& extract_related_nested_selector(home, sel).is_none()
&& !is_absorbable_descendant(home, sel)
{
unsafe_extract = true;
}
}
None => {
if contains_top_level_comma(sel)
&& exact_homes
.iter()
.any(|home| extract_related_nested_selector(home, sel).is_some())
{
} else {
*assigned.entry("").or_insert(0) += 1;
unsafe_extract = true;
}
}
}
}
let strong: Vec<(&str, usize)> = assigned
.iter()
.filter(|(home, _)| !home.is_empty() && is_strong_gather_home(home))
.map(|(h, c)| (*h, *c))
.collect();
let total = style_inners.len();
let dominant = strong.iter().any(|(_, c)| *c * 3 >= total * 2);
if name == "media" || name == "container" {
return unsafe_extract || strong.len() >= 2;
}
if strong.len() >= 2 && !dominant {
return true;
}
unsafe_extract && strong.is_empty()
}
fn mark_grouped_at_blocks(
source: &str,
nodes: &[SourceNode],
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
grouped: &mut HashSet<usize>,
) {
for node in nodes {
if let NodeKind::AtBlock { name, .. } = &node.kind
&& let Some(body_range) = &node.body_range
{
let inner_nodes = scan_nodes(source, body_range.clone());
if GATHERABLE_CONDITIONALS.contains(&name.as_str()) {
let style_inners: Vec<&SourceNode> = inner_nodes
.iter()
.filter(|n| matches!(&n.kind, NodeKind::Style))
.collect();
if should_group_at_block(name, source, &style_inners, exact_homes, home_weight) {
grouped.insert(node.start);
}
}
if GATHERABLE_CONDITIONALS.contains(&name.as_str())
|| GATHER_SCAN_CONTAINERS.contains(&name.as_str())
{
mark_grouped_at_blocks(source, &inner_nodes, exact_homes, home_weight, grouped);
}
}
}
}
fn style_belongs_to_home(
source: &str,
node: &SourceNode,
base: &str,
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
) -> bool {
let sel = node.prelude(source).trim();
sel == base || assign_gather_home(sel, exact_homes, home_weight) == Some(base)
}
fn collect_gather_cluster(
source: &str,
nodes: &[SourceNode],
base: &str,
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
grouped_at_blocks: &HashSet<usize>,
cluster: &mut Vec<GatherMember>,
) {
for node in nodes {
if matches!(&node.kind, NodeKind::Style) {
if style_belongs_to_home(source, node, base, exact_homes, home_weight) {
cluster.push(GatherMember::Style(node.clone()));
}
} else if let NodeKind::AtBlock { name, .. } = &node.kind {
let Some(body_range) = &node.body_range else {
continue;
};
let inner_nodes = scan_nodes(source, body_range.clone());
if GATHER_SCAN_CONTAINERS.contains(&name.as_str()) {
collect_gather_cluster(
source,
&inner_nodes,
base,
exact_homes,
home_weight,
grouped_at_blocks,
cluster,
);
continue;
}
if grouped_at_blocks.contains(&node.start) {
continue;
}
if !GATHERABLE_CONDITIONALS.contains(&name.as_str()) {
continue;
}
let style_inners: Vec<&SourceNode> = inner_nodes
.iter()
.filter(|n| matches!(n.kind, NodeKind::Style))
.collect();
let owned = style_inners
.iter()
.filter(|inner| {
style_belongs_to_home(source, inner, base, exact_homes, home_weight)
})
.count();
let dominate = !style_inners.is_empty() && owned * 3 >= style_inners.len() * 2;
let related: Vec<SourceNode> = inner_nodes
.iter()
.filter(|inner| {
matches!(&inner.kind, NodeKind::Style) && {
let sel = inner.prelude(source).trim();
style_belongs_to_home(source, inner, base, exact_homes, home_weight)
|| (dominate && is_absorbable_descendant(base, sel))
}
})
.cloned()
.collect();
if !related.is_empty() {
let delete_whole_at_block = related.len() == inner_nodes.len()
|| (dominate
&& style_inners.iter().all(|inner| {
let sel = inner.prelude(source).trim();
style_belongs_to_home(source, inner, base, exact_homes, home_weight)
|| is_absorbable_descendant(base, sel)
}));
for inner in related {
cluster.push(GatherMember::Conditional {
at_node: node.clone(),
inner,
delete_whole_at_block,
});
}
}
}
}
}
fn enclosing_container_end(
source: &str,
nodes: &[SourceNode],
first: &SourceNode,
) -> Option<usize> {
fn find(source: &str, nodes: &[SourceNode], first: &SourceNode) -> Option<usize> {
for node in nodes {
if let NodeKind::AtBlock { name, .. } = &node.kind
&& let Some(body_range) = &node.body_range
&& (GATHER_SCAN_CONTAINERS.contains(&name.as_str())
|| GATHERABLE_CONDITIONALS.contains(&name.as_str()))
&& first.start >= body_range.start
&& first.end <= body_range.end
{
let inner = scan_nodes(source, body_range.clone());
if let Some(deeper) = find(source, &inner, first) {
return Some(deeper);
}
return Some(body_range.end);
}
}
None
}
find(source, nodes, first)
}
fn named_layer_ident(prelude: &str) -> Option<String> {
let rest = prelude
.trim()
.strip_prefix("@layer")
.unwrap_or(prelude)
.trim();
if rest.is_empty() || contains_top_level_comma(rest) {
return None;
}
let ident: String = rest
.chars()
.take_while(|c| c.is_ascii_alphanumeric() || matches!(c, '-' | '_' | '.'))
.collect();
if ident.is_empty() { None } else { Some(ident) }
}
#[derive(Clone)]
struct LayeredExactHit {
selector: String,
layer_path: Vec<String>,
style: SourceNode,
layer: SourceNode,
}
fn collect_layered_exact_hits(
source: &str,
nodes: &[SourceNode],
path: &[String],
current_layer: Option<&SourceNode>,
out: &mut Vec<LayeredExactHit>,
) {
for node in nodes {
if matches!(node.kind, NodeKind::Style) {
if let Some(layer) = current_layer
&& !path.is_empty()
{
let sel = node.prelude(source).trim();
if !sel.is_empty() && !contains_top_level_comma(sel) {
out.push(LayeredExactHit {
selector: sel.to_string(),
layer_path: path.to_vec(),
style: node.clone(),
layer: layer.clone(),
});
}
}
continue;
}
if let NodeKind::AtBlock { name, .. } = &node.kind
&& name == "layer"
&& let Some(body_range) = &node.body_range
&& let Some(ident) = named_layer_ident(node.prelude(source))
{
let mut child = path.to_vec();
child.push(ident);
let inner = scan_nodes(source, body_range.clone());
collect_layered_exact_hits(source, &inner, &child, Some(node), out);
}
}
}
fn unwrap_style_block_body(block: &str) -> String {
let open = match block.find('{') {
Some(i) => i + 1,
None => return block.to_string(),
};
let close = match block.rfind('}') {
Some(i) => i,
None => return block[open..].to_string(),
};
if close <= open {
return String::new();
}
block[open..close].trim().to_string()
}
fn reindent_owned(text: &str, indent: &str) -> Vec<String> {
let lines: Vec<&str> = text.lines().collect();
let min_pad = lines
.iter()
.filter_map(|l| {
if l.trim().is_empty() {
None
} else {
Some(l.len() - l.trim_start().len())
}
})
.min()
.unwrap_or(0);
lines
.into_iter()
.map(|l| {
if l.trim().is_empty() {
String::new()
} else {
let rest = if l.len() >= min_pad {
&l[min_pad..]
} else {
l.trim_start()
};
format!("{indent}{rest}")
}
})
.collect()
}
fn layer_body_cluster_for_selector(
source: &str,
layer: &SourceNode,
selector: &str,
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
) -> Vec<GatherMember> {
let Some(body_range) = &layer.body_range else {
return Vec::new();
};
let inner = scan_nodes(source, body_range.clone());
let mut cluster = Vec::new();
for node in &inner {
if matches!(node.kind, NodeKind::Style) && node.prelude(source).trim() == selector {
cluster.push(GatherMember::Style(node.clone()));
} else if let NodeKind::AtBlock { name, .. } = &node.kind
&& GATHERABLE_CONDITIONALS.contains(&name.as_str())
&& let Some(at_body) = &node.body_range
{
let at_inners = scan_nodes(source, at_body.clone());
let related: Vec<SourceNode> = at_inners
.iter()
.filter(|n| {
matches!(n.kind, NodeKind::Style)
&& (n.prelude(source).trim() == selector
|| style_belongs_to_home(source, n, selector, exact_homes, home_weight))
})
.cloned()
.collect();
if related.is_empty() {
continue;
}
let delete_whole = related.len() == at_inners.len();
for inner_style in related {
cluster.push(GatherMember::Conditional {
at_node: node.clone(),
inner: inner_style,
delete_whole_at_block: delete_whole,
});
}
}
}
cluster
}
fn layer_contains_only_selector_cluster(
source: &str,
layer: &SourceNode,
selector: &str,
exact_homes: &[&str],
home_weight: &HashMap<&str, usize>,
) -> bool {
let Some(body_range) = &layer.body_range else {
return false;
};
let inner = scan_nodes(source, body_range.clone());
if inner.is_empty() {
return false;
}
inner.iter().all(|node| {
if matches!(node.kind, NodeKind::Style) {
return node.prelude(source).trim() == selector
|| style_belongs_to_home(source, node, selector, exact_homes, home_weight);
}
if let NodeKind::AtBlock { name, .. } = &node.kind
&& GATHERABLE_CONDITIONALS.contains(&name.as_str())
&& let Some(at_body) = &node.body_range
{
let at_inners = scan_nodes(source, at_body.clone());
return !at_inners.is_empty()
&& at_inners.iter().all(|n| {
matches!(n.kind, NodeKind::Style)
&& (n.prelude(source).trim() == selector
|| style_belongs_to_home(source, n, selector, exact_homes, home_weight))
});
}
false
})
}
fn intervening_unlayered(
nodes: &[SourceNode],
first_layer_start: usize,
last_layer_end: usize,
) -> bool {
nodes.iter().any(|n| {
n.start > first_layer_start
&& n.end < last_layer_end
&& match &n.kind {
NodeKind::Style => true,
NodeKind::AtBlock { name, .. } => name != "layer",
NodeKind::AtStatement { name, .. } => name != "layer",
}
})
}
fn plan_nest_layer_by_selector(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::NestLayerBySelector) {
return;
}
let mut hits = Vec::new();
collect_layered_exact_hits(source, nodes, &[], None, &mut hits);
if hits.is_empty() {
return;
}
let mut exact_homes: Vec<&str> = Vec::new();
let mut home_weight: HashMap<&str, usize> = HashMap::new();
collect_gather_homes(source, nodes, &mut exact_homes, &mut home_weight);
let mut by_sel: HashMap<String, Vec<LayeredExactHit>> = HashMap::new();
for hit in hits {
by_sel.entry(hit.selector.clone()).or_default().push(hit);
}
for (selector, mut group) in by_sel {
group.sort_by_key(|h| h.style.start);
let mut seen_paths: Vec<Vec<String>> = Vec::new();
for hit in &group {
if !seen_paths.iter().any(|p| p == &hit.layer_path) {
seen_paths.push(hit.layer_path.clone());
}
}
if seen_paths.len() < 2 {
continue;
}
let first_layer = &group[0].layer;
let last_layer = &group.last().unwrap().layer;
if intervening_unlayered(nodes, first_layer.start, last_layer.end) {
continue;
}
let parent_indent = String::new();
let unit = " ".to_string();
let mut layer_blocks = Vec::new();
let mut deletes: Vec<(usize, usize, usize)> = Vec::new();
let mut deleted_layers = HashSet::new();
for path in &seen_paths {
let Some(hit) = group.iter().find(|h| &h.layer_path == path) else {
continue;
};
let cluster = layer_body_cluster_for_selector(
source,
&hit.layer,
&selector,
&exact_homes,
&home_weight,
);
if cluster.is_empty() {
continue;
}
let merged = format_merged_rule(&selector, &parent_indent, &unit, &cluster, source);
let body = unwrap_style_block_body(&merged);
let header = format!("@layer {}", path.join("."));
layer_blocks.push((header, body));
if layer_contains_only_selector_cluster(
source,
&hit.layer,
&selector,
&exact_homes,
&home_weight,
) {
if deleted_layers.insert(hit.layer.start) {
deletes.push((
hit.layer.start,
extend_with_trailing_newline(source, hit.layer.end),
hit.layer.start,
));
}
} else {
for member in &cluster {
match member {
GatherMember::Style(n) => {
let start = leading_block_comment(source, n.start)
.map(|(s, _)| s)
.unwrap_or(n.start);
deletes.push((
start,
extend_with_trailing_newline(source, n.end),
n.start,
));
}
GatherMember::Conditional {
at_node,
inner,
delete_whole_at_block,
} => {
if *delete_whole_at_block {
deletes.push((
at_node.start,
extend_with_trailing_newline(source, at_node.end),
at_node.start,
));
} else {
deletes.push((
inner.start,
extend_with_trailing_newline(source, inner.end),
inner.start,
));
}
}
}
}
}
}
if layer_blocks.len() < 2 {
continue;
}
let first_layer_only_ours = layer_contains_only_selector_cluster(
source,
first_layer,
&selector,
&exact_homes,
&home_weight,
);
let insert_at = first_layer.start;
let replace_end = if first_layer_only_ours {
extend_with_trailing_newline(source, first_layer.end)
} else {
insert_at
};
deletes.retain(|(start, end, _)| !(*start == insert_at && *end == replace_end));
let nested_indent = unit.clone();
let inner_indent = format!("{unit}{unit}");
let mut hoisted = format!("{selector} {{\n");
for (i, (header, body)) in layer_blocks.iter().enumerate() {
if i > 0 {
hoisted.push('\n');
}
hoisted.push_str(&nested_indent);
hoisted.push_str(header);
hoisted.push_str(" {\n");
if !body.trim().is_empty() {
for line in reindent_owned(body, &inner_indent) {
hoisted.push_str(&line);
hoisted.push('\n');
}
}
hoisted.push_str(&nested_indent);
hoisted.push_str("}\n");
}
hoisted.push('}');
if first_layer_only_ours {
hoisted.push('\n');
} else {
hoisted.push('\n');
hoisted.push('\n');
}
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::NestLayerBySelector],
safety: Safety::Review,
source_range: SourceRange {
start: insert_at,
end: replace_end,
},
original: if first_layer_only_ours {
source[insert_at..replace_end].to_string()
} else {
String::new()
},
proposed: hoisted,
proof: Proof {
selector_set_equivalent: true,
specificity_equivalent: true,
cascade_context_equivalent: true,
source_order_equivalent: false,
layer_equivalent: true,
scope_equivalent: true,
declarations_exact: true,
important_exact: true,
},
warnings: vec![format!(
"Hoisted '{}' out of {} named layers so nested @layer blocks do not create child layers.",
selector,
layer_blocks.len()
)],
reason: format!(
"Nest {} named layers under shared selector '{}'.",
layer_blocks.len(),
selector
),
selected: true,
});
for (start, end, line_at) in deletes {
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::NestLayerBySelector],
safety: Safety::Review,
source_range: SourceRange { start, end },
original: source[start..end].to_string(),
proposed: String::new(),
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Remove layer-local '{}' after nesting layers at line {}.",
selector,
line_number(source, line_at)
),
selected: true,
});
}
}
}
fn plan_gather_related_selector_rules(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::GatherRelatedSelectorRules) {
return;
}
let mut exact_homes: Vec<&str> = Vec::new();
let mut home_weight: HashMap<&str, usize> = HashMap::new();
collect_gather_homes(source, nodes, &mut exact_homes, &mut home_weight);
let mut grouped_at_blocks: HashSet<usize> = HashSet::new();
mark_grouped_at_blocks(
source,
nodes,
&exact_homes,
&home_weight,
&mut grouped_at_blocks,
);
for base in exact_homes.clone() {
let mut cluster: Vec<GatherMember> = Vec::new();
collect_gather_cluster(
source,
nodes,
base,
&exact_homes,
&home_weight,
&grouped_at_blocks,
&mut cluster,
);
if cluster.len() > 1 {
let mut is_non_adjacent = false;
for window in cluster.windows(2) {
let prev_end = window[0].outer_end();
let next_start = window[1].outer_start();
if next_start < prev_end {
continue;
}
if !is_whitespace_only(source, prev_end..next_start) {
is_non_adjacent = true;
break;
}
}
let first_style = cluster.iter().find_map(|m| match m {
GatherMember::Style(n) => Some(n),
GatherMember::Conditional { .. } => None,
});
let Some(first) = first_style else {
continue;
};
let first_sel = first.prelude(source).trim();
let has_non_style = cluster
.iter()
.any(|m| matches!(m, GatherMember::Conditional { .. }));
let should_gather = is_non_adjacent || first_sel != base || has_non_style;
if !should_gather {
continue;
}
let cluster_safe = cluster.iter().all(|member| {
let sel = match member {
GatherMember::Style(n) => n.prelude(source).trim(),
GatherMember::Conditional { inner, .. } => inner.prelude(source).trim(),
};
can_nest_under_gather_home(base, sel)
});
if !cluster_safe {
continue;
}
let parent_indent = line_indent(source, first.start);
let unit = relative_indent_unit(source, first);
let mut merged = format_merged_rule(base, &parent_indent, &unit, &cluster, source);
let style_members: Vec<&SourceNode> = cluster
.iter()
.filter_map(|m| match m {
GatherMember::Style(n) => Some(n),
GatherMember::Conditional { .. } => None,
})
.collect();
let container_end = enclosing_container_end(source, nodes, first);
let (local_styles, remote_styles): (Vec<&SourceNode>, Vec<&SourceNode>) =
style_members.iter().copied().partition(|sec| {
if let Some(limit) = container_end {
sec.end <= limit
} else {
nodes.iter().any(|n| n.start == sec.start)
}
});
let last_local = local_styles.last().copied().unwrap_or(first);
let first_comment = leading_block_comment(source, first.start);
let span_start =
line_start(source, first_comment.map(|(s, _)| s).unwrap_or(first.start));
let mut span_end = extend_with_trailing_newline(source, last_local.end);
for member in &cluster {
let GatherMember::Conditional {
at_node,
delete_whole_at_block,
..
} = member
else {
continue;
};
if *delete_whole_at_block
&& at_node.start >= span_start
&& (container_end.is_none_or(|limit| at_node.end <= limit))
{
span_end = span_end.max(extend_with_trailing_newline(source, at_node.end));
}
}
if let Some((_, cmt)) = first_comment {
merged = format!("{parent_indent}{cmt}\n{merged}");
}
let mut replacements = vec![(span_start, first.end, merged)];
let mut cluster_starts = HashSet::new();
for sec in &local_styles {
cluster_starts.insert(sec.start);
}
for sec in local_styles.iter().skip(1) {
let start = leading_block_comment(source, sec.start)
.map(|(s, _)| s)
.unwrap_or(sec.start);
let end = extend_with_trailing_newline(source, sec.end);
replacements.push((start, end, String::new()));
}
let mut replaced_at = HashSet::new();
for member in &cluster {
let GatherMember::Conditional {
at_node,
delete_whole_at_block,
..
} = member
else {
continue;
};
if *delete_whole_at_block
&& at_node.start >= span_start
&& at_node.end <= span_end
&& replaced_at.insert(at_node.start)
{
replacements.push((
at_node.start,
extend_with_trailing_newline(source, at_node.end),
String::new(),
));
}
}
replacements.extend(leftover_style_replacements(
source,
nodes,
span_start,
span_end,
&cluster_starts,
));
let proposed = squeeze_excess_blank_lines(&apply_range_replacements(
source,
span_start,
span_end,
&replacements,
));
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::GatherRelatedSelectorRules],
safety: Safety::Review,
source_range: SourceRange {
start: span_start,
end: span_end,
},
original: source[span_start..span_end].to_string(),
proposed,
proof: Proof {
selector_set_equivalent: true,
specificity_equivalent: true,
cascade_context_equivalent: false,
source_order_equivalent: false,
layer_equivalent: remote_styles.is_empty(),
scope_equivalent: true,
declarations_exact: true,
important_exact: true,
},
warnings: vec![format!(
"Gathered {} related occurrences of '{}' across lines; review cascade ordering.",
cluster.len(),
base
)],
reason: format!(
"Gather {} related rules for '{}' into the canonical first selector block.",
cluster.len(),
base
),
selected: true,
});
let mut deleted_at_blocks = HashSet::new();
for sec in &remote_styles {
let start = leading_block_comment(source, sec.start)
.map(|(s, _)| s)
.unwrap_or(sec.start);
let end = extend_with_trailing_newline(source, sec.end);
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::GatherRelatedSelectorRules],
safety: Safety::Review,
source_range: SourceRange { start, end },
original: source[start..end].to_string(),
proposed: String::new(),
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Remove non-adjacent gathered rule for '{}' at line {}.",
base,
line_number(source, sec.start)
),
selected: true,
});
}
for member in &cluster {
let GatherMember::Conditional {
at_node,
inner,
delete_whole_at_block,
} = member
else {
continue;
};
let (sec_start, sec_end, line_at) = if *delete_whole_at_block {
if !deleted_at_blocks.insert(at_node.start) {
continue;
}
(
at_node.start,
extend_with_trailing_newline(source, at_node.end),
at_node.start,
)
} else {
(
inner.start,
extend_with_trailing_newline(source, inner.end),
inner.start,
)
};
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::GatherRelatedSelectorRules],
safety: Safety::Review,
source_range: SourceRange {
start: sec_start,
end: sec_end,
},
original: source[sec_start..sec_end].to_string(),
proposed: String::new(),
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Remove non-adjacent gathered rule for '{}' at line {}.",
base,
line_number(source, line_at)
),
selected: true,
});
}
}
}
}
fn plan_factor_identical_states_with_is(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::FactorIdenticalStatesWithIs) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if matches!(&first.kind, NodeKind::Style) {
let first_sel = first.prelude(source).trim();
if let Some(colon_pos) = first_sel.find(':')
&& !first_sel[colon_pos..].starts_with("::")
{
let base = &first_sel[..colon_pos];
if !base.is_empty() && !base.contains(' ') {
let first_body = first.body(source).unwrap_or("").trim();
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style) {
let next_sel = next.prelude(source).trim();
if next_sel.starts_with(base)
&& next_sel[base.len()..].starts_with(':')
&& !next_sel[base.len()..].starts_with("::")
&& next.body(source).unwrap_or("").trim() == first_body
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let pseudos: Vec<&str> = cluster
.iter()
.map(|c| {
let s = c.prelude(source).trim();
&s[base.len()..]
})
.collect();
let is_inner = pseudos.join(", ");
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let mut decls = String::new();
for line in source[first_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
decls.push_str(&inner_decl_indent);
decls.push_str(&ensure_semicolon(trimmed));
decls.push('\n');
}
}
let proposed = format!(
"{parent_indent}{base} {{\n{nested_indent}&:is({is_inner}) {{\n{decls}{nested_indent}}}\n{parent_indent}}}"
);
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::FactorIdenticalStatesWithIs],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!(
"Factor {} identical state rules for '{}' into &:is({}) form.",
cluster.len(),
base,
is_inner
),
selected: true,
});
i = cursor;
continue;
}
}
}
}
i += 1;
}
}
fn plan_factor_multi_selector_cluster_with_is(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::ModernizeIs) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if matches!(&first.kind, NodeKind::Style) {
let first_sel = first.prelude(source).trim();
if let Some((base_prefixes, first_suffix)) = extract_multi_branch_pattern(first_sel) {
let mut cluster = vec![(first, first_suffix)];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style) {
let next_sel = next.prelude(source).trim();
if let Some((next_prefixes, next_suffix)) =
extract_multi_branch_pattern(next_sel)
&& next_prefixes == base_prefixes
{
cluster.push((next, next_suffix));
prev_end = next.end;
cursor += 1;
continue;
}
}
break;
}
if cluster.len() > 1 {
let (last_node, _) = cluster.last().unwrap();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let is_header = format!(":is({})", base_prefixes.join(", "));
let mut out = format!("{parent_indent}{is_header} {{\n");
let mut has_direct_decls = false;
for &(c_node, ref suffix) in &cluster {
if suffix.is_none()
&& let Some(c_body_range) = &c_node.body_range
{
for line in source[c_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.push_str(&nested_indent);
out.push_str(&ensure_semicolon(trimmed));
out.push('\n');
has_direct_decls = true;
}
}
}
}
for (c_idx, &(c_node, ref suffix)) in cluster.iter().enumerate() {
if let Some(sub_sel) = suffix {
if has_direct_decls || c_idx > 0 {
out.push('\n');
}
out.push_str(&nested_indent);
out.push_str(sub_sel);
out.push_str(" {\n");
if let Some(c_body_range) = &c_node.body_range {
for line in source[c_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.push_str(&inner_decl_indent);
out.push_str(&ensure_semicolon(trimmed));
out.push('\n');
}
}
}
out.push_str(&nested_indent);
out.push_str("}\n");
}
}
out.push_str(&parent_indent);
out.push('}');
let specificities: Vec<Specificity> = base_prefixes
.iter()
.map(|p| calculate_specificity(p))
.collect();
let uniform = specificities.windows(2).all(|w| w[0] == w[1]);
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::ModernizeIs],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last_node.end,
},
original: source[first.start..last_node.end].to_string(),
proposed: out,
proof: Proof::safe_local(),
warnings: if uniform { Vec::new() } else { vec!["Notice: :is() takes the specificity of its most specific argument.".into()] },
reason: format!("Factor multi-selector cluster for {} into :is(...) with nested rules.", is_header),
selected: true,
});
i = cursor;
continue;
}
}
}
i += 1;
}
}
fn extract_multi_branch_pattern(selector: &str) -> Option<(Vec<String>, Option<String>)> {
let branches: Vec<&str> = split_top_level_comma(selector)
.into_iter()
.map(|s| s.trim())
.collect();
if branches.len() < 2 {
return None;
}
if branches.iter().any(|b| b.contains("::")) {
return None;
}
let first = branches[0];
if let Some(space_pos) = first.rfind(' ') {
let suffix = &first[space_pos..];
if branches.iter().all(|b| b.ends_with(suffix)) {
let prefixes: Vec<String> = branches
.iter()
.map(|b| b[..b.len() - suffix.len()].trim().to_string())
.collect();
if prefixes.iter().all(|p| is_valid_selector_token(p)) {
return Some((prefixes, Some(suffix.trim().to_string())));
}
}
}
if branches
.iter()
.all(|b| is_valid_selector_token(b) && !b.contains(' '))
{
let prefixes: Vec<String> = branches.iter().map(|b| b.to_string()).collect();
return Some((prefixes, None));
}
None
}
fn plan_merge_identical_rule_bodies(
path: &Path,
source: &str,
nodes: &[SourceNode],
enabled: &HashSet<RuleId>,
plans: &mut Vec<PlanEntry>,
) {
if !enabled.contains(&RuleId::MergeIdenticalRuleBodies) {
return;
}
let mut i = 0;
while i < nodes.len() {
let first = &nodes[i];
if matches!(&first.kind, NodeKind::Style) {
let first_body = first.body(source).unwrap_or("").trim();
if !first_body.is_empty() {
let mut cluster = vec![first];
let mut cursor = i + 1;
let mut prev_end = first.end;
while cursor < nodes.len() {
let next = &nodes[cursor];
if !is_whitespace_only(source, prev_end..next.start) {
break;
}
if matches!(&next.kind, NodeKind::Style)
&& next.body(source).unwrap_or("").trim() == first_body
{
cluster.push(next);
prev_end = next.end;
cursor += 1;
continue;
}
break;
}
if cluster.len() > 1 {
let last = cluster.last().unwrap();
let selectors: Vec<&str> =
cluster.iter().map(|c| c.prelude(source).trim()).collect();
let parent_indent = line_indent(source, first.start);
let first_body_range = first.body_range.as_ref().unwrap();
let unit = detect_indent_unit(source, first_body_range.clone())
.unwrap_or_else(|| " ".to_string());
let nested_indent = format!("{parent_indent}{unit}");
let mut decls = String::new();
for line in source[first_body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
decls.push_str(&nested_indent);
decls.push_str(&ensure_semicolon(trimmed));
decls.push('\n');
}
}
let joined_sel = selectors.join(&format!(",\n{parent_indent}"));
let proposed =
format!("{parent_indent}{joined_sel} {{\n{decls}{parent_indent}}}");
plans.push(PlanEntry {
id: String::new(),
file: path.to_path_buf(),
rules: vec![RuleId::MergeIdenticalRuleBodies],
safety: Safety::Safe,
source_range: SourceRange {
start: first.start,
end: last.end,
},
original: source[first.start..last.end].to_string(),
proposed,
proof: Proof::safe_local(),
warnings: Vec::new(),
reason: format!("Merge {} rules with identical declaration bodies into a single comma-separated rule.", cluster.len()),
selected: true,
});
i = cursor;
continue;
}
}
}
i += 1;
}
}
pub fn split_top_level_comma(selector: &str) -> Vec<&str> {
let bytes = selector.as_bytes();
let mut parts = Vec::new();
let mut last = 0;
let mut parens = 0usize;
let mut brackets = 0usize;
let mut quote: Option<u8> = None;
let mut escaped = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'\'' | b'"' => quote = Some(b),
b'(' => parens += 1,
b')' => parens = parens.saturating_sub(1),
b'[' => brackets += 1,
b']' => brackets = brackets.saturating_sub(1),
b',' if parens == 0 && brackets == 0 => {
parts.push(&selector[last..i]);
last = i + 1;
}
_ => {}
}
i += 1;
}
if last < selector.len() {
parts.push(&selector[last..]);
}
parts
}
pub fn factor_selector_list(
selector: &str,
body: &str,
indent: &str,
unit: &str,
) -> Option<String> {
let branches: Vec<&str> = split_top_level_comma(selector)
.into_iter()
.map(|s| s.trim())
.collect();
if branches.len() < 2 {
return None;
}
let base = branches[0];
if contains_top_level_comma(base) || base.contains("::") || base.is_empty() {
return None;
}
let mut inner_selectors = Vec::new();
for &branch in &branches {
if branch == base {
inner_selectors.push("&".to_string());
} else {
let rel = branch.strip_prefix(base)?;
if rel.starts_with("::")
|| rel.starts_with(':')
|| rel.starts_with('[')
|| rel.starts_with('.')
|| rel.starts_with('#')
{
inner_selectors.push(format!("&{rel}"));
} else {
let trimmed = rel.strip_prefix(' ')?;
inner_selectors.push(trimmed.trim_start().to_string());
}
}
}
let nested_indent = format!("{indent}{unit}");
let inner_decl_indent = format!("{nested_indent}{unit}");
let mut out = String::new();
out.push_str(base);
out.push_str(" {\n");
out.push_str(&nested_indent);
out.push_str(&inner_selectors.join(&format!(",\n{nested_indent}")));
out.push_str(" {\n");
for line in body.lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
out.push_str(&inner_decl_indent);
out.push_str(&ensure_semicolon(trimmed));
out.push('\n');
}
}
out.push_str(&nested_indent);
out.push_str("}\n");
out.push_str(indent);
out.push('}');
Some(out)
}
pub fn factor_with_is(selector: &str) -> Option<(String, bool)> {
let branches: Vec<&str> = split_top_level_comma(selector)
.into_iter()
.map(|s| s.trim())
.filter(|s| !s.is_empty())
.collect();
if branches.len() < 2 {
return None;
}
if branches.iter().any(|b| b.contains("::")) {
return None;
}
let specificities: Vec<Specificity> =
branches.iter().map(|b| calculate_specificity(b)).collect();
let uniform_specificity = specificities.windows(2).all(|w| w[0] == w[1]);
let first = branches[0];
if let Some(space_pos) = first.rfind(' ') {
let suffix = &first[space_pos..];
if branches.iter().all(|b| b.ends_with(suffix)) {
let prefixes: Vec<&str> = branches
.iter()
.map(|b| b[..b.len() - suffix.len()].trim())
.collect();
if prefixes.iter().all(|p| is_valid_selector_token(p)) {
let is_inner = prefixes.join(", ");
return Some((format!(":is({is_inner}){suffix}"), uniform_specificity));
}
}
}
if let Some(space_pos) = first.rfind(' ') {
let prefix = &first[..=space_pos];
if branches.iter().all(|b| b.starts_with(prefix)) {
let suffixes: Vec<&str> = branches.iter().map(|b| b[prefix.len()..].trim()).collect();
if suffixes.iter().all(|s| is_valid_selector_token(s)) {
let is_inner = suffixes.join(", ");
return Some((format!("{prefix}:is({is_inner})"), uniform_specificity));
}
}
}
if let Some(colon_pos) = first.find(':') {
let base = &first[..colon_pos];
if !base.is_empty()
&& !base.contains(' ')
&& branches.iter().all(|b| {
b.starts_with(base)
&& b[base.len()..].starts_with(':')
&& !b[base.len()..].starts_with("::")
})
{
let pseudos: Vec<&str> = branches.iter().map(|b| b[base.len()..].trim()).collect();
if pseudos
.iter()
.all(|p| p.starts_with(':') && !p.starts_with("::") && !p.contains(' '))
{
let is_inner = pseudos.join(", ");
return Some((format!("{base}:is({is_inner})"), uniform_specificity));
}
}
}
if let Some(bracket_pos) = first.find('[') {
let base = &first[..bracket_pos];
if !base.is_empty()
&& !base.contains(' ')
&& branches
.iter()
.all(|b| b.starts_with(base) && b[base.len()..].starts_with('['))
{
let attrs: Vec<&str> = branches.iter().map(|b| b[base.len()..].trim()).collect();
if attrs.iter().all(|a| a.starts_with('[') && a.ends_with(']')) {
let is_inner = attrs.join(", ");
return Some((format!("{base}:is({is_inner})"), uniform_specificity));
}
}
}
None
}
fn is_valid_selector_token(s: &str) -> bool {
if s.is_empty() {
return false;
}
let first = s.chars().next().unwrap();
first == '.'
|| first == '#'
|| first == '['
|| first == ':'
|| first.is_ascii_alphabetic()
|| first == '*'
|| first == '>'
|| first == '+'
|| first == '~'
}
pub fn factor_with_where(selector: &str) -> Option<String> {
let branches: Vec<&str> = split_top_level_comma(selector)
.into_iter()
.map(|s| s.trim())
.collect();
if branches.len() < 2 {
return None;
}
if branches.iter().any(|b| b.contains("::")) {
return None;
}
Some(format!(":where({})", branches.join(", ")))
}
pub fn modernize_media_query_str(prelude: &str) -> Option<String> {
let mut result = prelude.to_string();
let mut changed = false;
if let (Some((min_raw, min_val)), Some((max_raw, max_val))) = (
extract_media_feature_and_raw(&result, "min-width"),
extract_media_feature_and_raw(&result, "max-width"),
) {
let pattern = format!("{min_raw} and {max_raw}");
let replacement = format!("({min_val} <= width <= {max_val})");
if result.contains(&pattern) {
result = result.replace(&pattern, &replacement);
changed = true;
}
}
while let Some((raw, val)) = extract_media_feature_and_raw(&result, "min-width") {
let replacement = format!("(width >= {val})");
result = result.replacen(&raw, &replacement, 1);
changed = true;
}
while let Some((raw, val)) = extract_media_feature_and_raw(&result, "max-width") {
let replacement = format!("(width <= {val})");
result = result.replacen(&raw, &replacement, 1);
changed = true;
}
while let Some((raw, val)) = extract_media_feature_and_raw(&result, "min-height") {
let replacement = format!("(height >= {val})");
result = result.replacen(&raw, &replacement, 1);
changed = true;
}
while let Some((raw, val)) = extract_media_feature_and_raw(&result, "max-height") {
let replacement = format!("(height <= {val})");
result = result.replacen(&raw, &replacement, 1);
changed = true;
}
if changed { Some(result) } else { None }
}
fn extract_media_feature_and_raw(source: &str, feature: &str) -> Option<(String, String)> {
let feat_idx = source.find(feature)?;
let open_paren = source[..feat_idx].rfind('(')?;
if source[open_paren..feat_idx].contains(')') {
return None;
}
let colon_rel = source[feat_idx + feature.len()..].find(':')?;
let colon_idx = feat_idx + feature.len() + colon_rel;
let close_paren_rel = source[colon_idx..].find(')')?;
let close_paren = colon_idx + close_paren_rel;
let raw = source[open_paren..=close_paren].to_string();
let val = source[colon_idx + 1..close_paren].trim().to_string();
Some((raw, val))
}
fn selector_relation(parent: &str, child: &str) -> Option<(RelationKind, String)> {
let parent = parent.trim();
let child = child.trim();
if parent.is_empty()
|| child.is_empty()
|| contains_top_level_comma(parent)
|| contains_top_level_comma(child)
|| parent.contains("::")
|| child == parent
{
return None;
}
if !child.starts_with(parent) {
if is_weak_gather_base(parent) {
return None;
}
return appended_selector_relation(parent, child);
}
let remainder = &child[parent.len()..];
let trimmed = remainder.trim_start();
if trimmed.is_empty() {
return None;
}
let (relation, nested_selector) = if remainder.starts_with("::") {
(RelationKind::PseudoElement, format!("&{remainder}"))
} else if remainder.starts_with(':') {
(RelationKind::PseudoClass, format!("&{remainder}"))
} else if remainder.starts_with('[') {
(RelationKind::Attribute, format!("&{remainder}"))
} else if remainder.starts_with('.') || remainder.starts_with('#') {
(RelationKind::Compound, format!("&{remainder}"))
} else if let Some(first_char) = trimmed
.chars()
.next()
.filter(|c| *c == '>' || *c == '+' || *c == '~')
{
let after_comb = trimmed[first_char.len_utf8()..].trim();
if after_comb == parent {
(RelationKind::Combinator, format!("{first_char} {parent}"))
} else if let Some(after_parent) = after_comb.strip_prefix(parent) {
(
RelationKind::Combinator,
format!("{first_char} {parent}{after_parent}"),
)
} else {
(
RelationKind::Combinator,
format!("{first_char} {after_comb}"),
)
}
} else if remainder
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_whitespace())
{
let descendant = remainder.trim();
(RelationKind::Descendant, descendant.to_string())
} else {
return None;
};
Some((relation, nested_selector))
}
fn conditional_child(
source: &str,
parent_selector: &str,
node: &SourceNode,
enabled: &HashSet<RuleId>,
) -> Option<ClusterChild> {
let (name, rule) = match &node.kind {
NodeKind::AtBlock { name, .. } if name == "media" => (name.as_str(), RuleId::NestMedia),
NodeKind::AtBlock { name, .. } if name == "supports" => {
(name.as_str(), RuleId::NestSupports)
}
NodeKind::AtBlock { name, .. } if name == "container" => {
(name.as_str(), RuleId::NestContainer)
}
NodeKind::AtBlock { name, .. } if name == "starting-style" => {
(name.as_str(), RuleId::NestStartingStyle)
}
_ => return None,
};
if !enabled.contains(&rule) {
return None;
}
let body_range = node.body_range.clone()?;
let inner_nodes = scan_nodes(source, body_range.clone());
if inner_nodes.is_empty() {
return None;
}
let mut inners = Vec::new();
for inner in &inner_nodes {
if !matches!(&inner.kind, NodeKind::Style) {
return None;
}
let inner_prelude = inner.prelude(source);
let inner_body = inner.body_range.clone()?;
if inner_prelude == parent_selector.trim() {
inners.push(ConditionalInner::Direct {
body_range: inner_body,
});
} else if let Some((_rel, nested_sel)) = selector_relation(parent_selector, inner_prelude) {
inners.push(ConditionalInner::Nested {
nested_selector: nested_sel,
body_range: inner_body,
});
} else {
return None;
}
}
debug_assert!(
name == "media" || name == "supports" || name == "container" || name == "starting-style"
);
Some(ClusterChild::Conditional {
node: node.clone(),
rule,
inners,
})
}
pub fn consolidate_not_in_selector(selector: &str) -> Option<(String, bool)> {
if !selector.contains(":not(") {
return None;
}
let mut result = String::new();
let mut i = 0;
let bytes = selector.as_bytes();
let mut changed = false;
let mut uniform_specificity = true;
while i < bytes.len() {
if i + 5 <= bytes.len() && &selector[i..i + 5] == ":not(" {
let mut args = Vec::new();
let mut current_end = i;
while current_end + 5 <= bytes.len()
&& &selector[current_end..current_end + 5] == ":not("
{
let open = current_end + 4;
if let Some(close) = find_matching_paren(selector, open) {
let arg = selector[open + 1..close].trim();
args.push(arg);
current_end = close + 1;
} else {
break;
}
}
if args.len() > 1 {
changed = true;
let specs: Vec<Specificity> =
args.iter().map(|a| calculate_specificity(a)).collect();
if specs.windows(2).any(|w| w[0] != w[1]) {
uniform_specificity = false;
}
result.push_str(":not(");
result.push_str(&args.join(", "));
result.push(')');
i = current_end;
continue;
}
}
let ch = selector[i..].chars().next().unwrap();
result.push(ch);
i += ch.len_utf8();
}
if changed {
Some((result, uniform_specificity))
} else {
None
}
}
fn find_matching_paren(source: &str, open: usize) -> Option<usize> {
let bytes = source.as_bytes();
let mut depth = 1usize;
let mut i = open + 1;
let mut quote: Option<u8> = None;
let mut escaped = false;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'\'' | b'"' => quote = Some(b),
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
_ => {}
}
i += 1;
}
None
}
#[derive(Debug, Clone)]
struct HierarchicalRule {
relative_selector: String,
body_lines: Vec<String>,
sub_rules: Vec<HierarchicalRule>,
conditional_header: Option<String>,
}
fn render_cluster(source: &str, parent: &SourceNode, children: &[ClusterChild]) -> String {
let parent_body_range = parent.body_range.as_ref().expect("style rules have bodies");
let parent_indent = line_indent(source, parent.start);
let unit = relative_indent_unit(source, parent);
let nested_indent = format!("{parent_indent}{unit}");
let mut out = String::new();
let open = parent_body_range.start - 1;
out.push_str(&source[parent.start..=open]);
let parent_body = &source[parent_body_range.clone()];
let trimmed_body = parent_body.trim();
if !trimmed_body.is_empty() {
out.push('\n');
for line in parent_body.lines() {
let trimmed_line = line.trim();
if !trimmed_line.is_empty() {
out.push_str(&nested_indent);
out.push_str(&ensure_semicolon(trimmed_line));
out.push('\n');
}
}
}
let mut root_rules: Vec<HierarchicalRule> = Vec::new();
for child in children {
match child {
ClusterChild::Style {
node,
nested_selector,
..
} => {
let mut body_lines = Vec::new();
if let Some(body_range) = &node.body_range {
for line in source[body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
body_lines.push(trimmed.to_string());
}
}
}
insert_hierarchical_style(&mut root_rules, nested_selector.trim(), body_lines);
}
ClusterChild::Conditional { node, inners, .. } => {
let header = node.prelude(source).trim().to_string();
let mut cond_sub_rules = Vec::new();
for inner in inners {
match inner {
ConditionalInner::Direct { body_range } => {
let mut lines = Vec::new();
for line in source[body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
lines.push(trimmed.to_string());
}
}
cond_sub_rules.push(HierarchicalRule {
relative_selector: String::new(),
body_lines: lines,
sub_rules: Vec::new(),
conditional_header: None,
});
}
ConditionalInner::Nested {
nested_selector,
body_range,
} => {
let mut lines = Vec::new();
for line in source[body_range.clone()].lines() {
let trimmed = line.trim();
if !trimmed.is_empty() {
lines.push(trimmed.to_string());
}
}
cond_sub_rules.push(HierarchicalRule {
relative_selector: nested_selector.trim().to_string(),
body_lines: lines,
sub_rules: Vec::new(),
conditional_header: None,
});
}
}
}
root_rules.push(HierarchicalRule {
relative_selector: String::new(),
body_lines: Vec::new(),
sub_rules: cond_sub_rules,
conditional_header: Some(header),
});
}
}
}
for rule in &root_rules {
out.push('\n');
render_hierarchical_rule(&mut out, rule, &nested_indent, &unit);
}
out.push_str(&parent_indent);
out.push('}');
out
}
fn insert_hierarchical_style(
root_rules: &mut Vec<HierarchicalRule>,
selector: &str,
body_lines: Vec<String>,
) {
if let Some(last_rule) = root_rules.last_mut()
&& last_rule.conditional_header.is_none()
&& !last_rule.relative_selector.is_empty()
{
let parent_sel = &last_rule.relative_selector;
if let Some(rel) = extract_relative_subselector(parent_sel, selector) {
insert_hierarchical_style(&mut last_rule.sub_rules, &rel, body_lines);
return;
}
}
root_rules.push(HierarchicalRule {
relative_selector: selector.to_string(),
body_lines,
sub_rules: Vec::new(),
conditional_header: None,
});
}
fn extract_relative_subselector(parent: &str, child: &str) -> Option<String> {
let parent = parent.trim();
let child = child.trim();
if child == parent || !child.starts_with(parent) {
return None;
}
let remainder = &child[parent.len()..];
let trimmed = remainder.trim_start();
if trimmed.is_empty() {
return None;
}
if remainder.starts_with("::")
|| remainder.starts_with(':')
|| remainder.starts_with('[')
|| remainder.starts_with('.')
|| remainder.starts_with('#')
{
Some(format!("&{remainder}"))
} else if let Some(first_char) = trimmed
.chars()
.next()
.filter(|c| *c == '>' || *c == '+' || *c == '~')
{
let after_comb = trimmed[first_char.len_utf8()..].trim();
Some(format!("{first_char} {after_comb}"))
} else if remainder
.as_bytes()
.first()
.is_some_and(|b| b.is_ascii_whitespace())
{
Some(trimmed.to_string())
} else {
None
}
}
fn render_hierarchical_rule(out: &mut String, rule: &HierarchicalRule, indent: &str, unit: &str) {
let inner_indent = format!("{indent}{unit}");
if let Some(header) = &rule.conditional_header {
out.push_str(indent);
out.push_str(header);
out.push_str(" {\n");
for (idx, sub) in rule.sub_rules.iter().enumerate() {
if idx > 0 {
out.push('\n');
}
if sub.relative_selector.is_empty() {
for line in &sub.body_lines {
out.push_str(&inner_indent);
out.push_str(&ensure_semicolon(line));
out.push('\n');
}
} else {
render_hierarchical_rule(out, sub, &inner_indent, unit);
}
}
out.push_str(indent);
out.push_str("}\n");
} else {
out.push_str(indent);
out.push_str(&rule.relative_selector);
out.push_str(" {\n");
for line in &rule.body_lines {
out.push_str(&inner_indent);
out.push_str(&ensure_semicolon(line));
out.push('\n');
}
for sub in &rule.sub_rules {
out.push('\n');
render_hierarchical_rule(out, sub, &inner_indent, unit);
}
out.push_str(indent);
out.push_str("}\n");
}
}
fn line_indent(source: &str, offset: usize) -> String {
let line_start = source[..offset].rfind('\n').map_or(0, |idx| idx + 1);
source[line_start..offset]
.chars()
.take_while(|c| c.is_whitespace() && *c != '\n' && *c != '\r')
.collect()
}
fn ensure_semicolon(line: &str) -> std::borrow::Cow<'_, str> {
let trimmed = line.trim_end();
if trimmed.ends_with(';')
|| trimmed.ends_with('{')
|| trimmed.ends_with('}')
|| trimmed.ends_with(',')
|| trimmed.starts_with("//")
|| trimmed.starts_with("/*")
|| !trimmed.contains(':')
{
return std::borrow::Cow::Borrowed(line);
}
std::borrow::Cow::Owned(format!("{trimmed};"))
}
fn line_number(source: &str, offset: usize) -> usize {
source[..offset.min(source.len())].lines().count()
}
fn detect_indent_unit(source: &str, body: Range<usize>) -> Option<String> {
for line in source[body].lines() {
if line.trim().is_empty() {
continue;
}
let indent: String = line
.chars()
.take_while(|c| *c == ' ' || *c == '\t')
.collect();
if !indent.is_empty() {
return Some(indent);
}
}
None
}
fn contains_top_level_comma(selector: &str) -> bool {
let bytes = selector.as_bytes();
let mut parens = 0usize;
let mut brackets = 0usize;
let mut quote: Option<u8> = None;
let mut escaped = false;
let mut i = 0usize;
while i < bytes.len() {
let b = bytes[i];
if let Some(q) = quote {
if escaped {
escaped = false;
} else if b == b'\\' {
escaped = true;
} else if b == q {
quote = None;
}
i += 1;
continue;
}
match b {
b'\'' | b'"' => quote = Some(b),
b'(' => parens += 1,
b')' => parens = parens.saturating_sub(1),
b'[' => brackets += 1,
b']' => brackets = brackets.saturating_sub(1),
b',' if parens == 0 && brackets == 0 => return true,
_ => {}
}
i += 1;
}
false
}
pub fn apply_selected_plans(
source: &str,
plans: &[PlanEntry],
include_review: bool,
) -> Result<String> {
let selected: Vec<&PlanEntry> = plans
.iter()
.filter(|plan| {
plan.selected
&& (plan.safety == Safety::Safe
|| (include_review && plan.safety == Safety::Review))
})
.collect();
let owned: Vec<PlanEntry> = selected.into_iter().cloned().collect();
let keep = select_disjoint_plan_indices(&owned);
let mut non_overlapping: Vec<&PlanEntry> = keep.iter().map(|&i| &owned[i]).collect();
non_overlapping.sort_by(|a, b| {
a.source_range
.start
.cmp(&b.source_range.start)
.then_with(|| b.source_range.end.cmp(&a.source_range.end))
});
let mut output = source.to_string();
for plan in non_overlapping.into_iter().rev() {
if plan.source_range.start <= output.len()
&& plan.source_range.end <= output.len()
&& plan.source_range.start <= plan.source_range.end
{
output.replace_range(
plan.source_range.start..plan.source_range.end,
&plan.proposed,
);
}
}
Ok(output)
}
pub fn unified_diff(old: &str, new: &str, old_name: &str, new_name: &str) -> String {
TextDiff::from_lines(old, new)
.unified_diff()
.header(old_name, new_name)
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
fn plan(css: &str, rules: &[RuleId]) -> Vec<PlanEntry> {
analyze_source(PathBuf::from("test.css"), css, rules)
.unwrap()
.plans
}
#[test]
fn nests_adjacent_pseudo_and_descendant_rules() {
let css = ".card {\n color: red;\n}\n.card:hover {\n color: blue !important;\n}\n.card .title {\n font-weight: 700;\n}\n";
let plans = plan(css, &RuleId::ALL);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("&:hover"));
assert!(output.contains(".title"));
assert!(output.contains("color: blue !important;"));
}
#[test]
fn nests_exact_full_modernize_example() {
let original = r#".card {
color: #222;
padding: 1rem;
}
.card:hover {
color: #111 !important;
}
.card::before {
content: "";
}
.card[data-active] {
border-color: currentColor;
}
.card.featured {
box-shadow: 0 0 0 1px currentColor;
}
.card .title {
font-weight: 700;
}
.card > .body {
min-width: 0;
}
.card + .card {
margin-top: 1rem;
}
@media (width >= 48rem) {
.card {
padding: 1.5rem;
}
}
@supports (display: grid) {
.card {
display: grid;
}
}
"#;
let expected = r#".card {
color: #222;
padding: 1rem;
&:hover {
color: #111 !important;
}
&::before {
content: "";
}
&[data-active] {
border-color: currentColor;
}
&.featured {
box-shadow: 0 0 0 1px currentColor;
}
.title {
font-weight: 700;
}
> .body {
min-width: 0;
}
+ .card {
margin-top: 1rem;
}
@media (width >= 48rem) {
padding: 1.5rem;
}
@supports (display: grid) {
display: grid;
}
}"#;
let plans = plan(
original,
&[
RuleId::NestPseudoClass,
RuleId::NestPseudoElement,
RuleId::NestAttribute,
RuleId::NestCompound,
RuleId::NestDescendant,
RuleId::NestCombinator,
RuleId::NestMedia,
RuleId::NestSupports,
],
);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(original, &plans, false).unwrap();
assert_eq!(output.trim(), expected.trim());
}
#[test]
fn factors_selector_list_sharing_base() {
let css = ".marker,\n.marker::before,\n.marker::after {\n box-sizing: border-box;\n}\n";
let plans = plan(css, &[RuleId::FactorSelectorList]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".marker {"));
assert!(output.contains("&,"));
assert!(output.contains("&::before,"));
assert!(output.contains("&::after {"));
assert!(output.contains("box-sizing: border-box;"));
}
#[test]
fn modernizes_is_with_uniform_specificity() {
let css = ".button:hover, .button:focus, .button:active {\n color: blue;\n}\n";
let plans = plan(css, &[RuleId::ModernizeIs]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".button:is(:hover, :focus, :active)"));
}
#[test]
fn modernizes_media_range_syntax() {
let css = "@media (min-width: 800px) {\n .card { padding: 2rem; }\n}\n";
let plans = plan(css, &[RuleId::ModernizeMediaRange]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("@media (width >= 800px)"));
}
#[test]
fn consolidates_not_selectors() {
let css = "input:not([type=\"checkbox\"]):not([type=\"radio\"]) {\n border: 1px solid gray;\n}\n";
let plans = plan(css, &[RuleId::ConsolidateNot]);
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].safety, Safety::Review);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains("input:not([type=\"checkbox\"], [type=\"radio\"])"));
}
#[test]
fn refuses_subtoken_is_factoring_false_positive() {
let css = ".same-specificity-a,\n.same-specificity-b {\n color: black;\n}\n";
let plans = plan(css, &[RuleId::ModernizeIs]);
assert!(plans.is_empty());
}
#[test]
fn modernizes_descendant_is_alternatives() {
let css = ".card .title, .card .subtitle, .card .description {\n color: black;\n}\n";
let plans = plan(css, &[RuleId::ModernizeIs]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".card :is(.title, .subtitle, .description)"));
}
#[test]
fn modernizes_suffix_is_alternatives() {
let css = ".alpha .title,\n#hero .title {\n color: rebeccapurple;\n}\n";
let plans = plan(css, &[RuleId::ModernizeIs]);
assert_eq!(plans.len(), 1);
assert_eq!(plans[0].safety, Safety::Review);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains(":is(.alpha, #hero) .title"));
}
#[test]
fn factors_multi_selector_cluster_with_is_and_nesting() {
let css = r#".alpha .title,
#hero .title {
color: rebeccapurple;
}
.alpha .subtitle,
#hero .subtitle {
color: slateblue;
}
.alpha,
#hero {
border-color: currentColor;
}
"#;
let plans = plan(css, &[RuleId::ModernizeIs]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains(":is(.alpha, #hero) {"));
assert!(output.contains("border-color: currentColor;"));
assert!(output.contains(".title {"));
assert!(output.contains("color: rebeccapurple;"));
assert!(output.contains(".subtitle {"));
assert!(output.contains("color: slateblue;"));
}
#[test]
fn refuses_bem_token_concatenation() {
let css = ".card { color: red; }\n.card__title { font-weight: 700; }\n";
let plans = plan(css, &RuleId::ALL);
assert!(plans.is_empty());
}
#[test]
fn merges_same_named_layer_blocks() {
let css = "@layer overrides {\n .layered-card {\n color: darkgreen;\n }\n}\n\n@layer overrides {\n .layer-important {\n color: orange !important;\n }\n}\n";
let plans = plan(css, &[RuleId::MergeSameNamedLayer]);
assert_eq!(plans.len(), 2);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("@layer overrides {"));
assert!(output.contains(".layered-card {"));
assert!(output.contains(".layer-important {"));
}
#[test]
fn merges_adjacent_media_queries() {
let css = "@media (width >= 48rem) {\n .card {\n padding: 2rem;\n }\n}\n\n@media (width >= 48rem) {\n .panel {\n padding: 2rem;\n }\n}\n";
let plans = plan(css, &[RuleId::MergeAdjacentMedia]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("@media (width >= 48rem) {"));
assert!(output.contains(".card {"));
assert!(output.contains(".panel {"));
}
#[test]
fn merges_adjacent_supports_queries() {
let css = "@supports (display: grid) {\n .card {\n display: grid;\n }\n}\n\n@supports (display: grid) {\n .panel {\n display: grid;\n }\n}\n";
let plans = plan(css, &[RuleId::MergeAdjacentSupports]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("@supports (display: grid) {"));
assert!(output.contains(".card {"));
assert!(output.contains(".panel {"));
}
#[test]
fn merges_adjacent_identical_selectors() {
let css = ".card {\n color: black;\n}\n\n.card {\n padding: 1rem;\n}\n";
let plans = plan(css, &[RuleId::MergeAdjacentIdenticalSelector]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".card {"));
assert!(output.contains("color: black;"));
assert!(output.contains("padding: 1rem;"));
}
#[test]
fn merges_identical_rule_bodies() {
let css = ".card:hover {\n color: red;\n}\n\n.panel:hover {\n color: red;\n}\n";
let plans = plan(css, &[RuleId::MergeIdenticalRuleBodies]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".card:hover,"));
assert!(output.contains(".panel:hover {"));
assert!(output.contains("color: red;"));
}
#[test]
fn factors_identical_states_with_is() {
let css = ".card:hover {\n background: silver;\n}\n\n.card:focus {\n background: silver;\n}\n\n.card:focus-visible {\n background: silver;\n}\n";
let plans = plan(css, &[RuleId::FactorIdenticalStatesWithIs]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".card {"));
assert!(output.contains("&:is(:hover, :focus, :focus-visible) {"));
assert!(output.contains("background: silver;"));
}
#[test]
fn nests_multi_level_tree_hierarchy() {
let css = r#".tree {
display: grid;
gap: 0.5rem;
}
.tree .node {
position: relative;
}
.tree .node .label {
display: flex;
}
.tree .node .label:hover {
color: var(--accent);
}
.tree .node > .children {
margin-inline-start: 1.25rem;
}
.tree .node > .children > .node + .node {
margin-block-start: 0.25rem;
}
"#;
let plans = plan(
css,
&[
RuleId::NestDescendant,
RuleId::NestCombinator,
RuleId::NestPseudoClass,
],
);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".node {"));
assert!(output.contains(".label {"));
assert!(output.contains("&:hover {"));
assert!(output.contains("> .children {"));
assert!(output.contains("> .node + .node {"));
}
#[test]
fn nests_in_place_input_states() {
let css = "input:user-invalid {\n border-color: crimson;\n}\ninput:user-valid {\n border-color: seagreen;\n}\ninput:placeholder-shown {\n color: gray;\n}\n";
let plans = plan(css, &[RuleId::NestPseudoClass]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains("input {"));
assert!(output.contains("&:user-invalid {"));
assert!(output.contains("&:user-valid {"));
assert!(output.contains("&:placeholder-shown {"));
}
#[test]
fn gathers_consecutive_conditions_by_selector() {
let css = "@media (width >= 30rem) {\n .responsive-grid {\n gap: 1rem;\n }\n}\n\n@media (width >= 80rem) {\n .responsive-grid {\n gap: 2rem;\n }\n}\n";
let plans = plan(css, &[RuleId::NestMedia]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".responsive-grid {"));
assert!(output.contains("@media (width >= 30rem) {"));
assert!(output.contains("@media (width >= 80rem) {"));
}
#[test]
fn factors_selector_list_with_adjacent_hover() {
let css = ".notice,\n.notice::before,\n.notice::after {\n color: currentColor;\n}\n\n.notice:hover {\n background: color-mix(in srgb, currentColor 8%, transparent);\n}\n";
let plans = plan(css, &[RuleId::FactorSelectorList, RuleId::NestPseudoClass]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(".notice {"));
assert!(output.contains("&,"));
assert!(output.contains("&::before,"));
assert!(output.contains("&::after {"));
assert!(output.contains("&:hover {"));
assert!(output.contains("background: color-mix"));
}
#[test]
fn gathers_non_adjacent_related_selector_rules_with_nested_blocks() {
let css = r#".skip-link {
position: absolute;
inset-block-start: -48px;
inset-inline-start: 1rem;
z-index: 10000000000;
background: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 0.55rem 0.8rem;
text-decoration: none;
font-weight: 700;
transition: inset-block-start 0.2s ease;
&:focus-visible {
inset-block-start: 0.75rem;
}
}
.unrelated-rule {
color: red;
}
.skip-link {
font: optional;
&::after {
content: '';
}
:not(*) & {
all: unset
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains("font: optional;"));
}
#[test]
fn gathers_non_adjacent_related_pseudo_and_combinator_rules() {
let css = r#".skip-link {
position: absolute;
inset-block-start: -48px;
inset-inline-start: 1rem;
z-index: 10000000000;
background: var(--bg-color);
color: var(--text-color);
border: 1px solid var(--border-color);
border-radius: 0.5rem;
padding: 0.55rem 0.8rem;
text-decoration: none;
font-weight: 700;
transition: inset-block-start 0.2s ease;
&:focus-visible {
inset-block-start: 0.75rem;
}
}
.unrelated {
color: red;
}
.skip-link {
font: optional;
&::after {
content: '';
}
:not(*) & {
all: unset
}
}
.skip-link+* {
display: block;
}
.skip-link::backdrop {
background-color: gray;
}
.skip-link:has(*) {
color: #27ca3f;
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains("font: optional;"));
assert!(output.contains("+ * {"));
assert!(output.contains("&::backdrop {"));
assert!(output.contains("&:has(*) {"));
}
#[test]
fn test_modernizes_media_range_syntax_no_whitespace() {
let input = "@media (max-width:60rem) { .content { width: 100%; } }";
let plans = plan(input, &[RuleId::ModernizeMediaRange]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(input, &plans, true).unwrap();
assert!(output.contains("(width <= 60rem)"));
}
#[test]
fn plans_continue_when_lightningcss_rejects_picker_pseudo() {
let css = r#".custom-select {
appearance: none;
}
.custom-select:hover {
border-color: teal;
}
.custom-select::picker(select) {
background: white;
}
.custom-select::picker(select)::-webkit-scrollbar {
width: 6px;
}
@media (max-width: 768px) {
.custom-select {
inline-size: 100%;
}
}
"#;
let report = analyze_source(PathBuf::from("picker.css"), css, &RuleId::ALL).unwrap();
eprintln!(
"parse_ok={} err={:?} plans={}",
report.parse_ok,
report.parse_error,
report.plans.len()
);
for p in &report.plans {
eprintln!(" {:?} {:?}", p.rules, p.reason);
}
assert!(
!report.plans.is_empty(),
"must still emit plans (parse_ok={:?} err={:?}): findings={:?}",
report.parse_ok,
report.parse_error,
report.findings
);
let output = apply_selected_plans(css, &report.plans, true).unwrap();
assert!(
output.contains("&:hover") || output.contains("&::picker"),
"custom-select relatives should nest: {output}"
);
assert!(
output.contains("(width <= 768px)") || output.contains("inline-size: 100%"),
"media-range or nest should still apply: {output}"
);
}
#[test]
fn nesting_adds_semicolon_to_last_declaration_without_semicolon() {
let css = r#".tabpanel {
display: block !important
}
.tabpanel+.tabpanel {
margin-block-start: .5rem
}
"#;
let rules = &[
RuleId::NestCombinator,
RuleId::NestDescendant,
RuleId::NestPseudoClass,
RuleId::NestPseudoElement,
];
let plans = plan(css, rules);
assert!(!plans.is_empty(), "expected at least one nesting plan");
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
!output.contains("!important+"),
"semicolon missing before nested rule: {output}"
);
assert!(
output.contains("!important;") || output.contains("!important\n"),
"declaration should end with ';': {output}"
);
assert!(
output.contains("+ .tabpanel {") || output.contains("+.tabpanel {"),
"nested combinator rule missing: {output}"
);
}
#[test]
fn gather_related_selector_rules_adds_semicolon_to_declarations_without_semicolon() {
let css = r#".skip-link {
position: absolute;
font-weight: 700
}
.unrelated { color: red }
.skip-link:focus {
outline: 2px solid currentColor
}
.skip-link+* {
display: block
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
assert!(!plans.is_empty(), "expected gather plan");
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
!output.contains("700\n &") && !output.contains("700&") && !output.contains("700{"),
"semicolon missing before nested pseudo/combinator: {output}"
);
assert!(
!output.contains("currentColor\n + *") && !output.contains("currentColor{"),
"semicolon missing before nested combinator: {output}"
);
assert!(
output.contains("font-weight: 700;"),
"missing ';' after font-weight: {output}"
);
assert!(
output.contains("position: absolute;"),
"missing ';' after position: {output}"
);
}
#[test]
fn gathers_non_adjacent_related_rule_inside_media_query() {
let css = r#".skip-link {
position: absolute;
font-weight: 700;
&:focus-visible {
inset-block-start: 0.75rem;
}
}
.unrelated {
color: red;
}
@media (width <= 1024px) {
.skip-link :not(*) {
position: inherit
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
assert!(
!plans.is_empty(),
"expected gather plan for media-wrapped related rule"
);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
!output.contains(".skip-link :not(*)"),
"flat media descendant should be gathered: {output}"
);
assert!(
output.contains(":not(*) {"),
"descendant :not(*) should nest under .skip-link: {output}"
);
assert!(
output.contains("@media (width <= 1024px) {"),
"media query should nest inside :not(*): {output}"
);
assert!(
output.contains("position: inherit"),
"declaration should be preserved: {output}"
);
let not_pos = output.find(":not(*) {").expect(":not(*)");
let media_pos = output.find("@media (width <= 1024px) {").expect("@media");
assert!(
media_pos > not_pos,
"media must nest inside :not(*), not the reverse: {output}"
);
}
#[test]
fn gathers_exact_parent_inside_media_as_nested_at_rule() {
let css = r#".skip-link {
position: absolute;
}
.unrelated { color: red }
@media (width <= 600px) {
.skip-link {
display: none
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains("@media (width <= 600px) {"), "{output}");
assert!(output.contains("display: none;"), "{output}");
assert!(
!output.contains("@media (width <= 600px) {\n .skip-link"),
"should invert to .skip-link {{ @media }}: {output}"
);
}
#[test]
fn gather_media_leaves_unrelated_siblings_in_place() {
let css = r#".skip-link {
position: absolute;
}
.unrelated { color: red }
@media (width <= 1024px) {
.skip-link :not(*) {
position: inherit
}
.other {
color: blue
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains(":not(*) {"), "{output}");
assert!(
output.contains(".other") && output.contains("color: blue"),
"unrelated sibling must stay in the media block: {output}"
);
}
#[test]
fn nests_appended_nesting_selector_descendant() {
let css = ".card {\n color: red;\n}\n.featured .card {\n border: 1px solid;\n}\n";
let plans = plan(css, &[RuleId::NestDescendant]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(
output.contains(".featured & {"),
"expected appended &: {output}"
);
assert!(output.contains("border: 1px solid;"), "{output}");
}
#[test]
fn nests_appended_nesting_selector_not() {
let css = ".card {\n color: red;\n}\n:not(.card) {\n display: none;\n}\n";
let plans = plan(css, &[RuleId::NestPseudoClass]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(output.contains(":not(&) {"), "expected :not(&): {output}");
assert!(output.contains("display: none;"), "{output}");
}
#[test]
fn nests_appended_compound_selector() {
let css = ".card {\n color: red;\n}\n.featured.card {\n font-weight: 700;\n}\n";
let plans = plan(css, &[RuleId::NestCompound]);
assert_eq!(plans.len(), 1);
let output = apply_selected_plans(css, &plans, false).unwrap();
assert!(
output.contains(".featured& {"),
"expected compound appended &: {output}"
);
}
#[test]
fn gathers_non_adjacent_appended_nesting_selector() {
let css = r#".card {
color: red;
}
.unrelated { color: blue }
.featured .card {
border: 1px solid
}
:not(.card) {
display: none
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
assert!(!plans.is_empty());
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains(".featured & {"), "{output}");
assert!(output.contains(":not(&) {"), "{output}");
assert!(!output.contains(".featured .card"), "{output}");
assert!(!output.contains(":not(.card)"), "{output}");
}
#[test]
fn gather_does_not_nest_selector_list_under_one_branch() {
let css = r#"::view-transition-old(root),
::view-transition-new(root) {
position: absolute;
inset: 0;
animation: 0.55s ease-in-out both;
}
.unrelated { color: red }
::view-transition-old(root) {
animation-name: fadeOut;
}
::view-transition-new(root) {
animation-name: fadeIn;
}
"#;
let plans = plan(
css,
&[
RuleId::GatherRelatedSelectorRules,
RuleId::FactorSelectorList,
],
);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
!output.contains("::view-transition-old(root),\n &")
&& !output.contains("::view-transition-old(root),\n & {")
&& !output.contains("::view-transition-old(root),\n &"),
"selector list must not nest under one branch: {output}"
);
assert!(
output.contains("::view-transition-old(root)")
&& output.contains("::view-transition-new(root)")
&& output.contains("animation-name: fadeOut")
&& output.contains("animation-name: fadeIn"),
"both branches and their unique decls must remain: {output}"
);
assert!(
output.contains("position: absolute"),
"shared declarations must be kept: {output}"
);
}
#[test]
fn gather_nests_not_focus_visible_as_non_relative_amp() {
assert!(selector_contains_nesting_amp(".skip-link:focus:not(&)"));
assert!(!selector_contains_nesting_amp(".skip-link:focus"));
let css = r#":focus-visible {
outline: 2px solid blue;
}
.unrelated { color: red }
/* Preserve visible focus for skip-link activation (any modality). */
.skip-link:focus:not(:focus-visible) {
outline: 2px solid blue;
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains(".skip-link:focus:not(&)"),
"should nest as non-relative :not(&): {output}"
);
assert!(
!output.contains(":focus-visible .skip-link")
&& !output.contains(":focus-visible .skip-link"),
"must not insert a descendant combinator: {output}"
);
assert!(
output.contains("Preserve visible focus for skip-link activation"),
"leading comment must move with the gathered rule: {output}"
);
let cmt = output.find("Preserve visible focus").expect("comment");
let nest = output.find(".skip-link:focus:not(&)").expect("nest");
assert!(
cmt < nest,
"comment should precede the nested rule: {output}"
);
}
#[test]
fn gather_nests_skip_link_focus_under_skip_link_not_focus_visible() {
let css = r#":focus-visible {
outline: 2px solid blue;
}
.skip-link {
position: fixed;
}
.unrelated { color: red }
.skip-link:focus:not(:focus-visible) {
outline: 2px solid blue;
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("&:focus:not(:focus-visible)"),
"should nest under .skip-link: {output}"
);
assert!(
!output.contains(".skip-link:focus:not(&)"),
"must not nest under :focus-visible: {output}"
);
}
#[test]
fn gather_merges_same_nested_selector_from_style_and_media() {
let css = r#".demo-out-text {
font-size: 1.5rem;
&.flash {
animation: demo-out-flash .4s ease;
}
}
.unrelated { color: red }
@media (prefers-reduced-motion: reduce) {
.demo-out-text.flash {
animation: none
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
let flash_opens = output.matches("&.flash {").count();
assert_eq!(
flash_opens, 1,
"duplicate &.flash nests should merge: {output}"
);
assert!(output.contains("animation: demo-out-flash"), "{output}");
assert!(
output.contains("@media (prefers-reduced-motion: reduce)"),
"{output}"
);
assert!(output.contains("animation: none"), "{output}");
}
#[test]
fn gather_prefers_existing_specific_home_over_universal_append() {
let css = r#".skip-link {
position: absolute;
font-weight: 700;
}
* {
margin: 0;
}
.unrelated { color: red }
.skip-link+* {
display: block
}
.skip-link:has(*) {
color: #27ca3f
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("+ * {") || output.contains("+* {"),
"combinator should nest under .skip-link: {output}"
);
assert!(
output.contains("&:has(*) {"),
":has(*) should nest under .skip-link: {output}"
);
assert!(
!output.contains(".skip-link+&")
&& !output.contains(".skip-link + &")
&& !output.contains(".skip-link:has(&)"),
"must not append the same rules into *: {output}"
);
let star = output.find("\n* {").or_else(|| output.find("* {"));
if let Some(star_at) = star {
let after_star = &output[star_at..];
let star_body = after_star.split('}').next().unwrap_or(after_star);
assert!(
!star_body.contains("skip-link"),
"* must not absorb skip-link rules: {output}"
);
}
}
#[test]
fn gather_appends_only_when_no_prefix_home_exists() {
let css = r#".card {
color: red;
}
.unrelated { color: blue }
.featured .card {
border: 1px solid
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(output.contains(".featured & {"), "{output}");
assert!(!output.contains(".featured .card"), "{output}");
}
#[test]
fn gather_keeps_supports_block_with_comma_list_and_mixed_homes() {
let css = r#".custom-select {
color: navy;
}
.unrelated { color: red }
@supports (appearance: base-select) {
.custom-select,
.custom-select::picker(select) {
appearance: base-select;
}
.custom-select button { display: flex }
selectedcontent { display: flex }
.custom-select option { padding: 1rem }
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("appearance: base-select"),
"comma-list body must not be dropped: {output}"
);
assert!(
output.contains("selectedcontent") && output.contains(".custom-select button")
|| output.contains("button {"),
"mixed @supports inners must remain: {output}"
);
}
#[test]
fn gather_keeps_busy_mixed_media_grouped() {
let css = r#".nav-links { display: flex; }
.hamburger-menu { display: none; }
.nav-controls { gap: 1rem; }
.unrelated { color: red }
@media (width <= 1024px) {
.nav-links { display: none }
.hamburger-menu { display: flex }
.nav-controls { margin-inline-start: auto }
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("@media (width <= 1024px)"),
"mixed media with 3+ selectors should stay grouped: {output}"
);
assert!(
!output.contains(".nav-links {\n display: flex;\n\n @media")
&& !output.contains(".hamburger-menu {\n display: none;\n\n @media"),
"should not explode a busy media query into each parent: {output}"
);
}
#[test]
fn appended_nesting_does_not_match_ident_suffix() {
let css = ".card {\n color: red;\n}\n.mycard {\n color: blue;\n}\n";
let plans = plan(css, &[RuleId::NestCompound, RuleId::NestDescendant]);
assert!(
plans.is_empty(),
".mycard must not nest under .card: {:?}",
plans.iter().map(|p| &p.proposed).collect::<Vec<_>>()
);
}
#[test]
fn gather_preserves_nested_selector_lists() {
let css = r#"* {
margin: 0;
}
.unrelated { color: red }
@media (prefers-reduced-motion: reduce) {
* {
&,
&::before,
&::after {
animation-duration: 0.001ms !important
}
}
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
!output.contains("&::before,\n ;")
&& !output.contains("&::before,;")
&& !output.contains("&,\n ;")
&& !output.contains("&,;"),
"selector-list comma must not become a declaration terminator: {output}"
);
assert!(
output.contains("&::before,") && output.contains("&::after {"),
"compound pseudo selector list must stay intact: {output}"
);
let before = output.find("&::before,").expect("before");
let after = output.find("&::after {").expect("after");
let between = &output[before..after];
assert!(
!between.contains(';'),
"no semicolon between selector-list items: {between:?} in {output}"
);
}
#[test]
fn gather_keeps_every_custom_select_chrome_rule() {
let css = r#".custom-select {
color: navy;
}
@supports (appearance: base-select) {
.custom-select,
.custom-select::picker(select) {
appearance: base-select;
}
.custom-select button {
display: flex;
}
.custom-select button:hover {
border-color: teal;
}
.custom-select:focus button,
.custom-select button:focus-visible {
outline: none;
}
.custom-select .select-arrow {
color: gray;
}
.custom-select:open .select-arrow {
rotate: -180deg;
}
.custom-select:open button {
border-color: teal;
}
selectedcontent {
display: flex;
}
selectedcontent .opt-icon {
display: none;
}
.custom-select::picker(select) {
background: white;
}
.custom-select:not(:open)::picker(select) {
opacity: 0;
}
.custom-select option {
padding: 1rem;
}
.custom-select .dropdown-search-container {
position: sticky;
}
.custom-select .select-search {
inline-size: 100%;
}
.custom-select option .opt-icon {
color: gray;
}
.custom-select option:hover .opt-icon,
.custom-select option:checked .opt-icon {
color: teal;
}
.custom-select::picker-icon {
display: none;
}
}
"#;
let rules: Vec<RuleId> = RuleId::ALL
.iter()
.copied()
.filter(|r| *r != RuleId::ModernizeWhere)
.collect();
let plans = plan(css, &rules);
let output = apply_selected_plans(css, &plans, true).unwrap();
for needle in [
"appearance: base-select",
"display: flex",
"border-color: teal",
"outline: none",
"color: gray",
"rotate: -180deg",
"selectedcontent",
"display: none",
"background: white",
"opacity: 0",
"padding: 1rem",
"position: sticky",
"inline-size: 100%",
"::picker-icon",
] {
assert!(
output.contains(needle),
"lost `{needle}` after apply:\n{output}"
);
}
assert!(
output.contains("button") && output.contains("&:hover"),
"button:hover should nest as button {{ &:hover }}:\n{output}"
);
assert!(
!output.contains("&:open &") && !output.contains("&:focus &"),
"must not rewrite :open/:focus descendants as appended &:\n{output}"
);
assert!(
output.contains("&:open")
&& (output.contains("&:open {")
|| output.contains("&:open .select-arrow")
|| output.contains("&:open button")),
":open children should group under .custom-select:\n{output}"
);
assert!(
output.contains("&:focus button") || output.contains("button:focus-visible"),
"focus comma-list must stay as relative branches:\n{output}"
);
assert!(
output.contains("selectedcontent")
&& (output.contains(".opt-icon") || output.contains("& .opt-icon")),
"selectedcontent leftover must remain:\n{output}"
);
assert!(
output.contains(".custom-select")
&& output.contains("@supports (appearance: base-select)"),
"supports should invert under .custom-select:\n{output}"
);
let home = output.find(".custom-select {").expect("home");
let supports = output
.find("@supports (appearance: base-select)")
.expect("supports");
assert!(
supports > home,
"appearance supports should sit inside .custom-select:\n{output}"
);
assert!(
!output.contains("@supports (appearance: base-select) {\n .custom-select"),
"must not leave a wrapper .custom-select inside @supports:\n{output}"
);
}
#[test]
fn gather_folds_unlayered_picker_into_layered_home() {
let css = r#"@layer components {
.custom-select {
color: navy;
&:is(:hover) { color: teal; }
}
}
.custom-select::picker(select) {
scrollbar-width: thin;
}
.custom-select::picker(select)::-webkit-scrollbar {
width: 6px;
}
"#;
let plans = plan(
css,
&[
RuleId::GatherRelatedSelectorRules,
RuleId::NestLayerBySelector,
],
);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("@layer components") && output.contains(".custom-select"),
"layered home must stay in its layer:\n{output}"
);
assert!(
output.contains(".custom-select::picker(select)")
|| output.contains("&::picker(select)"),
"picker chrome must remain:\n{output}"
);
let layer_at = output.find("@layer components").expect("layer");
let picker_flat = output.find(".custom-select::picker(select)");
if let Some(picker_at) = picker_flat {
assert!(
picker_at < layer_at || !output[layer_at..].contains("scrollbar-width"),
"unlayered picker must not be dumped into @layer components:\n{output}"
);
}
}
#[test]
fn gather_does_not_dump_root_from_base_into_tokens() {
let css = r#"@layer tokens {
:root {
color-scheme: light dark;
}
}
@layer base {
:root {
scroll-behavior: smooth;
}
body { margin: 0; }
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("@layer tokens") && output.contains("@layer base"),
"both layers must remain:\n{output}"
);
let tokens = output.split("@layer base").next().unwrap_or(&output);
assert!(
!tokens.contains("scroll-behavior"),
"base :root decls must not move into tokens:\n{output}"
);
assert!(
output.contains("scroll-behavior: smooth"),
"base :root decls must survive:\n{output}"
);
}
#[test]
fn nest_layer_by_selector_preserves_layer_identity() {
let css = r#"@layer tokens {
:root {
color-scheme: light dark;
}
}
@layer base {
:root {
scroll-behavior: smooth;
}
body { margin: 0; }
}
"#;
let plans = plan(css, &[RuleId::NestLayerBySelector]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains(":root {")
&& output.contains("@layer tokens {")
&& output.contains("@layer base {"),
"should hoist :root and nest named layers:\n{output}"
);
assert!(
output.contains("color-scheme: light dark")
&& output.contains("scroll-behavior: smooth")
&& output.contains("body"),
"all declarations must survive:\n{output}"
);
assert!(
!output.contains("@layer tokens {\n :root")
|| output.find(":root {").unwrap() < output.find("@layer tokens {").unwrap_or(0)
|| output.matches("@layer tokens").count() >= 1,
"{output}"
);
let root_at = output.find(":root {").expect("root");
let tokens_inner = output[root_at..]
.find("@layer tokens {")
.expect("nested tokens");
let base_inner = output[root_at..]
.find("@layer base {")
.expect("nested base");
assert!(
tokens_inner < base_inner,
"layer order tokens then base must be preserved:\n{output}"
);
assert!(
output.contains("body {") || output.contains("body{"),
"unrelated base rules stay in @layer base:\n{output}"
);
}
#[test]
fn nest_layer_blocks_are_not_factored_into_anonymous_layer() {
let css = r#":root {
@layer tokens {
color-scheme: light dark;
}
@layer base {
scroll-behavior: smooth;
}
}
:root {
scrollbar-width: thin;
}
"#;
let plans = plan(
css,
&[
RuleId::GatherRelatedSelectorRules,
RuleId::NestLayerBySelector,
],
);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("@layer tokens") && output.contains("@layer base"),
"named layers must stay named:\n{output}"
);
assert!(
!output.contains("@layer {\n tokens")
&& !output.contains("@layer {\n tokens")
&& !output.contains("@layer {\n base"),
"must not wrap layer names as type selectors:\n{output}"
);
assert!(
output.contains("color-scheme: light dark")
&& output.contains("scroll-behavior: smooth")
&& output.contains("scrollbar-width: thin"),
"declarations must survive:\n{output}"
);
}
#[test]
fn nest_layer_skips_when_unlayered_rule_intervenes() {
let css = r#"@layer base {
.container { color: red; }
}
.some-rule { color: blue; }
@layer layout {
.container { color: green; }
}
"#;
let plans = plan(css, &[RuleId::NestLayerBySelector]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("@layer base")
&& output.contains("@layer layout")
&& output.contains(".some-rule"),
"intervening unlayered rule blocks the hoist:\n{output}"
);
assert!(
!output.contains(".container {\n @layer base"),
"must not move layout across .some-rule:\n{output}"
);
}
#[test]
fn gather_factors_compact_descendants_under_option_card() {
let css = r#".option-card.compact .option-label {
padding: 1rem;
}
.option-card.compact .option-icon {
inline-size: 24px;
}
.unrelated { color: red }
.option-card {
position: relative;
cursor: pointer;
}
"#;
let plans = plan(css, &[RuleId::GatherRelatedSelectorRules]);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("&.compact")
&& output.contains(".option-label")
&& output.contains(".option-icon"),
"compact descendants should nest under .option-card:\n{output}"
);
assert!(
output.contains("&.compact {")
&& output.contains(".option-label {")
&& output.contains(".option-icon {"),
"shared &.compact prefix should be factored:\n{output}"
);
assert!(output.contains("position: relative"), "{output}");
assert!(output.contains("padding: 1rem"), "{output}");
assert!(output.contains("inline-size: 24px"), "{output}");
}
#[test]
fn gather_does_not_orphan_deletes_when_shorter_nest_wins() {
let css = r#".custom-select button {
display: flex;
}
.custom-select button:hover {
color: red;
}
.unrelated { color: blue }
.custom-select::picker(select) {
background: white;
}
.custom-select::picker-icon {
display: none;
}
"#;
let rules = &[
RuleId::NestPseudoClass,
RuleId::NestDescendant,
RuleId::GatherRelatedSelectorRules,
];
let plans = plan(css, rules);
let output = apply_selected_plans(css, &plans, true).unwrap();
assert!(
output.contains("background: white") && output.contains("display: none"),
"picker chrome must survive mixed nest+gather:\n{output}"
);
assert!(
output.contains("display: flex") && output.contains("color: red"),
"button rules must survive:\n{output}"
);
}
}