Skip to main content

omena_parser/public_product/
syntax_index.rs

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