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}