Skip to main content

omena_parser/facts/
emission_selectors.rs

1//! Parser facts for selectors that are absent from the legacy named-selector surface.
2
3use cstree::text::TextRange;
4use omena_syntax::SyntaxKind;
5
6use crate::ParseResult;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
9#[non_exhaustive]
10pub enum ParsedEmissionSelectorFactKindV0 {
11    Element,
12    Attribute,
13    Universal,
14    PseudoClass,
15    PseudoElement,
16}
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct ParsedEmissionSelectorFactV0 {
21    pub kind: ParsedEmissionSelectorFactKindV0,
22    pub name: String,
23    pub range: TextRange,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq)]
27#[non_exhaustive]
28pub struct ParsedEmissionSelectorFactsV0 {
29    pub selectors: Vec<ParsedEmissionSelectorFactV0>,
30}
31
32pub fn collect_emission_selector_facts_from_cst(
33    text: &str,
34    parsed: &ParseResult,
35) -> ParsedEmissionSelectorFactsV0 {
36    let mut selectors = parsed
37        .syntax()
38        .descendants()
39        .filter_map(|node| {
40            let kind = match node.kind() {
41                SyntaxKind::TypeSelector => ParsedEmissionSelectorFactKindV0::Element,
42                SyntaxKind::AttributeSelector => ParsedEmissionSelectorFactKindV0::Attribute,
43                SyntaxKind::UniversalSelector => ParsedEmissionSelectorFactKindV0::Universal,
44                SyntaxKind::PseudoClassSelector => ParsedEmissionSelectorFactKindV0::PseudoClass,
45                SyntaxKind::PseudoElementSelector => {
46                    ParsedEmissionSelectorFactKindV0::PseudoElement
47                }
48                _ => return None,
49            };
50            let range = node.text_range();
51            let start = u32::from(range.start()) as usize;
52            let end = u32::from(range.end()) as usize;
53            Some(ParsedEmissionSelectorFactV0 {
54                kind,
55                name: text.get(start..end).unwrap_or_default().to_string(),
56                range,
57            })
58        })
59        .collect::<Vec<_>>();
60    selectors.sort_by_key(|selector| {
61        (
62            u32::from(selector.range.start()),
63            u32::from(selector.range.end()),
64            selector.kind,
65            selector.name.clone(),
66        )
67    });
68    ParsedEmissionSelectorFactsV0 { selectors }
69}
70
71#[cfg(test)]
72mod tests {
73    use super::{ParsedEmissionSelectorFactKindV0, collect_emission_selector_facts_from_cst};
74    use crate::{
75        StyleDialect, collect_style_fact_collection, parse, with_omena_parser_parse_instrumentation,
76    };
77
78    #[test]
79    fn collects_structural_selectors_absent_from_the_named_selector_surface() {
80        let source = "div, [data-state='open'], svg|*, :root, ::before { color: red; }";
81        let parsed = parse(source, StyleDialect::Css);
82        let facts = collect_emission_selector_facts_from_cst(source, &parsed);
83
84        assert_eq!(
85            facts
86                .selectors
87                .iter()
88                .map(|selector| (selector.kind, selector.name.as_str()))
89                .collect::<Vec<_>>(),
90            vec![
91                (ParsedEmissionSelectorFactKindV0::Element, "div"),
92                (
93                    ParsedEmissionSelectorFactKindV0::Attribute,
94                    "[data-state='open']"
95                ),
96                (ParsedEmissionSelectorFactKindV0::Universal, "svg|*"),
97                (ParsedEmissionSelectorFactKindV0::PseudoClass, ":root"),
98                (ParsedEmissionSelectorFactKindV0::PseudoElement, "::before"),
99            ]
100        );
101    }
102
103    #[test]
104    fn combined_fact_collection_parses_once() {
105        let (collection, snapshot) = with_omena_parser_parse_instrumentation(|| {
106            collect_style_fact_collection("div { color: red; }", StyleDialect::Css)
107        });
108
109        assert_eq!(snapshot.parse_invocation_count, 1);
110        assert!(collection.facts.selectors.is_empty());
111        assert_eq!(collection.emission_selectors.selectors.len(), 1);
112    }
113}