vexy-vsvg-plugin-sdk 2.4.2

Plugin SDK for vexy-vsvg
Documentation
// this_file: crates/vexy-vsvg-plugin-sdk/src/css_matching.rs

//! Advanced CSS selector matching with full tree traversal support.
//!
//! This module provides an alternative selector implementation that stores ancestry paths,
//! enabling full tree traversal (parent, sibling, child combinators). Unlike `selector.rs`
//! which has simplified implementations returning `None` for parent/sibling queries, this
//! module's `ElementWrapper` tracks the path from root to current element.
//!
//! # Architecture
//!
//! **Key difference from `selector.rs`:**
//! - `selector.rs`: `SvgElement` wraps a single `&Element`, parent/sibling return `None`
//! - `css_matching.rs`: `ElementWrapper` stores `Vec<&Element>` path, enabling full traversal
//!
//! This comes at a cost: every wrapper clones the path vector when navigating. Use this
//! module only when plugins need complex selectors like `g > rect` or `path + circle`.
//!
//! # Usage
//!
//! ```no_run
//! use vexy_vsvg::ast::Element;
//! use vexy_vsvg_plugin_sdk::css_matching::ElementWrapper;
//! # let root: &Element = todo!();
//!
//! let wrapper = ElementWrapper::new(root);
//! // Now supports full selector matching including parent/sibling combinators
//! ```

use cssparser::ToCss;
use precomputed_hash::PrecomputedHash;
use selectors::attr::{AttrSelectorOperation, CaseSensitivity, NamespaceConstraint};
use selectors::matching::{ElementSelectorFlags, MatchingContext};
use selectors::{Element, OpaqueElement, SelectorImpl};
use std::borrow::Borrow;
use std::fmt;
use vexy_vsvg::ast::{self, Node};

/// Newtype wrapper for `String` implementing all traits required by `selectors` crate.
///
/// This is similar to the wrapper types in `selector.rs`, but uses a single unified type
/// for all string-based associated types (attribute values, identifiers, local names, etc.).
/// This simplifies the implementation at the cost of less type safety.
///
/// **Implements:** `ToCss`, `PrecomputedHash`, `From<&str>`, `Clone`, `Eq`, `Borrow<str>`, `Default`.
#[derive(Debug, Clone, PartialEq, Eq, Hash, Default)]
pub struct CssString(pub String);

impl From<&str> for CssString {
    fn from(s: &str) -> Self {
        CssString(s.to_owned())
    }
}

impl ToCss for CssString {
    fn to_css<W>(&self, dest: &mut W) -> fmt::Result
    where
        W: fmt::Write,
    {
        dest.write_str(&self.0)
    }
}

impl PrecomputedHash for CssString {
    fn precomputed_hash(&self) -> u32 {
        use std::collections::hash_map::DefaultHasher;
        use std::hash::{Hash, Hasher};
        let mut hasher = DefaultHasher::new();
        self.0.hash(&mut hasher);
        hasher.finish() as u32
    }
}

impl Borrow<str> for CssString {
    fn borrow(&self) -> &str {
        &self.0
    }
}

impl AsRef<str> for CssString {
    fn as_ref(&self) -> &str {
        &self.0
    }
}

/// Selector implementation using unified `CssString` for all associated types.
///
/// Simpler than `SvgSelectorImpl` in `selector.rs`, which uses distinct newtypes for each
/// associated type. This reduces boilerplate at the cost of less type safety.
#[derive(Debug, Clone)]
pub struct VexySelectorImpl;

impl SelectorImpl for VexySelectorImpl {
    type AttrValue = CssString;
    type Identifier = CssString;
    type LocalName = CssString;
    type NamespaceUrl = CssString;
    type NamespacePrefix = CssString;
    type BorrowedNamespaceUrl = str;
    type BorrowedLocalName = str;
    type NonTSPseudoClass = NonTSPseudoClass;
    type PseudoElement = PseudoElement;
    type ExtraMatchingData<'a> = ();
}

