Skip to main content

omena_syntax/
selector.rs

1//! Canonical selector structure shared by parser, transforms, queries, and matching.
2//!
3//! The parser supplies a selector CST node. This module projects that CST into
4//! one source-preserving authority: nesting tokens retain exact byte spans,
5//! while identity-bearing selector names are represented by sealed decoded keys.
6
7use std::ops::Range;
8
9use crate::{
10    SyntaxKind, SyntaxNode,
11    ident::{CanonicalClassKeyV0, CanonicalIdKeyV0, CanonicalTypeSelectorKeyV0, ClassNameV0},
12};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum CanonicalSelectorCombinatorV0 {
16    Descendant,
17    Child,
18    NextSibling,
19    SubsequentSibling,
20    Column,
21    Other,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub struct CanonicalSelectorSpecificityWitnessV0 {
26    pub ids: u32,
27    pub classes: u32,
28    pub types: u32,
29    pub exact: bool,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub struct NestingTokenV0 {
34    byte_range: Range<usize>,
35}
36
37impl NestingTokenV0 {
38    pub fn byte_range(&self) -> Range<usize> {
39        self.byte_range.clone()
40    }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct CanonicalCompoundSelectorV0 {
45    byte_range: Range<usize>,
46    required_tag: Option<CanonicalTypeSelectorKeyV0>,
47    required_id: Option<CanonicalIdKeyV0>,
48    required_classes: Vec<CanonicalClassKeyV0>,
49    nesting_tokens: Vec<NestingTokenV0>,
50}
51
52impl CanonicalCompoundSelectorV0 {
53    pub fn byte_range(&self) -> Range<usize> {
54        self.byte_range.clone()
55    }
56
57    pub fn required_tag(&self) -> Option<&CanonicalTypeSelectorKeyV0> {
58        self.required_tag.as_ref()
59    }
60
61    pub fn required_id(&self) -> Option<&CanonicalIdKeyV0> {
62        self.required_id.as_ref()
63    }
64
65    pub fn required_classes(&self) -> &[CanonicalClassKeyV0] {
66        self.required_classes.as_slice()
67    }
68
69    pub fn nesting_tokens(&self) -> &[NestingTokenV0] {
70        self.nesting_tokens.as_slice()
71    }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct CanonicalSelectorBranchV0 {
76    authored: String,
77    byte_range: Range<usize>,
78    compounds: Vec<CanonicalCompoundSelectorV0>,
79    combinators: Vec<CanonicalSelectorCombinatorV0>,
80    nesting_tokens: Vec<NestingTokenV0>,
81    specificity: CanonicalSelectorSpecificityWitnessV0,
82}
83
84impl CanonicalSelectorBranchV0 {
85    pub fn authored(&self) -> &str {
86        self.authored.as_str()
87    }
88
89    pub fn byte_range(&self) -> Range<usize> {
90        self.byte_range.clone()
91    }
92
93    pub fn compounds(&self) -> &[CanonicalCompoundSelectorV0] {
94        self.compounds.as_slice()
95    }
96
97    pub fn combinators(&self) -> &[CanonicalSelectorCombinatorV0] {
98        self.combinators.as_slice()
99    }
100
101    pub fn nesting_tokens(&self) -> &[NestingTokenV0] {
102        self.nesting_tokens.as_slice()
103    }
104
105    pub fn specificity(&self) -> CanonicalSelectorSpecificityWitnessV0 {
106        self.specificity
107    }
108
109    fn substitute_nesting(&self, parent: &str) -> String {
110        if self.nesting_tokens.is_empty() {
111            return format!("{parent} {}", self.authored.trim());
112        }
113        let branch_start = self.byte_range.start;
114        let mut output = String::with_capacity(self.authored.len().saturating_add(parent.len()));
115        let mut cursor = 0usize;
116        for token in &self.nesting_tokens {
117            let start = token.byte_range.start.saturating_sub(branch_start);
118            let end = token.byte_range.end.saturating_sub(branch_start);
119            if start < cursor || end > self.authored.len() {
120                continue;
121            }
122            output.push_str(&self.authored[cursor..start]);
123            output.push_str(parent);
124            cursor = end;
125        }
126        output.push_str(&self.authored[cursor..]);
127        output
128    }
129}
130
131/// The sole selector canonicalization authority.
132///
133/// Construction accepts parser CST nodes, so quoted, escaped, and attribute
134/// interiors never need to be rediscovered by a string replacement pass.
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct CanonicalSelectorAst {
137    authored: String,
138    source_byte_range: Range<usize>,
139    branches: Vec<CanonicalSelectorBranchV0>,
140}
141
142impl CanonicalSelectorAst {
143    pub fn from_cst(selector_node: &SyntaxNode) -> Option<Self> {
144        if !matches!(
145            selector_node.kind(),
146            SyntaxKind::SelectorList
147                | SyntaxKind::RelativeSelectorList
148                | SyntaxKind::BogusSelectorList
149                | SyntaxKind::Selector
150                | SyntaxKind::RelativeSelector
151                | SyntaxKind::BogusSelector
152        ) {
153            return None;
154        }
155        let (authority_start, authority_prefix) = leading_selector_trivia(selector_node);
156        let authority_end = byte_end(selector_node);
157        let authored = format!("{authority_prefix}{}", syntax_node_text(selector_node)?);
158        let branch_nodes = if matches!(
159            selector_node.kind(),
160            SyntaxKind::Selector | SyntaxKind::RelativeSelector | SyntaxKind::BogusSelector
161        ) {
162            vec![selector_node.clone()]
163        } else {
164            selector_node
165                .children()
166                .filter(|child| {
167                    matches!(
168                        child.kind(),
169                        SyntaxKind::Selector
170                            | SyntaxKind::RelativeSelector
171                            | SyntaxKind::BogusSelector
172                    )
173                })
174                .cloned()
175                .collect::<Vec<_>>()
176        };
177        let branches = branch_nodes
178            .iter()
179            .enumerate()
180            .filter_map(|(index, branch)| {
181                build_branch(branch, authority_start, authored.as_str(), index == 0)
182            })
183            .collect::<Vec<_>>();
184        (!branches.is_empty()).then_some(Self {
185            authored,
186            source_byte_range: authority_start..authority_end,
187            branches,
188        })
189    }
190
191    pub fn authored(&self) -> &str {
192        self.authored.as_str()
193    }
194
195    pub fn branches(&self) -> &[CanonicalSelectorBranchV0] {
196        self.branches.as_slice()
197    }
198
199    pub fn nesting_token_count(&self) -> usize {
200        self.branches
201            .iter()
202            .map(|branch| branch.nesting_tokens.len())
203            .sum()
204    }
205
206    pub fn canonical_class_keys(&self) -> impl Iterator<Item = &CanonicalClassKeyV0> {
207        self.branches
208            .iter()
209            .flat_map(|branch| branch.compounds.iter())
210            .flat_map(|compound| compound.required_classes.iter())
211    }
212
213    /// Issues a sealed class key only when the parser-owned selector AST covers
214    /// the source fact that produced the projected semantic name.
215    pub fn canonical_class_key_for_source_span(
216        &self,
217        semantic_name: &str,
218        source_byte_range: Range<usize>,
219        nesting_parent_name: Option<&str>,
220    ) -> Option<CanonicalClassKeyV0> {
221        if self.source_byte_range.start > source_byte_range.start
222            || source_byte_range.start >= source_byte_range.end
223            || source_byte_range.end > self.source_byte_range.end
224        {
225            return None;
226        }
227        let relative_source_range = source_byte_range.start - self.source_byte_range.start
228            ..source_byte_range.end - self.source_byte_range.start;
229        let key = ClassNameV0::new(semantic_name).canonical_key();
230        let matching_branches = self
231            .branches
232            .iter()
233            .filter(|branch| {
234                branch.byte_range.start <= relative_source_range.start
235                    && relative_source_range.end <= branch.byte_range.end
236            })
237            .collect::<Vec<_>>();
238        let directly_issued = matching_branches.iter().any(|branch| {
239            branch.compounds.iter().any(|compound| {
240                compound.byte_range.start <= relative_source_range.start
241                    && relative_source_range.end <= compound.byte_range.end
242                    && compound.required_classes.contains(&key)
243            })
244        });
245        let expected = format!(".{semantic_name}");
246        let nesting_issued = matching_branches.iter().any(|branch| {
247            if branch.nesting_tokens.is_empty() {
248                return false;
249            }
250            let issued_for_parent = |parent_name: &str| {
251                let parent = format!(".{parent_name}");
252                branch.substitute_nesting(parent.as_str()).trim() == expected
253            };
254            nesting_parent_name.is_some_and(issued_for_parent)
255        });
256        (directly_issued || nesting_issued).then_some(key)
257    }
258
259    pub fn expand_with_parent(&self, parent: &Self) -> Option<String> {
260        let mut expanded = Vec::new();
261        for parent_branch in &parent.branches {
262            let parent_text = parent_branch.authored.trim();
263            if parent_text.is_empty() {
264                continue;
265            }
266            for nested_branch in &self.branches {
267                expanded.push(nested_branch.substitute_nesting(parent_text));
268            }
269        }
270        (!expanded.is_empty()).then(|| expanded.join(", "))
271    }
272}
273
274fn build_branch(
275    branch: &SyntaxNode,
276    authority_start: usize,
277    authority_text: &str,
278    include_authority_prefix: bool,
279) -> Option<CanonicalSelectorBranchV0> {
280    let raw_branch_range = relative_range(branch, authority_start)?;
281    let authored_start = if include_authority_prefix {
282        0
283    } else {
284        raw_branch_range.start
285    };
286    let raw_authored = authority_text.get(authored_start..raw_branch_range.end)?;
287    let leading_trivia_bytes = raw_authored
288        .len()
289        .saturating_sub(raw_authored.trim_start().len());
290    let trailing_trivia_bytes = raw_authored
291        .len()
292        .saturating_sub(raw_authored.trim_end().len());
293    let branch_range = authored_start.saturating_add(leading_trivia_bytes)
294        ..raw_branch_range.end.saturating_sub(trailing_trivia_bytes);
295    let authored = authority_text.get(branch_range.clone())?.to_string();
296    let complex = branch
297        .children()
298        .find(|child| child.kind() == SyntaxKind::ComplexSelector)
299        .unwrap_or(branch);
300    let compounds = complex
301        .children()
302        .filter(|child| child.kind() == SyntaxKind::CompoundSelector)
303        .filter_map(|compound| build_compound(compound, authority_start))
304        .collect::<Vec<_>>();
305    let combinators = complex
306        .children()
307        .filter(|child| child.kind() == SyntaxKind::Combinator)
308        .map(|node| classify_combinator(syntax_node_text(node).as_deref().unwrap_or_default()))
309        .collect::<Vec<_>>();
310    let nesting_tokens = branch
311        .descendants()
312        .filter(|node| node.kind() == SyntaxKind::NestingSelectorNode)
313        .filter_map(|node| {
314            relative_range(node, authority_start).map(|byte_range| NestingTokenV0 { byte_range })
315        })
316        .collect::<Vec<_>>();
317    let specificity = specificity_witness(branch);
318    Some(CanonicalSelectorBranchV0 {
319        authored,
320        byte_range: branch_range,
321        compounds,
322        combinators,
323        nesting_tokens,
324        specificity,
325    })
326}
327
328fn build_compound(
329    compound: &SyntaxNode,
330    authority_start: usize,
331) -> Option<CanonicalCompoundSelectorV0> {
332    let mut required_tag = None;
333    let mut required_id = None;
334    let mut required_classes = Vec::new();
335    let mut nesting_tokens = Vec::new();
336    for node in compound.children() {
337        match node.kind() {
338            SyntaxKind::ClassSelector => {
339                if let Some(text) = syntax_node_text(node)
340                    && let Some(raw) = text.strip_prefix('.')
341                {
342                    required_classes.push(ClassNameV0::new(raw).canonical_key());
343                }
344            }
345            SyntaxKind::IdSelector => {
346                if let Some(text) = syntax_node_text(node)
347                    && let Some(raw) = text.strip_prefix('#')
348                {
349                    required_id = Some(CanonicalIdKeyV0::from_authored(raw));
350                }
351            }
352            SyntaxKind::TypeSelector => {
353                if let Some(text) = syntax_node_text(node) {
354                    let raw = text.rsplit('|').next().unwrap_or(text.as_str()).trim();
355                    if raw != "*" && !raw.is_empty() {
356                        required_tag = Some(CanonicalTypeSelectorKeyV0::from_authored(raw));
357                    }
358                }
359            }
360            SyntaxKind::NestingSelectorNode => {
361                if let Some(byte_range) = relative_range(node, authority_start) {
362                    nesting_tokens.push(NestingTokenV0 { byte_range });
363                }
364            }
365            _ => {}
366        }
367    }
368    Some(CanonicalCompoundSelectorV0 {
369        byte_range: relative_range(compound, authority_start)?,
370        required_tag,
371        required_id,
372        required_classes,
373        nesting_tokens,
374    })
375}
376
377fn specificity_witness(branch: &SyntaxNode) -> CanonicalSelectorSpecificityWitnessV0 {
378    let mut witness = CanonicalSelectorSpecificityWitnessV0 {
379        ids: 0,
380        classes: 0,
381        types: 0,
382        exact: true,
383    };
384    for node in branch.descendants() {
385        match node.kind() {
386            SyntaxKind::IdSelector => witness.ids = witness.ids.saturating_add(1),
387            SyntaxKind::ClassSelector
388            | SyntaxKind::AttributeSelector
389            | SyntaxKind::PseudoClassSelector => {
390                witness.classes = witness.classes.saturating_add(1)
391            }
392            SyntaxKind::TypeSelector | SyntaxKind::PseudoElementSelector => {
393                witness.types = witness.types.saturating_add(1)
394            }
395            SyntaxKind::PseudoSelectorArgument
396            | SyntaxKind::NthSelectorArgument
397            | SyntaxKind::BogusSelector
398            | SyntaxKind::BogusCompoundSelector => witness.exact = false,
399            _ => {}
400        }
401    }
402    witness
403}
404
405fn classify_combinator(text: &str) -> CanonicalSelectorCombinatorV0 {
406    match text.trim() {
407        "" => CanonicalSelectorCombinatorV0::Descendant,
408        ">" => CanonicalSelectorCombinatorV0::Child,
409        "+" => CanonicalSelectorCombinatorV0::NextSibling,
410        "~" => CanonicalSelectorCombinatorV0::SubsequentSibling,
411        "||" => CanonicalSelectorCombinatorV0::Column,
412        _ => CanonicalSelectorCombinatorV0::Other,
413    }
414}
415
416fn relative_range(node: &SyntaxNode, authority_start: usize) -> Option<Range<usize>> {
417    let start = byte_start(node).checked_sub(authority_start)?;
418    let end = byte_end(node).checked_sub(authority_start)?;
419    (start <= end).then_some(start..end)
420}
421
422fn byte_start(node: &SyntaxNode) -> usize {
423    u32::from(node.text_range().start()) as usize
424}
425
426fn byte_end(node: &SyntaxNode) -> usize {
427    u32::from(node.text_range().end()) as usize
428}
429
430fn leading_selector_trivia(selector_node: &SyntaxNode) -> (usize, String) {
431    let mut start = byte_start(selector_node);
432    let mut authored_parts = Vec::<String>::new();
433    let trivia_anchor = selector_node
434        .ancestors()
435        .find(|node| node.kind() == SyntaxKind::Rule)
436        .unwrap_or(selector_node);
437    let mut previous = trivia_anchor.prev_sibling_or_token();
438    while let Some(element) = previous {
439        let Some(token) = element.as_token() else {
440            break;
441        };
442        if !matches!(
443            token.kind(),
444            SyntaxKind::Whitespace | SyntaxKind::LineComment | SyntaxKind::BlockComment
445        ) {
446            break;
447        }
448        let Some(text) = token
449            .try_resolved()
450            .map(|resolved| resolved.text().to_string())
451        else {
452            break;
453        };
454        start = u32::from(token.text_range().start()) as usize;
455        authored_parts.push(text);
456        previous = element.prev_sibling_or_token();
457    }
458    authored_parts.reverse();
459    (start, authored_parts.concat())
460}
461
462fn syntax_node_text(node: &SyntaxNode) -> Option<String> {
463    node.try_resolved()
464        .map(|resolved| resolved.text().to_string())
465}