oxipdf-html 0.1.0

HTML+CSS → StyledTree adapter for the oxipdf PDF engine
Documentation
//! CSS cascade application: rule matching, specificity ordering, !important handling.

use scraper::{Html, Node, Selector};

use oxipdf_ir::style::ResolvedStyle;

use crate::css::{CssRule, Declaration, Specificity, apply_declarations};

/// Find all CSS rules that match the given DOM node, sorted by specificity (ascending).
///
/// Lower specificity first means that when declarations are applied in order,
/// higher-specificity declarations win (last-write semantics).
fn find_matching_rules<'a>(
    document: &Html,
    node_id: ego_tree::NodeId,
    rules: &'a [CssRule],
) -> Vec<(&'a CssRule, Specificity)> {
    let mut matching: Vec<(&CssRule, Specificity)> = Vec::new();

    for rule in rules {
        if let Ok(selector) = Selector::parse(&rule.selector) {
            if let Some(el_ref) = document.tree.get(node_id) {
                if let Node::Element(_) = el_ref.value() {
                    if let Some(el) = scraper::ElementRef::wrap(el_ref) {
                        if selector.matches(&el) {
                            matching.push((rule, rule.specificity));
                        }
                    }
                }
            }
        }
    }

    // Sort by specificity (lower first, so later = higher = wins).
    matching.sort_by_key(|(_, spec)| *spec);
    matching
}

/// Apply normal (non-`!important`) declarations from matching CSS rules.
///
/// This is phase 1 of the cascade. Call this before applying inline normal
/// declarations when the element has a `style=""` attribute.
pub(crate) fn apply_normal_stylesheet_rules(
    document: &Html,
    node_id: ego_tree::NodeId,
    style: &mut ResolvedStyle,
    rules: &[CssRule],
) {
    let matching = find_matching_rules(document, node_id, rules);
    for (rule, _) in &matching {
        let normal: Vec<Declaration> = rule
            .declarations
            .iter()
            .filter(|d| !d.important)
            .cloned()
            .collect();
        if !normal.is_empty() {
            apply_declarations(style, &normal);
        }
    }
}

/// Apply `!important` declarations from matching CSS rules.
///
/// This is phase 2 of the cascade. Call this after applying inline normal
/// declarations and before applying inline `!important` declarations.
pub(crate) fn apply_important_stylesheet_rules(
    document: &Html,
    node_id: ego_tree::NodeId,
    style: &mut ResolvedStyle,
    rules: &[CssRule],
) {
    let matching = find_matching_rules(document, node_id, rules);
    for (rule, _) in &matching {
        let important: Vec<Declaration> = rule
            .declarations
            .iter()
            .filter(|d| d.important)
            .cloned()
            .collect();
        if !important.is_empty() {
            apply_declarations(style, &important);
        }
    }
}

/// Apply matching CSS rules to a style, respecting `!important` and specificity.
///
/// Cascade order:
/// 1. Normal declarations from matching rules (sorted by specificity)
/// 2. `!important` declarations from matching rules (sorted by specificity)
///
/// Use this when the element has no inline `style=""` attribute.
/// When an inline style is present, use [`apply_normal_stylesheet_rules`] and
/// [`apply_important_stylesheet_rules`] to correctly interleave inline declarations.
pub(crate) fn apply_matching_rules(
    document: &Html,
    node_id: ego_tree::NodeId,
    style: &mut ResolvedStyle,
    rules: &[CssRule],
) {
    let matching = find_matching_rules(document, node_id, rules);

    // Phase 1: Apply normal (non-!important) declarations.
    for (rule, _) in &matching {
        let normal: Vec<Declaration> = rule
            .declarations
            .iter()
            .filter(|d| !d.important)
            .cloned()
            .collect();
        if !normal.is_empty() {
            apply_declarations(style, &normal);
        }
    }

    // Phase 2: Apply !important declarations (override everything above).
    for (rule, _) in &matching {
        let important: Vec<Declaration> = rule
            .declarations
            .iter()
            .filter(|d| d.important)
            .cloned()
            .collect();
        if !important.is_empty() {
            apply_declarations(style, &important);
        }
    }
}