/// Uninhabited enum for pseudo-classes (`:hover`, `:active`, etc.).
///
/// Not used in SVG optimization. Required by `selectors` crate trait bounds.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NonTSPseudoClass {}

impl selectors::parser::NonTSPseudoClass for NonTSPseudoClass {
    type Impl = VexySelectorImpl;
    fn is_active_or_hover(&self) -> bool {
        false
    }
    fn is_user_action_state(&self) -> bool {
        false
    }
}

impl ToCss for NonTSPseudoClass {
    fn to_css<W>(&self, _dest: &mut W) -> fmt::Result
    where
        W: fmt::Write,
    {
        Ok(())
    }
}

/// Uninhabited enum for pseudo-elements (`::before`, `::after`, etc.).
///
/// Not applicable to SVG. Required by `selectors` crate trait bounds.
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PseudoElement {}

impl selectors::parser::PseudoElement for PseudoElement {
    type Impl = VexySelectorImpl;
}

impl ToCss for PseudoElement {
    fn to_css<W>(&self, _dest: &mut W) -> fmt::Result
    where
        W: fmt::Write,
    {
        Ok(())
    }
}

/// Element wrapper that tracks the path from root to current element.
///
/// Unlike `SvgElement` in `selector.rs`, this stores the full ancestry path, enabling
/// parent and sibling queries needed for complex CSS combinators.
///
/// # Memory Cost
///
/// Each navigation operation (e.g., `first_element_child()`) clones the path vector and
/// pushes/pops elements. For deep trees or frequent navigation, this can be expensive.
///
/// # Design Choice
///
/// Vexy Vsvg's AST doesn't store parent pointers (saves memory, simplifies mutation).
/// To support CSS combinators like `>`, `+`, `~`, we reconstruct parent context by
/// tracking the path during traversal. This is a classic space-time tradeoff.
#[derive(Debug, Clone)]
pub struct ElementWrapper<'a> {
    /// Path from root to this element (inclusive).
    ///
    /// `path[0]` is the root, `path[path.len() - 1]` is the current element.
    pub path: Vec<&'a ast::Element<'a>>,
}

impl<'a> ElementWrapper<'a> {
    /// Creates a new wrapper for the root element.
    pub fn new(root: &'a ast::Element<'a>) -> Self {
        Self { path: vec![root] }
    }

    /// Returns the current element (last in the path).
    pub fn current(&self) -> &'a ast::Element<'a> {
        self.path.last().expect("Path cannot be empty")
    }
}

impl<'a> Element for ElementWrapper<'a> {
    type Impl = VexySelectorImpl;

    fn opaque(&self) -> OpaqueElement {
        // Use the address of the underlying Element as unique ID
        OpaqueElement::new(self.current())
    }

    fn parent_element(&self) -> Option<Self> {
        if self.path.len() > 1 {
            let mut new_path = self.path.clone();
            new_path.pop();
            Some(Self { path: new_path })
        } else {
            None
        }
    }

    fn parent_node_is_shadow_root(&self) -> bool {
        false
    }

    fn containing_shadow_host(&self) -> Option<Self> {
        None
    }

    fn is_pseudo_element(&self) -> bool {
        false
    }

    fn first_element_child(&self) -> Option<Self> {
        self.current().children.iter().find_map(|node| {
            if let Node::Element(el) = node {
                let mut new_path = self.path.clone();
                new_path.push(el);
                Some(Self { path: new_path })
            } else {
                None
            }
        })
    }

    fn prev_sibling_element(&self) -> Option<Self> {
        // O(N) operation where N is the number of siblings.
        // We must scan the parent's children to find our index, then return the previous sibling.
        if self.path.len() <= 1 {
            return None;
        }

        let parent = self.path[self.path.len() - 2];
        let current_ptr = self.current() as *const _;

        let mut prev_el: Option<&'a ast::Element<'a>> = None;

        for child in &parent.children {
            if let Node::Element(el) = child {
                if std::ptr::eq(el, current_ptr) {
                    if let Some(prev) = prev_el {
                        let mut new_path = self.path.clone();
                        new_path.pop();
                        new_path.push(prev);
                        return Some(Self { path: new_path });
                    }
                    return None;
                }
                prev_el = Some(el);
            }
        }
        None
    }

