use rustc_hash::FxHashMap;
use std::cmp::Ordering;
use selectors::bloom::BloomFilter;
use selectors::context::{MatchingContext, QuirksMode, SelectorCaches};
use selectors::parser::{AncestorHashes, Component, Selector};
use super::declaration::Declaration;
use super::parse::{CssRule, Origin, Specificity, Stylesheet};
use super::style_pool::StylePool;
use super::types::ComputedStyle;
use crate::dom::element_ref::{BokoSelectors, ElementRef};
#[derive(Debug)]
struct MatchedDecl {
sheet: u32,
rule: u32,
decl: u32,
origin: Origin,
specificity: Specificity,
order: u32,
important: bool,
}
fn inherit_from_parent(parent: &ComputedStyle) -> ComputedStyle {
ComputedStyle {
font_size: parent.font_size,
font_weight: parent.font_weight,
font_style: parent.font_style,
font_variant: parent.font_variant,
font_family: parent.font_family.clone(),
color: parent.color,
text_align: parent.text_align,
text_indent: parent.text_indent,
line_height: parent.line_height,
letter_spacing: parent.letter_spacing,
word_spacing: parent.word_spacing,
text_transform: parent.text_transform,
hyphens: parent.hyphens,
text_decoration_underline: parent.text_decoration_underline,
text_decoration_line_through: parent.text_decoration_line_through,
underline_style: parent.underline_style,
underline_color: parent.underline_color,
overline: parent.overline,
list_style_type: parent.list_style_type,
list_style_position: parent.list_style_position,
visibility: parent.visibility,
language: parent.language.clone(),
..ComputedStyle::default()
}
}
type RuleRef = (u32, u32);
enum BucketKey {
Id(String),
Class(String),
Local(String),
Universal,
}
fn selector_bucket_key(selector: &Selector<BokoSelectors>) -> BucketKey {
let mut id: Option<String> = None;
let mut class: Option<String> = None;
let mut local: Option<String> = None;
for component in selector.iter() {
match component {
Component::ID(v) if id.is_none() => id = Some(v.0.clone()),
Component::Class(v) if class.is_none() => class = Some(v.0.clone()),
Component::LocalName(name) if local.is_none() => {
local = Some(name.lower_name.as_ref().to_ascii_lowercase());
}
_ => {}
}
}
if let Some(id) = id {
BucketKey::Id(id)
} else if let Some(class) = class {
BucketKey::Class(class)
} else if let Some(local) = local {
BucketKey::Local(local)
} else {
BucketKey::Universal
}
}
#[derive(Default)]
pub struct CascadeScratch {
caches: SelectorCaches,
candidates: Vec<RuleRef>,
matched: Vec<MatchedDecl>,
}
pub struct CascadeIndex<'a> {
stylesheets: &'a [(&'a Stylesheet, Origin)],
by_id: FxHashMap<String, Vec<RuleRef>>,
by_class: FxHashMap<String, Vec<RuleRef>>,
by_local: FxHashMap<String, Vec<RuleRef>>,
universal: Vec<RuleRef>,
ancestor_hashes: Vec<Vec<Box<[AncestorHashes]>>>,
has_ancestor_hashes: bool,
}
impl<'a> CascadeIndex<'a> {
pub fn build(stylesheets: &'a [(&'a Stylesheet, Origin)]) -> Self {
let mut index = CascadeIndex {
stylesheets,
by_id: FxHashMap::default(),
by_class: FxHashMap::default(),
by_local: FxHashMap::default(),
universal: Vec::new(),
ancestor_hashes: Vec::with_capacity(stylesheets.len()),
has_ancestor_hashes: false,
};
for (sheet_idx, (sheet, _origin)) in stylesheets.iter().enumerate() {
let mut sheet_hashes = Vec::with_capacity(sheet.rules.len());
for (rule_idx, rule) in sheet.rules.iter().enumerate() {
let rref = (sheet_idx as u32, rule_idx as u32);
for selector in &rule.selectors {
match selector_bucket_key(selector) {
BucketKey::Id(k) => index.by_id.entry(k).or_default().push(rref),
BucketKey::Class(k) => index.by_class.entry(k).or_default().push(rref),
BucketKey::Local(k) => index.by_local.entry(k).or_default().push(rref),
BucketKey::Universal => index.universal.push(rref),
}
}
let rule_hashes: Box<[AncestorHashes]> = rule
.selectors
.iter()
.map(|selector| {
let hashes = AncestorHashes::new(selector, QuirksMode::NoQuirks);
index.has_ancestor_hashes |= hashes.packed_hashes[0] != 0;
hashes
})
.collect();
sheet_hashes.push(rule_hashes);
}
index.ancestor_hashes.push(sheet_hashes);
}
index
}
pub fn has_complex_selectors(&self) -> bool {
self.has_ancestor_hashes
}
fn candidate_rules(&self, elem: ElementRef<'_>, out: &mut Vec<RuleRef>) {
out.clear();
out.extend_from_slice(&self.universal);
if let Some(name) = elem.dom.element_name(elem.id) {
let name = name.as_ref();
let bucket = if name.bytes().any(|b| b.is_ascii_uppercase()) {
self.by_local.get(name.to_ascii_lowercase().as_str())
} else {
self.by_local.get(name)
};
if let Some(v) = bucket {
out.extend_from_slice(v);
}
}
if let Some(id) = elem.dom.element_id(elem.id)
&& let Some(v) = self.by_id.get(id)
{
out.extend_from_slice(v);
}
for class in elem.dom.element_classes(elem.id) {
if let Some(v) = self.by_class.get(class) {
out.extend_from_slice(v);
}
}
out.sort_unstable();
out.dedup();
}
}
pub fn compute_styles(
elem: ElementRef<'_>,
stylesheets: &[(Stylesheet, Origin)],
parent_style: Option<&ComputedStyle>,
style_pool: &mut StylePool,
) -> ComputedStyle {
let refs: Vec<(&Stylesheet, Origin)> = stylesheets.iter().map(|(s, o)| (s, *o)).collect();
let index = CascadeIndex::build(&refs);
compute_styles_indexed(
elem,
&index,
parent_style,
style_pool,
&mut CascadeScratch::default(),
None,
None,
)
}
pub fn compute_styles_indexed(
elem: ElementRef<'_>,
index: &CascadeIndex<'_>,
parent_style: Option<&ComputedStyle>,
_style_pool: &mut StylePool,
scratch: &mut CascadeScratch,
bloom: Option<&BloomFilter>,
inline_style: Option<&crate::style::InlineStyle>,
) -> ComputedStyle {
let CascadeScratch {
caches,
candidates,
matched,
} = scratch;
index.candidate_rules(elem, candidates);
matched.clear();
let mut order: u32 = 0;
for &(sheet_idx, rule_idx) in candidates.iter() {
let (stylesheet, origin) = index.stylesheets[sheet_idx as usize];
let rule = &stylesheet.rules[rule_idx as usize];
let hashes = &index.ancestor_hashes[sheet_idx as usize][rule_idx as usize];
if let Some(specificity) = rule_match_specificity(elem, rule, hashes, bloom, caches) {
for decl_idx in 0..rule.declarations.len() {
matched.push(MatchedDecl {
sheet: sheet_idx,
rule: rule_idx,
decl: decl_idx as u32,
origin,
specificity,
order,
important: false,
});
order += 1;
}
for decl_idx in 0..rule.important_declarations.len() {
matched.push(MatchedDecl {
sheet: sheet_idx,
rule: rule_idx,
decl: decl_idx as u32,
origin,
specificity,
order,
important: true,
});
order += 1;
}
}
}
if matched.len() > 1 {
matched.sort_unstable_by(|a, b| {
if a.important != b.important {
return a.important.cmp(&b.important);
}
let origin_cmp = if a.important {
b.origin.cmp(&a.origin)
} else {
a.origin.cmp(&b.origin)
};
if origin_cmp != Ordering::Equal {
return origin_cmp;
}
let spec_cmp = a.specificity.cmp(&b.specificity);
if spec_cmp != Ordering::Equal {
return spec_cmp;
}
a.order.cmp(&b.order)
});
}
let mut style = if let Some(parent) = parent_style {
inherit_from_parent(parent)
} else {
ComputedStyle::default()
};
let mut inline_normal_pending = inline_style.is_some_and(|i| !i.declarations.is_empty());
for m in matched.iter() {
if m.important && inline_normal_pending {
for decl in &inline_style.expect("checked above").declarations {
apply_declaration(&mut style, decl);
}
inline_normal_pending = false;
}
let (stylesheet, _) = index.stylesheets[m.sheet as usize];
let rule = &stylesheet.rules[m.rule as usize];
let decl = if m.important {
&rule.important_declarations[m.decl as usize]
} else {
&rule.declarations[m.decl as usize]
};
apply_declaration(&mut style, decl);
}
if let Some(inline) = inline_style {
if inline_normal_pending {
for decl in &inline.declarations {
apply_declaration(&mut style, decl);
}
}
for decl in &inline.important_declarations {
apply_declaration(&mut style, decl);
}
}
style
}
fn rule_match_specificity(
elem: ElementRef<'_>,
rule: &CssRule,
hashes: &[AncestorHashes],
bloom: Option<&BloomFilter>,
caches: &mut SelectorCaches,
) -> Option<Specificity> {
let mut context = MatchingContext::new(
selectors::matching::MatchingMode::Normal,
bloom,
caches,
QuirksMode::NoQuirks,
selectors::matching::NeedsSelectorFlags::No,
selectors::matching::MatchingForInvalidation::No,
);
debug_assert_eq!(rule.selectors.len(), hashes.len());
rule.selectors
.iter()
.zip(&rule.selector_specificities)
.zip(hashes)
.filter(|&((selector, _), hashes)| {
selectors::matching::matches_selector(selector, 0, Some(hashes), &elem, &mut context)
})
.map(|((_, spec), _)| *spec)
.max()
}
fn apply_declaration(style: &mut ComputedStyle, decl: &Declaration) {
match decl {
Declaration::Color(c) => style.color = Some(*c),
Declaration::BackgroundColor(c) => style.background_color = Some(*c),
Declaration::FontFamily(s) => style.font_family = Some(s.clone()),
Declaration::FontSize(l) => style.font_size = *l,
Declaration::FontWeight(w) => style.font_weight = *w,
Declaration::FontStyle(s) => style.font_style = *s,
Declaration::FontVariant(v) => style.font_variant = *v,
Declaration::TextAlign(a) => style.text_align = *a,
Declaration::TextIndent(l) => style.text_indent = *l,
Declaration::LineHeight(l) => style.line_height = *l,
Declaration::LetterSpacing(l) => style.letter_spacing = *l,
Declaration::WordSpacing(l) => style.word_spacing = *l,
Declaration::TextTransform(t) => style.text_transform = *t,
Declaration::Hyphens(h) => style.hyphens = *h,
Declaration::WhiteSpace(ws) => style.white_space = *ws,
Declaration::VerticalAlign(v) => style.vertical_align = *v,
Declaration::TextDecoration(d) => {
style.text_decoration_underline = d.underline;
style.text_decoration_line_through = d.line_through;
}
Declaration::TextDecorationStyle(s) => style.underline_style = *s,
Declaration::TextDecorationColor(c) => style.underline_color = Some(*c),
Declaration::Margin(l) => {
style.margin_top = *l;
style.margin_right = *l;
style.margin_bottom = *l;
style.margin_left = *l;
}
Declaration::MarginTop(l) => style.margin_top = *l,
Declaration::MarginRight(l) => style.margin_right = *l,
Declaration::MarginBottom(l) => style.margin_bottom = *l,
Declaration::MarginLeft(l) => style.margin_left = *l,
Declaration::Padding(l) => {
style.padding_top = *l;
style.padding_right = *l;
style.padding_bottom = *l;
style.padding_left = *l;
}
Declaration::PaddingTop(l) => style.padding_top = *l,
Declaration::PaddingRight(l) => style.padding_right = *l,
Declaration::PaddingBottom(l) => style.padding_bottom = *l,
Declaration::PaddingLeft(l) => style.padding_left = *l,
Declaration::Width(l) => style.width = *l,
Declaration::Height(l) => style.height = *l,
Declaration::MaxWidth(l) => style.max_width = *l,
Declaration::MaxHeight(l) => style.max_height = *l,
Declaration::MinWidth(l) => style.min_width = *l,
Declaration::MinHeight(l) => style.min_height = *l,
Declaration::Display(d) => style.display = *d,
Declaration::Float(f) => style.float = *f,
Declaration::Clear(c) => style.clear = *c,
Declaration::Visibility(v) => style.visibility = *v,
Declaration::BoxSizing(bs) => style.box_sizing = *bs,
Declaration::Orphans(n) => style.orphans = *n,
Declaration::Widows(n) => style.widows = *n,
Declaration::WordBreak(wb) => style.word_break = *wb,
Declaration::OverflowWrap(ow) => style.overflow_wrap = *ow,
Declaration::BreakBefore(b) => style.break_before = *b,
Declaration::BreakAfter(b) => style.break_after = *b,
Declaration::BreakInside(b) => style.break_inside = *b,
Declaration::BorderStyle(s) => {
style.border_style_top = *s;
style.border_style_right = *s;
style.border_style_bottom = *s;
style.border_style_left = *s;
}
Declaration::BorderTopStyle(s) => style.border_style_top = *s,
Declaration::BorderRightStyle(s) => style.border_style_right = *s,
Declaration::BorderBottomStyle(s) => style.border_style_bottom = *s,
Declaration::BorderLeftStyle(s) => style.border_style_left = *s,
Declaration::BorderWidth(l) => {
style.border_width_top = *l;
style.border_width_right = *l;
style.border_width_bottom = *l;
style.border_width_left = *l;
}
Declaration::BorderTopWidth(l) => style.border_width_top = *l,
Declaration::BorderRightWidth(l) => style.border_width_right = *l,
Declaration::BorderBottomWidth(l) => style.border_width_bottom = *l,
Declaration::BorderLeftWidth(l) => style.border_width_left = *l,
Declaration::BorderColor(c) => {
style.border_color_top = Some(*c);
style.border_color_right = Some(*c);
style.border_color_bottom = Some(*c);
style.border_color_left = Some(*c);
}
Declaration::BorderTopColor(c) => style.border_color_top = Some(*c),
Declaration::BorderRightColor(c) => style.border_color_right = Some(*c),
Declaration::BorderBottomColor(c) => style.border_color_bottom = Some(*c),
Declaration::BorderLeftColor(c) => style.border_color_left = Some(*c),
Declaration::BorderRadius(l) => {
style.border_radius_top_left = *l;
style.border_radius_top_right = *l;
style.border_radius_bottom_left = *l;
style.border_radius_bottom_right = *l;
}
Declaration::BorderTopLeftRadius(l) => style.border_radius_top_left = *l,
Declaration::BorderTopRightRadius(l) => style.border_radius_top_right = *l,
Declaration::BorderBottomLeftRadius(l) => style.border_radius_bottom_left = *l,
Declaration::BorderBottomRightRadius(l) => style.border_radius_bottom_right = *l,
Declaration::ListStyleType(lst) => style.list_style_type = *lst,
Declaration::ListStylePosition(p) => style.list_style_position = *p,
Declaration::BorderCollapse(bc) => style.border_collapse = *bc,
Declaration::BorderSpacing(l) => style.border_spacing = *l,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::style::properties::Color;
fn p_color(css: &str) -> Option<Color> {
let dom = crate::dom::parse_dom("<p>x</p>");
let p = dom.find_by_tag("p").unwrap();
let elem = ElementRef::new(&dom, p);
let sheet = Stylesheet::parse(css);
let mut pool = StylePool::default();
compute_styles(elem, &[(sheet, Origin::Author)], None, &mut pool).color
}
#[test]
fn important_declaration_beats_later_normal() {
assert_eq!(
p_color("p { color: red !important } p { color: blue }"),
Some(Color::rgb(255, 0, 0))
);
}
#[test]
fn specificity_is_per_selector_not_first_in_list() {
assert_eq!(
p_color(".never, p { color: red } p { color: blue }"),
Some(Color::rgb(0, 0, 255))
);
}
#[test]
fn box_shorthand_keeps_important() {
for css in [
"p { margin: 5px !important }",
"p { margin: 5px 6px !important }",
"p { padding: 1px 2px 3px !important }",
"p { border-width: 1px !important }",
"p { border-style: solid !important }",
"p { border-color: #123 !important }",
] {
let sheet = Stylesheet::parse(css);
let rule = &sheet.rules[0];
assert!(
rule.declarations.is_empty(),
"{css}: normal declarations should be empty, got {:?}",
rule.declarations
);
assert!(
!rule.important_declarations.is_empty(),
"{css}: expected important declarations"
);
}
}
}