Skip to main content

omena_parser/public_product/
syntax_index.rs

1use std::collections::HashMap;
2
3use cstree::syntax::SyntaxNode;
4use omena_syntax::{SyntaxKind, css_keyword};
5
6use crate::{ParseResult, ParserByteSpanV0, StyleDialect, is_at_rule_node_kind, parse};
7
8/// One selector-bearing CST ancestor of a declaration.
9///
10/// `reset_to_root` is true only for the selector form of Sass `@at-root`.
11/// The selector members come from CST-owned rule boundaries rather than a
12/// declaration-string scan.
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct ParserDeclarationSelectorContextV0 {
15    pub reset_to_root: bool,
16    pub selector_members: Vec<String>,
17}
18
19/// CST-owned syntax projection for one declaration.
20#[derive(Debug, Clone, PartialEq, Eq)]
21pub struct ParserDeclarationSyntaxFactV0 {
22    pub byte_span: ParserByteSpanV0,
23    pub property_name: String,
24    pub value_span: ParserByteSpanV0,
25    pub value_text: String,
26    pub important: bool,
27    pub selector_contexts: Vec<ParserDeclarationSelectorContextV0>,
28    pub condition_contexts: Vec<String>,
29    pub source_order: usize,
30}
31
32#[derive(Default)]
33struct DeclarationContextCache {
34    selector_contexts: HashMap<(usize, usize), Option<ParserDeclarationSelectorContextV0>>,
35    condition_contexts: HashMap<(usize, usize), Option<String>>,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq)]
39struct CssModuleValueSyntaxV0 {
40    span: ParserByteSpanV0,
41    value_span: Option<ParserByteSpanV0>,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Default)]
45pub struct ProductSyntaxIndexV0 {
46    css_module_values: Vec<CssModuleValueSyntaxV0>,
47    scss_forward_rules: Vec<ParserByteSpanV0>,
48    keyframes_rules: Vec<ParserByteSpanV0>,
49    declarations: Vec<ParserDeclarationSyntaxFactV0>,
50    sass_parameter_lists: Vec<ParserByteSpanV0>,
51}
52
53impl ProductSyntaxIndexV0 {
54    pub fn new(source: &str, parsed: &ParseResult) -> Self {
55        let mut index = Self::default();
56        let mut declaration_source_order = 0usize;
57        let mut declaration_context_cache = DeclarationContextCache::default();
58        for node in parsed.syntax().descendants() {
59            match node.kind() {
60                SyntaxKind::CssModuleExportBlock | SyntaxKind::CssModuleImportBlock => {
61                    index.css_module_values.push(CssModuleValueSyntaxV0 {
62                        span: node_span(node),
63                        value_span: value_span_after_colon(node),
64                    });
65                }
66                SyntaxKind::ScssForwardRule => {
67                    index.scss_forward_rules.push(node_span(node));
68                }
69                SyntaxKind::KeyframesRule => {
70                    index.keyframes_rules.push(node_span(node));
71                }
72                SyntaxKind::Declaration | SyntaxKind::CustomPropertyDeclaration => {
73                    if let Some(declaration) = declaration_syntax(
74                        source,
75                        node,
76                        declaration_source_order,
77                        &mut declaration_context_cache,
78                    ) {
79                        index.declarations.push(declaration);
80                        declaration_source_order = declaration_source_order.saturating_add(1);
81                    }
82                }
83                SyntaxKind::ScssMixinDeclaration | SyntaxKind::ScssFunctionDeclaration => {
84                    if let Some(span) = parameter_list_span(node) {
85                        index.sass_parameter_lists.push(span);
86                    }
87                }
88                _ => {}
89            }
90        }
91        index
92    }
93
94    pub fn declarations(&self) -> &[ParserDeclarationSyntaxFactV0] {
95        self.declarations.as_slice()
96    }
97
98    /// Project declarations from an existing parse without building the
99    /// unrelated product-index families.
100    pub fn declarations_from_parse(
101        source: &str,
102        parsed: &ParseResult,
103    ) -> Vec<ParserDeclarationSyntaxFactV0> {
104        let mut declarations = Vec::new();
105        let root = parsed.syntax();
106        collect_declarations_from_subtree(
107            source,
108            &root,
109            &mut Vec::new(),
110            &mut Vec::new(),
111            &mut declarations,
112        );
113        declarations
114    }
115
116    /// Consume the index and return its declaration projection without
117    /// cloning declaration strings.
118    pub fn into_declarations(self) -> Vec<ParserDeclarationSyntaxFactV0> {
119        self.declarations
120    }
121
122    pub(super) fn css_module_value_span_for_offset(
123        &self,
124        offset: usize,
125    ) -> Option<ParserByteSpanV0> {
126        containing_span(
127            self.css_module_values
128                .iter()
129                .map(|definition| definition.span),
130            offset,
131        )
132    }
133
134    pub(super) fn css_module_value_text(&self, source: &str, offset: usize) -> Option<String> {
135        self.css_module_values
136            .iter()
137            .filter(|definition| span_contains_offset(definition.span, offset))
138            .min_by_key(|definition| span_len(definition.span))
139            .and_then(|definition| definition.value_span)
140            .and_then(|span| source.get(span.start..span.end))
141            .map(str::trim)
142            .map(ToString::to_string)
143    }
144
145    pub(super) fn scss_forward_span_for_offset(&self, offset: usize) -> Option<ParserByteSpanV0> {
146        containing_span(self.scss_forward_rules.iter().copied(), offset)
147    }
148
149    pub(super) fn keyframes_span_for_offset(&self, offset: usize) -> Option<ParserByteSpanV0> {
150        containing_span(self.keyframes_rules.iter().copied(), offset)
151    }
152
153    pub(super) fn declaration_span_for_offset(&self, offset: usize) -> Option<ParserByteSpanV0> {
154        containing_span(
155            self.declarations
156                .iter()
157                .map(|declaration| declaration.byte_span),
158            offset,
159        )
160    }
161
162    pub(super) fn declaration_property_name_for_offset(&self, offset: usize) -> Option<&str> {
163        self.declaration_for_offset(offset)
164            .map(|declaration| declaration.property_name.as_str())
165    }
166
167    pub(super) fn declaration_value_text(&self, source: &str, offset: usize) -> Option<String> {
168        let declaration = self.declaration_for_offset(offset)?;
169        source
170            .get(declaration.value_span.start..declaration.value_span.end)
171            .map(str::trim)
172            .map(ToString::to_string)
173    }
174
175    pub(super) fn sass_parameter_list_contains(&self, offset: usize) -> bool {
176        self.sass_parameter_lists
177            .iter()
178            .any(|span| span_contains_offset(*span, offset))
179    }
180
181    fn declaration_for_offset(&self, offset: usize) -> Option<&ParserDeclarationSyntaxFactV0> {
182        self.declarations
183            .iter()
184            .filter(|declaration| span_contains_offset(declaration.byte_span, offset))
185            .min_by_key(|declaration| span_len(declaration.byte_span))
186    }
187}
188
189/// Parse a stylesheet and project its declarations through the product syntax
190/// index. This is the additive consumer boundary for declaration-oriented
191/// products; callers do not need to reconstruct declaration boundaries.
192pub fn collect_parser_declaration_syntax_facts(
193    source: &str,
194    dialect: StyleDialect,
195) -> Vec<ParserDeclarationSyntaxFactV0> {
196    let parsed = parse(source, dialect);
197    ProductSyntaxIndexV0::declarations_from_parse(source, &parsed)
198}
199
200fn parameter_list_span(node: &SyntaxNode<SyntaxKind>) -> Option<ParserByteSpanV0> {
201    let mut depth = 0usize;
202    let mut start = None;
203    for token in node
204        .descendants_with_tokens()
205        .filter_map(|element| element.into_token())
206    {
207        let span = byte_span(token.text_range());
208        match token.kind() {
209            SyntaxKind::LeftParen => {
210                depth = depth.saturating_add(1);
211                if depth == 1 {
212                    start = Some(span.end);
213                }
214            }
215            SyntaxKind::RightParen if depth == 1 => {
216                return start.map(|start| ParserByteSpanV0 {
217                    start,
218                    end: span.start,
219                });
220            }
221            SyntaxKind::RightParen => depth = depth.saturating_sub(1),
222            SyntaxKind::LeftBrace if depth == 0 => return None,
223            _ => {}
224        }
225    }
226    None
227}
228
229fn declaration_syntax(
230    source: &str,
231    node: &SyntaxNode<SyntaxKind>,
232    source_order: usize,
233    context_cache: &mut DeclarationContextCache,
234) -> Option<ParserDeclarationSyntaxFactV0> {
235    let (selector_contexts, condition_contexts) = declaration_contexts(source, node, context_cache);
236    declaration_syntax_with_context(
237        source,
238        node,
239        source_order,
240        selector_contexts,
241        condition_contexts,
242    )
243}
244
245fn declaration_syntax_with_context(
246    source: &str,
247    node: &SyntaxNode<SyntaxKind>,
248    source_order: usize,
249    selector_contexts: Vec<ParserDeclarationSelectorContextV0>,
250    condition_contexts: Vec<String>,
251) -> Option<ParserDeclarationSyntaxFactV0> {
252    let mut declaration_span = node_span(node);
253    let declaration_end = declaration_span.end;
254    let mut colon = None;
255    let mut value_end = None;
256    let mut important_start = None;
257    let mut property_name = String::new();
258    let mut value_text = String::new();
259    for token in node
260        .descendants_with_tokens()
261        .filter_map(|element| element.into_token())
262    {
263        let token_span = byte_span(token.text_range());
264        if token.parent().kind() == SyntaxKind::ImportantAnnotation {
265            important_start.get_or_insert(token_span.start);
266        }
267        if colon.is_none() && token.kind() == SyntaxKind::Colon {
268            colon = Some(token_span);
269            continue;
270        }
271        if colon.is_some()
272            && value_end.is_none()
273            && matches!(
274                token.kind(),
275                SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
276            )
277        {
278            value_end = Some(token_span.start);
279        }
280        if token.kind() == SyntaxKind::Semicolon {
281            declaration_span.end = declaration_span.end.min(token_span.start);
282        }
283
284        let target = if colon.is_none() {
285            Some(&mut property_name)
286        } else if value_end.is_none() && important_start.is_none() {
287            Some(&mut value_text)
288        } else {
289            None
290        };
291        if let Some(target) = target {
292            append_normalized_declaration_token(source, token.kind(), token_span, target);
293        }
294    }
295    let colon = colon?;
296    let value_span = ParserByteSpanV0 {
297        start: colon.end,
298        end: value_end
299            .unwrap_or(declaration_end)
300            .min(important_start.unwrap_or(usize::MAX)),
301    };
302    property_name = property_name.trim().to_ascii_lowercase();
303    let value_text = value_text.trim().to_string();
304    (!property_name.is_empty()).then_some(ParserDeclarationSyntaxFactV0 {
305        byte_span: declaration_span,
306        property_name,
307        value_span,
308        value_text,
309        important: important_start.is_some(),
310        selector_contexts,
311        condition_contexts,
312        source_order,
313    })
314}
315
316fn collect_declarations_from_subtree(
317    source: &str,
318    node: &SyntaxNode<SyntaxKind>,
319    selector_contexts: &mut Vec<ParserDeclarationSelectorContextV0>,
320    condition_contexts: &mut Vec<String>,
321    declarations: &mut Vec<ParserDeclarationSyntaxFactV0>,
322) {
323    if matches!(
324        node.kind(),
325        SyntaxKind::Declaration | SyntaxKind::CustomPropertyDeclaration
326    ) {
327        if let Some(declaration) = declaration_syntax_with_context(
328            source,
329            node,
330            declarations.len(),
331            selector_contexts.clone(),
332            condition_contexts.clone(),
333        ) {
334            declarations.push(declaration);
335        }
336        return;
337    }
338
339    let selector_pushed = selector_context_for_node(source, node).is_some_and(|context| {
340        selector_contexts.push(context);
341        true
342    });
343    let condition_context = condition_context_for_node(source, node);
344    let condition_pushed = condition_context
345        .filter(|context| condition_contexts.last() != Some(context))
346        .is_some_and(|context| {
347            condition_contexts.push(context);
348            true
349        });
350
351    for child in node.children() {
352        collect_declarations_from_subtree(
353            source,
354            child,
355            selector_contexts,
356            condition_contexts,
357            declarations,
358        );
359    }
360
361    if condition_pushed {
362        condition_contexts.pop();
363    }
364    if selector_pushed {
365        selector_contexts.pop();
366    }
367}
368
369fn selector_context_for_node(
370    source: &str,
371    node: &SyntaxNode<SyntaxKind>,
372) -> Option<ParserDeclarationSelectorContextV0> {
373    let (reset_to_root, selector_members) = match node.kind() {
374        SyntaxKind::Rule | SyntaxKind::NestRule => (false, selector_members_for_rule(source, node)),
375        SyntaxKind::ScssAtRootRule => (true, at_root_selector_members(source, node)),
376        _ => return None,
377    };
378    (!selector_members.is_empty()).then_some(ParserDeclarationSelectorContextV0 {
379        reset_to_root,
380        selector_members,
381    })
382}
383
384fn condition_context_for_node(source: &str, node: &SyntaxNode<SyntaxKind>) -> Option<String> {
385    if !is_at_rule_node_kind(node.kind())
386        || matches!(
387            node.kind(),
388            SyntaxKind::LayerRule | SyntaxKind::ScssAtRootRule | SyntaxKind::NestRule
389        )
390    {
391        return None;
392    }
393    block_header_text(source, node)
394        .map(|header| header.split_whitespace().collect::<Vec<_>>().join(" "))
395        .filter(|header| !is_non_condition_wrapper_header(header))
396        .filter(|header| !header.is_empty())
397}
398
399fn declaration_contexts(
400    source: &str,
401    node: &SyntaxNode<SyntaxKind>,
402    cache: &mut DeclarationContextCache,
403) -> (Vec<ParserDeclarationSelectorContextV0>, Vec<String>) {
404    let mut ancestors = node.ancestors().skip(1).collect::<Vec<_>>();
405    ancestors.reverse();
406    let mut selector_contexts = Vec::new();
407    let mut condition_contexts = Vec::new();
408    for ancestor in ancestors {
409        let span = node_span(ancestor);
410        let key = (span.start, span.end);
411        if matches!(
412            ancestor.kind(),
413            SyntaxKind::Rule | SyntaxKind::NestRule | SyntaxKind::ScssAtRootRule
414        ) {
415            let context = cache
416                .selector_contexts
417                .entry(key)
418                .or_insert_with(|| match ancestor.kind() {
419                    SyntaxKind::Rule | SyntaxKind::NestRule => {
420                        let selector_members = selector_members_for_rule(source, ancestor);
421                        (!selector_members.is_empty()).then_some(
422                            ParserDeclarationSelectorContextV0 {
423                                reset_to_root: false,
424                                selector_members,
425                            },
426                        )
427                    }
428                    SyntaxKind::ScssAtRootRule => {
429                        let selector_members = at_root_selector_members(source, ancestor);
430                        (!selector_members.is_empty()).then_some(
431                            ParserDeclarationSelectorContextV0 {
432                                reset_to_root: true,
433                                selector_members,
434                            },
435                        )
436                    }
437                    _ => None,
438                })
439                .clone();
440            if let Some(context) = context {
441                selector_contexts.push(context);
442            }
443        }
444        if is_at_rule_node_kind(ancestor.kind())
445            && !matches!(
446                ancestor.kind(),
447                SyntaxKind::LayerRule | SyntaxKind::ScssAtRootRule | SyntaxKind::NestRule
448            )
449        {
450            let context = cache
451                .condition_contexts
452                .entry(key)
453                .or_insert_with(|| {
454                    block_header_text(source, ancestor)
455                        .map(|header| header.split_whitespace().collect::<Vec<_>>().join(" "))
456                        .filter(|header| !is_non_condition_wrapper_header(header))
457                        .filter(|header| !header.is_empty())
458                })
459                .clone();
460            if let Some(context) = context
461                && condition_contexts.last() != Some(&context)
462            {
463                condition_contexts.push(context);
464            }
465        }
466    }
467    (selector_contexts, condition_contexts)
468}
469
470fn selector_members_for_rule(source: &str, node: &SyntaxNode<SyntaxKind>) -> Vec<String> {
471    let Some(selector_list) = node.children().find(|child| {
472        matches!(
473            child.kind(),
474            SyntaxKind::SelectorList
475                | SyntaxKind::RelativeSelectorList
476                | SyntaxKind::BogusSelectorList
477        )
478    }) else {
479        return Vec::new();
480    };
481    selector_list
482        .children()
483        .filter(|child| {
484            matches!(
485                child.kind(),
486                SyntaxKind::Selector | SyntaxKind::RelativeSelector | SyntaxKind::BogusSelector
487            )
488        })
489        .filter_map(|selector| source_text_for_node(source, selector))
490        .map(|selector| selector.trim().to_string())
491        .filter(|selector| !selector.is_empty())
492        .collect()
493}
494
495fn at_root_selector_members(source: &str, node: &SyntaxNode<SyntaxKind>) -> Vec<String> {
496    let Some(header) = block_header_text(source, node) else {
497        return Vec::new();
498    };
499    let Some(rest) = css_keyword(header.trim_start()).strip_prefix("@at-root") else {
500        return Vec::new();
501    };
502    if let Some(next) = rest.chars().next()
503        && !next.is_ascii_whitespace()
504    {
505        return Vec::new();
506    }
507    let selector = rest.trim();
508    if selector.is_empty() || selector.starts_with('(') {
509        Vec::new()
510    } else {
511        vec![selector.to_string()]
512    }
513}
514
515fn is_non_condition_wrapper_header(header: &str) -> bool {
516    ["@layer", "@at-root", "@nest"]
517        .into_iter()
518        .any(|keyword| at_rule_header_has_keyword(header, keyword))
519}
520
521fn at_rule_header_has_keyword(header: &str, keyword: &str) -> bool {
522    let Some(rest) = css_keyword(header.trim_start()).strip_prefix(keyword) else {
523        return false;
524    };
525    rest.is_empty() || rest.chars().next().is_some_and(char::is_whitespace)
526}
527
528fn append_normalized_declaration_token(
529    source: &str,
530    kind: SyntaxKind,
531    span: ParserByteSpanV0,
532    target: &mut String,
533) {
534    if matches!(
535        kind,
536        SyntaxKind::BlockComment | SyntaxKind::LineComment | SyntaxKind::ScssSilentComment
537    ) {
538        if !target.ends_with(char::is_whitespace) {
539            target.push(' ');
540        }
541    } else if let Some(token_text) = source.get(span.start..span.end) {
542        target.push_str(token_text);
543    }
544}
545
546fn block_header_text<'a>(source: &'a str, node: &SyntaxNode<SyntaxKind>) -> Option<&'a str> {
547    let open = node
548        .descendants_with_tokens()
549        .filter_map(|element| element.into_token())
550        .find(|token| token.kind() == SyntaxKind::LeftBrace)
551        .map(|token| byte_span(token.text_range()))?;
552    source.get(node_span(node).start..open.start)
553}
554
555fn source_text_for_node<'a>(source: &'a str, node: &SyntaxNode<SyntaxKind>) -> Option<&'a str> {
556    let span = node_span(node);
557    source.get(span.start..span.end)
558}
559
560fn value_span_after_colon(node: &SyntaxNode<SyntaxKind>) -> Option<ParserByteSpanV0> {
561    let mut colon_end = None;
562    let mut value_end = None;
563    for token in node
564        .descendants_with_tokens()
565        .filter_map(|element| element.into_token())
566    {
567        let span = byte_span(token.text_range());
568        if colon_end.is_none() && token.kind() == SyntaxKind::Colon {
569            colon_end = Some(span.end);
570            continue;
571        }
572        if colon_end.is_some()
573            && matches!(
574                token.kind(),
575                SyntaxKind::Semicolon | SyntaxKind::SassOptionalSemicolon
576            )
577        {
578            value_end = Some(span.start);
579            break;
580        }
581    }
582    let start = colon_end?;
583    let end = value_end.unwrap_or_else(|| node_span(node).end);
584    (start <= end).then_some(ParserByteSpanV0 { start, end })
585}
586
587fn containing_span(
588    spans: impl Iterator<Item = ParserByteSpanV0>,
589    offset: usize,
590) -> Option<ParserByteSpanV0> {
591    spans
592        .filter(|span| span_contains_offset(*span, offset))
593        .min_by_key(|span| span_len(*span))
594}
595
596fn span_contains_offset(span: ParserByteSpanV0, offset: usize) -> bool {
597    span.start <= offset && offset < span.end
598}
599
600fn span_len(span: ParserByteSpanV0) -> usize {
601    span.end.saturating_sub(span.start)
602}
603
604fn node_span(node: &SyntaxNode<SyntaxKind>) -> ParserByteSpanV0 {
605    byte_span(node.text_range())
606}
607
608fn byte_span(range: cstree::text::TextRange) -> ParserByteSpanV0 {
609    ParserByteSpanV0 {
610        start: u32::from(range.start()) as usize,
611        end: u32::from(range.end()) as usize,
612    }
613}