    fn next_sibling_element(&self) -> Option<Self> {
        if self.path.len() <= 1 {
            return None;
        }

        let parent = self.path[self.path.len() - 2];
        let current_ptr = self.current() as *const _;
        let mut found_self = false;

        for child in &parent.children {
            if let Node::Element(el) = child {
                if found_self {
                    let mut new_path = self.path.clone();
                    new_path.pop();
                    new_path.push(el);
                    return Some(Self { path: new_path });
                }
                if std::ptr::eq(el, current_ptr) {
                    found_self = true;
                }
            }
        }
        None
    }

    fn is_html_element_in_html_document(&self) -> bool {
        false
    }

    fn has_local_name(&self, local_name: &<Self::Impl as SelectorImpl>::BorrowedLocalName) -> bool {
        self.current().name == local_name
    }

    fn has_namespace(&self, _ns: &<Self::Impl as SelectorImpl>::BorrowedNamespaceUrl) -> bool {
        // Namespace support is minimal for now as SVGO selectors typically ignore namespaces.
        true
    }

    fn is_same_type(&self, other: &Self) -> bool {
        self.current().name == other.current().name
    }

    fn attr_matches(
        &self,
        _ns: &NamespaceConstraint<&<Self::Impl as SelectorImpl>::NamespaceUrl>,
        local_name: &<Self::Impl as SelectorImpl>::LocalName,
        operation: &AttrSelectorOperation<&<Self::Impl as SelectorImpl>::AttrValue>,
    ) -> bool {
        // Ignores namespace constraint for simplicity (SVG typically doesn't use namespaced attributes).
        if let Some(value) = self.current().attr(&local_name.0) {
            operation.eval_str(value)
        } else {
            false
        }
    }

    fn match_non_ts_pseudo_class(
        &self,
        _pc: &<Self::Impl as SelectorImpl>::NonTSPseudoClass,
        _context: &mut MatchingContext<Self::Impl>,
    ) -> bool {
        false
    }

    fn match_pseudo_element(
        &self,
        _pe: &<Self::Impl as SelectorImpl>::PseudoElement,
        _context: &mut MatchingContext<Self::Impl>,
    ) -> bool {
        false
    }

    fn apply_selector_flags(&self, _flags: ElementSelectorFlags) {}

    fn is_link(&self) -> bool {
        self.current().name == "a"
    }

    fn is_html_slot_element(&self) -> bool {
        false
    }

    fn has_id(
        &self,
        id: &<Self::Impl as SelectorImpl>::Identifier,
        _case_sensitivity: CaseSensitivity,
    ) -> bool {
        self.current().attr("id") == Some(id.0.as_str())
    }

    fn has_class(
        &self,
        name: &<Self::Impl as SelectorImpl>::Identifier,
        case_sensitivity: CaseSensitivity,
    ) -> bool {
        if let Some(class_attr) = self.current().attr("class") {
            class_attr
                .split_whitespace()
                .any(|c| case_sensitivity.eq(c.as_bytes(), name.0.as_bytes()))
        } else {
            false
        }
    }

    fn imported_part(
        &self,
        _name: &<Self::Impl as SelectorImpl>::Identifier,
    ) -> Option<<Self::Impl as SelectorImpl>::Identifier> {
        None
    }

    fn is_part(&self, _name: &<Self::Impl as SelectorImpl>::Identifier) -> bool {
        false
    }

    fn has_custom_state(&self, _name: &<Self::Impl as SelectorImpl>::Identifier) -> bool {
        false
    }

    fn add_element_unique_hashes(
        &self,
        _filter: &mut selectors::bloom::CountingBloomFilter<selectors::bloom::BloomStorageU8>,
    ) -> bool {
        false
    }

    fn is_empty(&self) -> bool {
        self.current().is_empty()
    }

    fn is_root(&self) -> bool {
        self.path.len() == 1
    }
}