Skip to main content

fhp_selector/
ast.rs

1//! CSS selector abstract syntax tree.
2//!
3//! The AST is produced by [`crate::parser`] and consumed by [`crate::matcher`].
4//! Selectors are stored in right-to-left order for efficient matching.
5
6use fhp_core::tag::Tag;
7
8/// A comma-separated list of selectors.
9///
10/// A node matches the list if it matches **any** of the selectors.
11#[derive(Debug, Clone)]
12pub struct SelectorList {
13    /// Individual selectors.
14    pub selectors: Vec<Selector>,
15}
16
17/// A single CSS selector (complex selector).
18///
19/// Stored in right-to-left order: the subject (rightmost compound) is
20/// matched first, then combinators walk leftward through ancestors/siblings.
21#[derive(Debug, Clone)]
22pub struct Selector {
23    /// The rightmost compound selector (the subject to match).
24    pub subject: CompoundSelector,
25    /// Chain of (combinator, compound) walking leftward from the subject.
26    pub chain: Vec<(Combinator, CompoundSelector)>,
27}
28
29/// A compound selector: one or more simple selectors applied to a single node.
30///
31/// E.g., `div.active#main[data-x]` is a single compound with 4 parts.
32#[derive(Debug, Clone)]
33pub struct CompoundSelector {
34    /// Simple selectors that must all match the same node.
35    pub parts: Vec<SimpleSelector>,
36}
37
38/// A single simple selector.
39#[derive(Debug, Clone)]
40pub enum SimpleSelector {
41    /// Match by tag name: `div`, `p`, `span`.
42    Tag(Tag),
43    /// Match by unknown/custom tag name, e.g. `my-widget`.
44    UnknownTag(String),
45    /// Match by class: `.class`. Second field is precomputed 64-bit bloom bit.
46    Class(String, u64),
47    /// Match by id: `#id`. Second field is precomputed FNV-1a hash.
48    Id(String, u32),
49    /// Universal selector: `*`.
50    Universal,
51    /// Attribute selector: `[attr]`, `[attr=val]`, etc.
52    Attr(AttrSelector),
53    /// `:first-child` pseudo-class.
54    PseudoFirstChild,
55    /// `:last-child` pseudo-class.
56    PseudoLastChild,
57    /// `:nth-child(an+b)` pseudo-class.
58    PseudoNthChild {
59        /// The `a` coefficient.
60        a: i32,
61        /// The `b` offset.
62        b: i32,
63    },
64    /// `:not(selector)` pseudo-class.
65    PseudoNot(Box<CompoundSelector>),
66}
67
68/// Attribute selector with comparison operator.
69#[derive(Debug, Clone)]
70pub struct AttrSelector {
71    /// Attribute name.
72    pub name: String,
73    /// Comparison operator.
74    pub op: AttrOp,
75    /// Value to compare against (`None` for existence check).
76    pub value: Option<String>,
77}
78
79/// Attribute comparison operator.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum AttrOp {
82    /// `[attr]` — attribute exists.
83    Exists,
84    /// `[attr=val]` — exact match.
85    Equals,
86    /// `[attr~=val]` — word in space-separated list.
87    Includes,
88    /// `[attr^=val]` — starts with.
89    StartsWith,
90    /// `[attr$=val]` — ends with.
91    EndsWith,
92    /// `[attr*=val]` — contains substring.
93    Substring,
94}
95
96/// Combinator between compound selectors.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum Combinator {
99    /// ` ` — descendant (any depth).
100    Descendant,
101    /// `>` — direct child.
102    Child,
103    /// `+` — adjacent sibling (immediately preceding).
104    AdjacentSibling,
105    /// `~` — general sibling (any preceding sibling).
106    GeneralSibling,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112
113    #[test]
114    fn selector_debug() {
115        let sel = Selector {
116            subject: CompoundSelector {
117                parts: vec![SimpleSelector::Tag(Tag::Div)],
118            },
119            chain: vec![],
120        };
121        let debug = format!("{sel:?}");
122        assert!(debug.contains("Div"));
123    }
124
125    #[test]
126    fn attr_op_eq() {
127        assert_eq!(AttrOp::Exists, AttrOp::Exists);
128        assert_ne!(AttrOp::Equals, AttrOp::Substring);
129    }
130
131    #[test]
132    fn combinator_eq() {
133        assert_eq!(Combinator::Descendant, Combinator::Descendant);
134        assert_ne!(Combinator::Child, Combinator::Descendant);
135    }
136}