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