use oxc_css_parser::pos::Span;
use crate::doc::{Body, Item, selector_lines};
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Sel {
pub(crate) parts: Vec<Part>,
pub(crate) line_break: bool,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub(crate) struct Part {
pub(crate) combinator: Option<char>,
pub(crate) compound: Compound,
}
#[derive(Clone, Debug, PartialEq, Eq, Default)]
pub(crate) struct Compound {
pub(crate) simples: Vec<String>,
}
impl Compound {
pub(crate) fn render(&self) -> String {
self.simples.concat()
}
fn has_placeholder(&self) -> bool {
self.simples.iter().any(|s| s.starts_with('%'))
}
}
impl Sel {
pub(crate) fn render(&self) -> String {
let mut out = String::new();
for (i, part) in self.parts.iter().enumerate() {
if i > 0 {
match part.combinator {
Some(c) => {
out.push(' ');
out.push(c);
out.push(' ');
}
None => out.push(' '),
}
} else if let Some(c) = part.combinator {
out.push(c);
out.push(' ');
}
out.push_str(&part.compound.render());
}
out
}
fn single_compound(&self) -> Option<&Compound> {
match self.parts.as_slice() {
[part] if part.combinator.is_none() => Some(&part.compound),
_ => None,
}
}
}
#[derive(Clone)]
pub(crate) struct Extension {
pub(crate) extenders: Vec<Sel>,
pub(crate) target: Compound,
pub(crate) optional: bool,
pub(crate) span: usize,
}
enum Event {
Rule,
Extend(Extension),
}
#[derive(Default)]
pub(crate) struct Engine {
pub(crate) lists: Vec<Vec<Sel>>,
events: Vec<Event>,
registered: usize,
known: std::collections::HashSet<String>,
ext_list: Vec<Extension>,
pub(crate) dirty: Vec<bool>,
}
impl Engine {
pub(crate) fn new() -> Self {
Self::default()
}
pub(crate) fn add_rule(&mut self, mut selectors: Vec<Sel>) -> usize {
let table = self.table();
let mut dirty = false;
if !table.is_empty() {
if let Some(extended) = extend_list(&selectors, &table) {
selectors = extended;
dirty = true;
}
}
for sel in &selectors {
for part in &sel.parts {
for simple in &part.compound.simples {
self.known.insert(simple.clone());
}
}
}
let id = self.lists.len();
self.lists.push(selectors);
self.dirty.push(dirty);
self.events.push(Event::Rule);
self.registered += 1;
id
}
pub(crate) fn note_selectors(&mut self, selectors: &[Sel]) {
for sel in selectors {
for part in &sel.parts {
for simple in &part.compound.simples {
self.known.insert(simple.clone());
}
}
}
}
pub(crate) fn add_extension(&mut self, mut extension: Extension) {
let table = self.table();
if !table.is_empty() {
if let Some(extended) = extend_list(&extension.extenders, &table) {
extension.extenders = extended;
}
}
self.ext_list.push(extension.clone());
self.events.push(Event::Extend(extension));
}
fn table(&self) -> Vec<(Compound, Extension)> {
self.ext_list
.iter()
.map(|extension| (extension.target.clone(), extension.clone()))
.collect()
}
pub(crate) fn is_empty(&self) -> bool {
self.events.is_empty()
}
pub(crate) fn resolve(&mut self) {
let mut registered = 0usize;
for index in 0..self.events.len() {
match &self.events[index] {
Event::Rule => registered += 1,
Event::Extend(extension) => {
let extension = extension.clone();
let table = [(extension.target.clone(), extension.clone())];
for id in 0..registered {
let mut changed = false;
let updated = extend_list(&self.lists[id], &table);
if let Some(updated) = updated {
self.lists[id] = updated;
if let Some(flag) = self.dirty.get_mut(id) {
*flag = true;
}
changed = true;
}
let _ = changed;
}
}
}
let _ = index;
}
}
pub(crate) fn debug_lists(&self) {
for (i, list) in self.lists.iter().enumerate() {
eprintln!(
"list {i}: {:?}",
list.iter()
.map(|s| (
s.render(),
s.parts.iter().map(|p| p.combinator).collect::<Vec<_>>()
))
.collect::<Vec<_>>()
);
}
}
pub(crate) fn unmatched(&self) -> Option<usize> {
self.events.iter().find_map(|event| match event {
Event::Extend(extension)
if !extension.optional
&& !extension
.target
.simples
.iter()
.all(|simple| self.known.contains(simple)) =>
{
Some(extension.span)
}
_ => None,
})
}
}
#[derive(Clone)]
struct Produced {
sel: Sel,
fresh: bool,
origin: Option<String>,
}
impl Produced {
fn plain(sel: Sel) -> Self {
Self {
sel,
fresh: false,
origin: None,
}
}
}
fn extend_list(list: &[Sel], table: &[(Compound, Extension)]) -> Option<Vec<Sel>> {
let mut out: Vec<Produced> = Vec::with_capacity(list.len());
let mut changed = false;
for sel in list {
match extend_sel(sel, table) {
Some(result) => {
changed = true;
out.extend(result);
}
None => out.push(Produced {
sel: sel.clone(),
fresh: false,
origin: Some(sel.render()),
}),
}
}
changed.then(|| trim(out))
}
fn subsumes(wide: &Sel, narrow: &Sel) -> bool {
if wide.parts.len() != narrow.parts.len() {
return false;
}
wide.parts.iter().zip(&narrow.parts).all(|(a, b)| {
(a.combinator == b.combinator || a.combinator.is_none())
&& a.compound
.simples
.iter()
.all(|s| b.compound.simples.contains(s))
})
}
fn extend_sel(sel: &Sel, table: &[(Compound, Extension)]) -> Option<Vec<Produced>> {
let mut options: Option<Vec<Vec<Produced>>> = None;
for (i, part) in sel.parts.iter().enumerate() {
let extended = extend_compound(&part.compound, table);
match extended {
None => {
if let Some(options) = options.as_mut() {
options.push(vec![Produced::plain(Sel {
parts: vec![part.clone()],
line_break: sel.line_break,
})]);
}
}
Some(mut alternatives) => {
if options.is_none() {
let mut initial = Vec::new();
if i != 0 {
initial.push(Produced::plain(prefix_sel(&sel.parts[..i], sel.line_break)));
}
options = Some(if initial.is_empty() {
Vec::new()
} else {
vec![initial]
});
}
for alternative in &mut alternatives {
if alternative.sel.parts[0].combinator.is_none() {
alternative.sel.parts[0].combinator = part.combinator;
}
}
options
.as_mut()
.expect("just initialized")
.push(alternatives);
}
}
}
let options = options?;
let mut out = Vec::new();
for path in paths(&options) {
let origins = path
.iter()
.filter_map(|produced| produced.origin.clone())
.collect::<Vec<_>>();
let sels = path
.iter()
.map(|produced| produced.sel.clone())
.collect::<Vec<_>>();
let fresh = !origins.is_empty();
let origin = (origins.len() == 1).then(|| origins[0].clone());
for woven in weave(&sels, sel.line_break) {
out.push(Produced {
sel: woven,
fresh,
origin: origin.clone(),
});
}
}
Some(out)
}
fn prefix_sel(parts: &[Part], line_break: bool) -> Sel {
Sel {
parts: parts.to_vec(),
line_break,
}
}
fn extend_compound(compound: &Compound, table: &[(Compound, Extension)]) -> Option<Vec<Produced>> {
let mut options: Option<Vec<Vec<Alternative>>> = None;
for (i, simple) in compound.simples.iter().enumerate() {
let keep = single_compound_sel(Compound {
simples: vec![simple.clone()],
});
let extenders = extenders_for(simple, table);
match extenders {
None => {
if let Some(options) = options.as_mut() {
options.push(vec![(None, keep)]);
}
}
Some(list) => {
if options.is_none() {
let mut initial = Vec::new();
if i != 0 {
initial.push((
None,
single_compound_sel(Compound {
simples: compound.simples[..i].to_vec(),
}),
));
}
options = Some(if initial.is_empty() {
Vec::new()
} else {
vec![initial]
});
}
let position = options.as_mut().expect("just initialized");
let mut slot = vec![(None, keep)];
slot.extend(list.into_iter().map(|sel| (Some(sel.clone()), sel)));
position.push(slot);
}
}
}
let options = options?;
let mut out = Vec::new();
for path in paths_tagged(&options) {
let mut base: Vec<String> = Vec::new();
let mut origins: Vec<Sel> = Vec::new();
let mut extenders: Vec<Sel> = Vec::new();
for (origin, sel) in &path {
if let Some(origin) = origin {
origins.push(origin.clone());
extenders.push(sel.clone());
continue;
}
let compound = sel.single_compound()?;
base.extend(compound.simples.iter().cloned());
}
let fresh = !origins.is_empty();
let origin = (origins.len() == 1).then(|| origins[0].render());
let line_break = origins.iter().any(|sel| sel.line_break);
if extenders.is_empty() {
out.push(Produced {
sel: single_compound_sel(Compound { simples: base }),
fresh,
origin,
});
continue;
}
if extenders.iter().all(|sel| sel.single_compound().is_some()) {
let mut merged = Compound { simples: base };
if extenders
.iter()
.filter_map(Sel::single_compound)
.all(|compound| merge_compound(&mut merged, compound))
{
let mut sel = single_compound_sel(merged);
sel.line_break = line_break;
out.push(Produced { sel, fresh, origin });
}
continue;
}
let mut simple = Compound { simples: base };
if !extenders
.iter()
.filter_map(Sel::single_compound)
.all(|compound| merge_compound(&mut simple, compound))
{
continue;
}
for sel in unify_extenders(&simple, &extenders) {
out.push(Produced {
sel,
fresh: true,
origin: None,
});
}
}
Some(out)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum SimpleKind {
Type,
PseudoElement,
PseudoClass,
Other,
}
impl SimpleKind {
fn of(simple: &str) -> Self {
if simple.starts_with("::") {
return Self::PseudoElement;
}
if let Some(rest) = simple.strip_prefix(':') {
let name = rest.split('(').next().unwrap_or(rest).to_ascii_lowercase();
return if matches!(
name.as_str(),
"before" | "after" | "first-line" | "first-letter"
) {
Self::PseudoElement
} else {
Self::PseudoClass
};
}
if simple
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '*' || c == '|')
{
Self::Type
} else {
Self::Other
}
}
}
fn unify_simple(merged: &mut Vec<String>, simple: &str) -> bool {
match SimpleKind::of(simple) {
SimpleKind::Type if simple == "*" => {
if merged.is_empty() {
merged.push(simple.to_string());
}
true
}
SimpleKind::Type => match merged.first() {
Some(first) if SimpleKind::of(first) == SimpleKind::Type => first == simple,
_ => {
merged.insert(0, simple.to_string());
true
}
},
kind => {
if merged.iter().any(|s| s == simple) {
return true;
}
let first_pseudo = |want: SimpleKind, merged: &[String]| {
merged.iter().position(|s| SimpleKind::of(s) == want)
};
let at = match kind {
SimpleKind::PseudoElement => {
if merged
.iter()
.any(|s| SimpleKind::of(s) == SimpleKind::PseudoElement)
{
return false;
}
merged.len()
}
SimpleKind::PseudoClass => {
first_pseudo(SimpleKind::PseudoElement, merged).unwrap_or(merged.len())
}
_ => merged
.iter()
.position(|s| {
matches!(
SimpleKind::of(s),
SimpleKind::PseudoClass | SimpleKind::PseudoElement
)
})
.unwrap_or(merged.len()),
};
merged.insert(at, simple.to_string());
true
}
}
}
fn merge_compound(merged: &mut Compound, extender: &Compound) -> bool {
if merged.simples.is_empty() {
merged.simples.clone_from(&extender.simples);
return true;
}
let mut pseudo_element_found = false;
let mut pseudo: Vec<String> = Vec::new();
for simple in &extender.simples {
let kind = SimpleKind::of(simple);
let target = if pseudo_element_found
&& matches!(kind, SimpleKind::PseudoClass | SimpleKind::PseudoElement)
{
&mut pseudo
} else {
pseudo_element_found |= kind == SimpleKind::PseudoElement;
&mut merged.simples
};
if !unify_simple(target, simple) {
return false;
}
}
merged.simples.extend(pseudo);
true
}
fn single_compound_sel(compound: Compound) -> Sel {
Sel {
parts: vec![Part {
combinator: None,
compound,
}],
line_break: false,
}
}
fn extenders_for(simple: &str, table: &[(Compound, Extension)]) -> Option<Vec<Sel>> {
let mut hit = false;
let mut out = Vec::new();
for (target, extension) in table {
if !target.simples.iter().any(|s| s == simple) {
continue;
}
hit = true;
out.extend(extension.extenders.iter().cloned());
}
if hit { Some(out) } else { None }
}
fn paths(options: &[Vec<Produced>]) -> Vec<Vec<Produced>> {
let mut acc: Vec<Vec<Produced>> = vec![Vec::new()];
for position in options {
let mut next = Vec::with_capacity(acc.len() * position.len());
for option in position {
for path in &acc {
let mut merged = path.clone();
merged.push(option.clone());
next.push(merged);
}
}
acc = next;
}
acc
}
type Alternative = (Option<Sel>, Sel);
fn paths_tagged(options: &[Vec<Alternative>]) -> Vec<Vec<Alternative>> {
let mut acc: Vec<Vec<Alternative>> = vec![Vec::new()];
for position in options {
let mut next = Vec::with_capacity(acc.len() * position.len());
for option in position {
for path in &acc {
let mut merged = path.clone();
merged.push(option.clone());
next.push(merged);
}
}
acc = next;
}
acc
}
fn unify_extenders(originals: &Compound, extenders: &[Sel]) -> Vec<Sel> {
let complexes: Vec<&Sel> = extenders
.iter()
.filter(|sel| sel.single_compound().is_none())
.collect();
let mut out = Vec::new();
for base in &complexes {
let mut merged = (*base).clone();
let last = merged.parts.last_mut().expect("multiple segments");
let mut compound = originals.clone();
if !merge_compound(&mut compound, &last.compound) {
continue;
}
last.compound = compound;
out.push(merged);
}
out
}
fn weave(path: &[Sel], line_break: bool) -> Vec<Sel> {
let mut iter = path.iter();
let Some(first) = iter.next() else {
return Vec::new();
};
let mut prefixes = vec![first.clone()];
for complex in iter {
let mut next = Vec::new();
for prefix in &prefixes {
if complex.parts.len() == 1 {
let mut merged = prefix.clone();
merged.line_break |= complex.line_break | line_break;
merged.parts.push(complex.parts[0].clone());
next.push(merged);
continue;
}
if complex.parts.len() > 1 && prefix.parts.last() == complex.parts.first() {
let mut merged = prefix.clone();
merged.line_break |= complex.line_break;
merged.parts.extend(complex.parts[1..].iter().cloned());
next.push(merged);
continue;
}
let shared = prefix
.parts
.iter()
.zip(complex.parts.iter())
.take_while(|(a, b)| a == b)
.count();
let mut merged = Sel {
parts: prefix.parts[..shared].to_vec(),
line_break: prefix.line_break || complex.line_break || line_break,
};
merged.parts.extend(prefix.parts[shared..].iter().cloned());
merged.parts.extend(complex.parts[shared..].iter().cloned());
next.push(merged);
let target = complex.parts.len() - 1;
if shared < target
&& shared < prefix.parts.len()
&& prefix.parts[shared..]
.iter()
.chain(&complex.parts[shared..target])
.all(|part| part.combinator.is_none())
{
let mut alternated = Sel {
parts: prefix.parts[..shared].to_vec(),
line_break: prefix.line_break || complex.line_break || line_break,
};
alternated
.parts
.extend(complex.parts[shared..target].iter().cloned());
alternated
.parts
.extend(prefix.parts[shared..].iter().cloned());
alternated
.parts
.extend(complex.parts[target..].iter().cloned());
next.push(alternated);
}
}
prefixes = next;
}
for sel in &mut prefixes {
sel.line_break |= line_break;
}
prefixes
}
fn trim(produced: Vec<Produced>) -> Vec<Sel> {
let mut drop_early = vec![false; produced.len()];
for i in 0..produced.len() {
let item = &produced[i];
drop_early[i] = produced[..i].iter().any(|kept| {
kept.sel == item.sel
|| (item.fresh && kept.origin == item.origin && subsumes(&kept.sel, &item.sel))
}) || (item.fresh
&& produced.iter().enumerate().any(|(j, other)| {
j != i && other.origin == item.origin && subsumes(&other.sel, &item.sel)
}));
}
let mut out: Vec<&Produced> = Vec::new();
for (i, item) in produced.iter().enumerate().rev() {
if drop_early[i] {
continue;
}
if out.iter().any(|kept| covers(&kept.sel, &item.sel)) {
continue;
}
out.retain(|kept| !covers(&item.sel, &kept.sel) || kept.sel.parts != item.sel.parts);
out.push(item);
}
out.reverse();
out.into_iter().map(|item| item.sel.clone()).collect()
}
fn covers(wide: &Sel, narrow: &Sel) -> bool {
wide.parts.len() == narrow.parts.len()
&& wide.parts.iter().zip(&narrow.parts).all(|(a, b)| {
a.compound == b.compound && (a.combinator == b.combinator || a.combinator.is_none())
})
}
pub(crate) fn parse_sel(text: &str, line_break: bool) -> Option<Sel> {
let bytes = text.as_bytes();
let mut parts: Vec<Part> = Vec::new();
let mut i = 0usize;
let mut combinator: Option<char> = None;
while i < bytes.len() {
let c = bytes[i] as char;
if c.is_ascii_whitespace() {
let mut j = i;
while j < bytes.len() && (bytes[j] as char).is_ascii_whitespace() {
j += 1;
}
if j < bytes.len() && matches!(bytes[j] as char, '>' | '+' | '~') {
i = j;
continue;
}
if j < bytes.len() && combinator.is_none() {
combinator = Some(' ');
}
i = j;
continue;
}
if matches!(c, '>' | '+' | '~') {
combinator = Some(c);
i += 1;
continue;
}
let end = compound_end(text, i);
let simple_texts = split_simples(&text[i..end]);
if simple_texts.is_empty() {
return None;
}
parts.push(Part {
combinator: combinator.filter(|c| *c != ' '),
compound: Compound {
simples: simple_texts,
},
});
combinator = None;
i = end;
}
if parts.is_empty() {
None
} else {
Some(Sel { parts, line_break })
}
}
fn compound_end(text: &str, start: usize) -> usize {
let bytes = text.as_bytes();
let mut i = start;
let mut bracket = 0usize;
let mut paren = 0usize;
let mut quote: Option<u8> = None;
while i < bytes.len() {
let c = bytes[i];
match quote {
Some(q) => {
if c == b'\\' {
i += 1;
} else if c == q {
quote = None;
}
}
None => match c {
b'"' | b'\'' => quote = Some(c),
b'[' => bracket += 1,
b']' => bracket = bracket.saturating_sub(1),
b'(' => paren += 1,
b')' => paren = paren.saturating_sub(1),
b'\\' => i += 1,
_ if bracket == 0 && paren == 0 => {
let ch = c as char;
if ch.is_ascii_whitespace() || matches!(ch, '>' | '+' | '~') {
return i;
}
}
_ => {}
},
}
i += 1;
}
text.len()
}
fn split_simples(text: &str) -> Vec<String> {
let bytes = text.as_bytes();
let mut out = Vec::new();
let mut i = 0usize;
while i < bytes.len() {
let start = i;
match bytes[i] {
b'[' => {
let mut depth = 0usize;
let mut quote: Option<u8> = None;
while i < bytes.len() {
let b = bytes[i];
match quote {
Some(q) => {
if b == b'\\' {
i += 1;
} else if b == q {
quote = None;
}
}
None => match b {
b'"' | b'\'' => quote = Some(b),
b'[' => depth += 1,
b']' => {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
_ => {}
},
}
i += 1;
}
}
b':' => {
i += 1;
if bytes.get(i) == Some(&b':') {
i += 1;
}
i = consume_ident(bytes, i);
if bytes.get(i) == Some(&b'(') {
let mut depth = 0usize;
while i < bytes.len() {
match bytes[i] {
b'(' => depth += 1,
b')' => {
depth -= 1;
if depth == 0 {
i += 1;
break;
}
}
b'"' | b'\'' => {
let q = bytes[i];
i += 1;
while i < bytes.len() && bytes[i] != q {
if bytes[i] == b'\\' {
i += 1;
}
i += 1;
}
}
_ => {}
}
i += 1;
}
}
}
b'.' | b'#' | b'%' => {
i += 1;
i = consume_ident(bytes, i);
}
_ => {
let next = consume_ident(bytes, i);
if next == i {
i += 1;
} else {
i = next;
if bytes.get(i) == Some(&b'|') {
i += 1;
if bytes.get(i) == Some(&b'*') {
i += 1;
} else {
i = consume_ident(bytes, i);
}
}
}
}
}
out.push(text[start..i].to_string());
}
out
}
fn consume_ident(bytes: &[u8], mut i: usize) -> usize {
while i < bytes.len() {
let ch = bytes[i] as char;
if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_') {
i += 1;
} else if ch == '\\' {
i += 2;
} else {
break;
}
}
i
}
pub(crate) fn target_compound(text: &str) -> Option<Compound> {
let sel = parse_sel(text, false)?;
match sel.parts.as_slice() {
[part] if part.combinator.is_none() => Some(part.compound.clone()),
_ => None,
}
}
pub(crate) fn apply(items: &mut Vec<Item>, lists: &[Vec<Sel>], dirty: &[bool]) {
for item in items.iter_mut() {
match item {
Item::Rule(rule) => {
if let Some(id) = rule.extend_id {
if let Some(selectors) = lists.get(id) {
let has_placeholder = |sels: &[Sel]| {
sels.iter()
.any(|sel| sel.parts.iter().any(|p| p.compound.has_placeholder()))
};
if dirty.get(id).copied() == Some(true) || has_placeholder(selectors) {
let kept = selectors
.iter()
.filter(|sel| {
!sel.parts.iter().any(|p| p.compound.has_placeholder())
})
.collect::<Vec<_>>();
let span = rule
.selector
.first()
.map_or(Span { start: 0, end: 0 }, |(_, span)| *span);
rule.selector = selector_lines(&render_list(&kept), span);
rule.placeholder = kept.is_empty();
}
}
}
}
Item::AtRule(at) => {
if let Some(Body::Rules(nested)) = at.body.as_mut() {
apply(nested, lists, dirty);
}
}
Item::Keyframe(_) | Item::Comment(_) | Item::Raw(_) => {}
}
}
items.retain(|item| {
!matches!(item, Item::AtRule(at)
if matches!(&at.body, Some(Body::Rules(rules)) if rules.is_empty()))
});
items.retain(|item| !matches!(item, Item::Rule(rule) if rule.placeholder));
}
fn render_list(selectors: &[&Sel]) -> String {
let mut out = String::new();
for sel in selectors {
if !out.is_empty() {
out.push(',');
out.push(if sel.line_break { '\n' } else { ' ' });
}
out.push_str(&sel.render());
}
out
}