Skip to main content

omena_semantic/
lib.rs

1//! Semantic fact layer for parsed omena-css style modules.
2//!
3//! This crate lifts parser facts into selector, custom-property, Sass module,
4//! design-token, and source-evidence summaries. It is the bridge between the
5//! lossless parser substrate and query/LSP consumers that need stable semantic
6//! contracts rather than raw CST traversal.
7
8use engine_input_producers::EngineInputV2;
9use omena_cascade::selector_context_witness_for_declaration;
10use omena_interner::{
11    intern_class_name, intern_css_ident, intern_custom_property_name, intern_keyframes_name,
12    intern_mixin_name,
13};
14use omena_parser::{
15    ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind, ParsedCssModuleComposesFactKind,
16    ParsedCssModuleValueFactKind, ParsedSassModuleEdgeFactKind, ParsedSassSymbolFactKind,
17    ParsedSelectorFactKind, ParsedStyleFacts, ParsedVariableFactKind, StyleDialect,
18    collect_style_facts, parse,
19};
20use serde::Serialize;
21use std::collections::BTreeSet;
22
23mod css_modules;
24mod design_tokens;
25mod evidence;
26mod lossless_cst;
27mod observation;
28mod selector_identity;
29mod selector_references;
30mod source_evidence;
31mod types;
32
33pub use css_modules::{
34    CssModulesSemanticCapabilitiesV0, CssModulesSemanticSummaryV0, summarize_css_modules_semantics,
35    summarize_css_modules_semantics_from_source,
36};
37pub use design_tokens::{
38    DesignTokenCascadeRankingSignalV0, DesignTokenContextSignalV0,
39    DesignTokenExternalDeclarationCandidateScopeV0, DesignTokenRankedReferenceV0,
40    DesignTokenResolutionSignalV0, DesignTokenSemanticCapabilitiesV0, DesignTokenSemanticSummaryV0,
41    DesignTokenWorkspaceDeclarationFactV0, collect_design_token_workspace_declarations,
42    summarize_design_token_semantics,
43    summarize_design_token_semantics_with_scoped_workspace_declarations,
44    summarize_design_token_semantics_with_workspace_declarations,
45};
46pub use evidence::{
47    SemanticPromotionEvidenceItemV0, SemanticPromotionEvidenceSummaryV0,
48    summarize_semantic_promotion_evidence, summarize_semantic_promotion_evidence_with_source_input,
49};
50pub use lossless_cst::{
51    LosslessCstConsumerReadinessV0, LosslessCstContractV0, LosslessCstSpanInvariantsV0,
52    summarize_lossless_cst_contract,
53};
54pub use observation::{
55    SelectorIdentityObservationV0, SemanticCouplingBoundaryObservationV0,
56    SemanticGraphDownstreamReadinessV0, SourceEvidenceObservationV0, TheoryObservationContractV0,
57    TheoryObservationHarnessInput, TheoryObservationHarnessSummaryV0,
58    summarize_theory_observation_contract, summarize_theory_observation_harness,
59};
60pub use selector_identity::{
61    SelectorCanonicalIdentityV0, SelectorIdentityEngineSummaryV0, SelectorIdentityRewriteSafetyV0,
62    summarize_selector_identity_engine,
63};
64pub use selector_references::{
65    SelectorEditableDirectReferenceSiteV0, SelectorReferenceEngineSummaryV0,
66    SelectorReferenceSiteV0, SelectorReferenceSummaryV0, summarize_selector_reference_engine,
67};
68pub use source_evidence::{
69    BindingOriginEvidenceV0, CertaintyReasonEvidenceV0, ReferenceSiteIdentityEvidenceV0,
70    SourceInputPromotionEvidenceSummaryV0, StyleModuleEdgeEvidenceV0,
71    ValueDomainExplanationEvidenceV0, summarize_source_input_evidence,
72};
73pub use types::{
74    NestedSafetyCountsV0, ParserBoundarySyntaxFactsV0, ParserByteSpanV0,
75    ParserIndexComposesFactsV0, ParserIndexCustomPropertyDeclFactV0,
76    ParserIndexCustomPropertyFactsV0, ParserIndexCustomPropertyRefFactV0,
77    ParserIndexKeyframesFactsV0, ParserIndexSassModuleUseFactV0,
78    ParserIndexSassSameFileResolutionFactsV0, ParserIndexSelectorDefinitionFactV0,
79    ParserIndexSelectorFactsV0, ParserIndexValueFactsV0, ParserIndexWrapperFactsV0,
80    ParserLosslessCstFactsV0, ParserPositionV0, ParserRangeV0, ParserSassSyntaxFactsV0,
81    StyleContainerIndexV0, StyleContextBlockV0, StyleContextIndexV0,
82    StyleContextSelectorMembershipV0, StyleCustomPropertySemanticFactsV0, StyleLayerIndexV0,
83    StyleLayerStatementV0, StyleSassSemanticFactsV0, StyleScopeIndexV0,
84    StyleSelectorIdentityFactsV0, StyleSemanticFactsV0, Stylesheet,
85};
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct StyleSemanticBoundarySummaryV0 {
90    pub schema_version: &'static str,
91    pub language: &'static str,
92    pub parser_facts: ParserBoundarySyntaxFactsV0,
93    pub semantic_facts: StyleSemanticFactsV0,
94    pub design_token_semantics: DesignTokenSemanticSummaryV0,
95    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
96    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
97    pub lossless_cst_contract: LosslessCstContractV0,
98}
99
100#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
101#[serde(rename_all = "camelCase")]
102pub struct StyleSemanticGraphSummaryV0 {
103    pub schema_version: &'static str,
104    pub product: &'static str,
105    pub language: &'static str,
106    pub parser_facts: ParserBoundarySyntaxFactsV0,
107    pub semantic_facts: StyleSemanticFactsV0,
108    pub css_modules_semantics: CssModulesSemanticSummaryV0,
109    pub design_token_semantics: DesignTokenSemanticSummaryV0,
110    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
111    pub selector_reference_engine: SelectorReferenceEngineSummaryV0,
112    pub source_input_evidence: SourceInputPromotionEvidenceSummaryV0,
113    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
114    pub lossless_cst_contract: LosslessCstContractV0,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct StyleSemanticSoaTablesV0 {
120    pub schema_version: &'static str,
121    pub product: &'static str,
122    pub selector_names: SemanticNameSoaTableV0,
123    pub custom_property_names: SemanticNameSoaTableV0,
124    pub sass_names: SemanticNameSoaTableV0,
125    pub total_row_count: usize,
126    pub interned_row_count: usize,
127    pub ready_surfaces: Vec<&'static str>,
128}
129
130#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
131#[serde(rename_all = "camelCase")]
132pub struct SemanticNameSoaTableV0 {
133    pub table_name: &'static str,
134    pub name_kind: &'static str,
135    pub row_indices: Vec<usize>,
136    pub names: Vec<String>,
137    pub interned_row_count: usize,
138    pub unique_name_count: usize,
139}
140
141pub fn summarize_style_semantic_boundary(sheet: &Stylesheet) -> StyleSemanticBoundarySummaryV0 {
142    summarize_omena_parser_style_semantic_boundary_from_source(&sheet.path, &sheet.source)
143}
144
145pub fn summarize_style_semantic_graph(
146    sheet: &Stylesheet,
147    input: &EngineInputV2,
148) -> StyleSemanticGraphSummaryV0 {
149    summarize_style_semantic_graph_for_path(sheet, input, None)
150}
151
152pub fn summarize_style_semantic_graph_for_path(
153    sheet: &Stylesheet,
154    input: &EngineInputV2,
155    style_path: Option<&str>,
156) -> StyleSemanticGraphSummaryV0 {
157    summarize_style_semantic_graph_for_path_with_workspace_declarations(
158        sheet,
159        input,
160        style_path,
161        &[],
162    )
163}
164
165pub fn summarize_style_semantic_graph_for_path_with_workspace_declarations(
166    sheet: &Stylesheet,
167    input: &EngineInputV2,
168    style_path: Option<&str>,
169    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
170) -> StyleSemanticGraphSummaryV0 {
171    let boundary = summarize_style_semantic_boundary(sheet);
172    let parser_facts = boundary.parser_facts;
173    let semantic_facts = boundary.semantic_facts;
174    let effective_style_path = style_path.or(Some(sheet.path.as_str()));
175    let design_token_semantics = summarize_design_token_semantics_with_workspace_declarations(
176        &parser_facts,
177        &semantic_facts,
178        effective_style_path,
179        workspace_declarations,
180    );
181    let css_modules_semantics = summarize_css_modules_semantics(sheet);
182    let selector_identity_engine =
183        summarize_selector_identity_engine(&semantic_facts.selector_identity);
184    let selector_reference_engine = summarize_selector_reference_engine(input, style_path);
185    let source_input_evidence = summarize_source_input_evidence(input);
186    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
187        &parser_facts,
188        &semantic_facts,
189        input,
190    );
191    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
192
193    StyleSemanticGraphSummaryV0 {
194        schema_version: "0",
195        product: "omena-semantic.style-semantic-graph",
196        language: boundary.language,
197        parser_facts,
198        semantic_facts,
199        css_modules_semantics,
200        design_token_semantics,
201        selector_identity_engine,
202        selector_reference_engine,
203        source_input_evidence,
204        promotion_evidence,
205        lossless_cst_contract,
206    }
207}
208
209pub fn summarize_style_semantic_graph_from_source(
210    style_path: &str,
211    style_source: &str,
212    input: &EngineInputV2,
213) -> Option<StyleSemanticGraphSummaryV0> {
214    let css_modules_semantics =
215        summarize_css_modules_semantics_from_source(style_path, style_source)?;
216    let boundary =
217        summarize_omena_parser_style_semantic_boundary_from_source(style_path, style_source);
218    let parser_facts = boundary.parser_facts;
219    let semantic_facts = boundary.semantic_facts;
220    let selector_reference_engine = summarize_selector_reference_engine(input, Some(style_path));
221    let source_input_evidence = summarize_source_input_evidence(input);
222    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
223        &parser_facts,
224        &semantic_facts,
225        input,
226    );
227
228    Some(StyleSemanticGraphSummaryV0 {
229        schema_version: "0",
230        product: "omena-semantic.style-semantic-graph",
231        language: boundary.language,
232        parser_facts,
233        semantic_facts,
234        css_modules_semantics,
235        design_token_semantics: boundary.design_token_semantics,
236        selector_identity_engine: boundary.selector_identity_engine,
237        selector_reference_engine,
238        source_input_evidence,
239        promotion_evidence,
240        lossless_cst_contract: boundary.lossless_cst_contract,
241    })
242}
243
244pub fn summarize_style_semantic_facts(sheet: &Stylesheet) -> StyleSemanticFactsV0 {
245    summarize_style_semantic_boundary(sheet).semantic_facts
246}
247
248pub fn summarize_style_semantic_soa_tables(
249    semantic_facts: &StyleSemanticFactsV0,
250    db: &dyn salsa::Database,
251) -> StyleSemanticSoaTablesV0 {
252    let selector_names = semantic_name_soa_table(
253        "selectors",
254        "className",
255        semantic_facts.selector_identity.canonical_names.as_slice(),
256        |name| intern_class_name(db, name).is_ok(),
257    );
258    let custom_property_names = semantic_name_soa_table(
259        "customProperties",
260        "customPropertyName",
261        semantic_facts.custom_properties.decl_names.as_slice(),
262        |name| intern_custom_property_name(db, name).is_ok(),
263    );
264    let mut sass_name_sources = Vec::new();
265    sass_name_sources.extend(
266        semantic_facts
267            .sass
268            .same_file_resolution
269            .resolved_variable_ref_names
270            .iter()
271            .cloned(),
272    );
273    sass_name_sources.extend(
274        semantic_facts
275            .sass
276            .same_file_resolution
277            .unresolved_variable_ref_names
278            .iter()
279            .cloned(),
280    );
281    sass_name_sources.extend(
282        semantic_facts
283            .sass
284            .same_file_resolution
285            .resolved_mixin_include_names
286            .iter()
287            .cloned(),
288    );
289    sass_name_sources.extend(
290        semantic_facts
291            .sass
292            .same_file_resolution
293            .unresolved_mixin_include_names
294            .iter()
295            .cloned(),
296    );
297    sass_name_sources.extend(
298        semantic_facts
299            .sass
300            .same_file_resolution
301            .resolved_function_call_names
302            .iter()
303            .cloned(),
304    );
305    let sass_names =
306        semantic_name_soa_table("sass", "cssIdentOrMixinName", &sass_name_sources, |name| {
307            intern_css_ident(db, name).is_ok()
308                || intern_mixin_name(db, name).is_ok()
309                || intern_keyframes_name(db, name).is_ok()
310        });
311    let total_row_count = selector_names.row_indices.len()
312        + custom_property_names.row_indices.len()
313        + sass_names.row_indices.len();
314    let interned_row_count = selector_names.interned_row_count
315        + custom_property_names.interned_row_count
316        + sass_names.interned_row_count;
317
318    StyleSemanticSoaTablesV0 {
319        schema_version: "0",
320        product: "omena-semantic.soa-tables",
321        selector_names,
322        custom_property_names,
323        sass_names,
324        total_row_count,
325        interned_row_count,
326        ready_surfaces: vec!["semanticSoaTables", "semanticSoaNameTables"],
327    }
328}
329
330fn semantic_name_soa_table(
331    table_name: &'static str,
332    name_kind: &'static str,
333    names: &[String],
334    mut intern: impl FnMut(&str) -> bool,
335) -> SemanticNameSoaTableV0 {
336    let mut unique_names = BTreeSet::new();
337    let mut interned_row_count = 0usize;
338    for name in names {
339        unique_names.insert(name.clone());
340        if intern(name) {
341            interned_row_count += 1;
342        }
343    }
344
345    SemanticNameSoaTableV0 {
346        table_name,
347        name_kind,
348        row_indices: (0..names.len()).collect(),
349        names: names.to_vec(),
350        interned_row_count,
351        unique_name_count: unique_names.len(),
352    }
353}
354
355pub fn summarize_parser_contract_facts(sheet: &Stylesheet) -> ParserBoundarySyntaxFactsV0 {
356    summarize_style_semantic_boundary(sheet).parser_facts
357}
358
359pub fn parse_style_module(path: &str, source: &str) -> Option<Stylesheet> {
360    Some(Stylesheet {
361        path: path.to_string(),
362        language: dialect_for_style_path(path)?,
363        source: source.to_string(),
364    })
365}
366
367pub fn summarize_omena_parser_style_semantic_boundary_from_source(
368    style_path: &str,
369    style_source: &str,
370) -> StyleSemanticBoundarySummaryV0 {
371    let dialect = omena_parser_dialect_for_style_path(style_path);
372    let parsed = parse(style_source, dialect);
373    let facts = collect_style_facts(style_source, dialect);
374    let parser_facts = summarize_omena_parser_contract_facts(
375        style_source,
376        parsed.token_count(),
377        parsed.syntax().children().count(),
378        parsed.errors().len(),
379        &facts,
380    );
381    let semantic_facts = summarize_omena_parser_semantic_facts(style_source, &facts, &parser_facts);
382    let design_token_semantics = summarize_design_token_semantics(&parser_facts, &semantic_facts);
383    let selector_identity_engine =
384        summarize_selector_identity_engine(&semantic_facts.selector_identity);
385    let promotion_evidence = summarize_semantic_promotion_evidence(&parser_facts, &semantic_facts);
386    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
387
388    StyleSemanticBoundarySummaryV0 {
389        schema_version: "0",
390        language: omena_parser_dialect_label(dialect),
391        parser_facts,
392        semantic_facts,
393        design_token_semantics,
394        selector_identity_engine,
395        promotion_evidence,
396        lossless_cst_contract,
397    }
398}
399
400fn summarize_omena_parser_contract_facts(
401    source: &str,
402    token_count: usize,
403    root_node_count: usize,
404    diagnostic_count: usize,
405    facts: &ParsedStyleFacts,
406) -> ParserBoundarySyntaxFactsV0 {
407    ParserBoundarySyntaxFactsV0 {
408        lossless_cst: ParserLosslessCstFactsV0 {
409            source_byte_len: source.len(),
410            token_count,
411            root_node_count,
412            diagnostic_count,
413            all_token_spans_within_source: true,
414            all_node_spans_within_source: true,
415        },
416        selectors: summarize_omena_parser_selector_facts(source, facts),
417        values: summarize_omena_parser_value_facts(facts),
418        custom_properties: summarize_omena_parser_custom_property_facts(source, facts),
419        sass: summarize_omena_parser_sass_syntax_facts(facts),
420        keyframes: summarize_omena_parser_keyframe_facts(facts),
421        composes: summarize_omena_parser_composes_facts(facts),
422        wrappers: ParserIndexWrapperFactsV0::default(),
423    }
424}
425
426fn summarize_omena_parser_semantic_facts(
427    source: &str,
428    facts: &ParsedStyleFacts,
429    parser_facts: &ParserBoundarySyntaxFactsV0,
430) -> StyleSemanticFactsV0 {
431    let custom_properties =
432        summarize_omena_parser_custom_property_semantic_facts(&parser_facts.custom_properties);
433    let sass_same_file_resolution =
434        summarize_omena_parser_sass_same_file_resolution(&parser_facts.sass);
435    let sass_selector_resolution =
436        summarize_omena_parser_sass_selector_resolution(source, facts, &sass_same_file_resolution);
437    StyleSemanticFactsV0 {
438        selector_identity: StyleSelectorIdentityFactsV0 {
439            canonical_names: parser_facts.selectors.names.clone(),
440            bem_suffix_safe_names: parser_facts.selectors.bem_suffix_safe_names.clone(),
441            bem_suffix_parent_names: parser_facts.selectors.bem_suffix_parent_names.clone(),
442            nested_unsafe_names: parser_facts.selectors.nested_unsafe_names.clone(),
443            nested_safety_counts: parser_facts.selectors.nested_safety_counts.clone(),
444        },
445        custom_properties,
446        sass: StyleSassSemanticFactsV0 {
447            selector_symbol_facts: Vec::new(),
448            selectors_with_resolved_variable_refs_names: sass_selector_resolution
449                .resolved_variable_ref_selectors,
450            selectors_with_unresolved_variable_refs_names: sass_selector_resolution
451                .unresolved_variable_ref_selectors,
452            selectors_with_resolved_mixin_includes_names: sass_selector_resolution
453                .resolved_mixin_include_selectors,
454            selectors_with_unresolved_mixin_includes_names: sass_selector_resolution
455                .unresolved_mixin_include_selectors,
456            selectors_with_function_calls_names: parser_facts.sass.function_call_names.clone(),
457            same_file_resolution: sass_same_file_resolution,
458        },
459        context_index: summarize_style_context_index(source),
460    }
461}
462
463fn summarize_style_context_index(source: &str) -> StyleContextIndexV0 {
464    let layer_statements = collect_layer_statement_facts(source);
465    let (context_blocks, memberships) = collect_style_context_blocks_and_memberships(source);
466    let block_layers = context_blocks
467        .iter()
468        .filter(|block| block.kind == "layer")
469        .cloned()
470        .collect::<Vec<_>>();
471    let containers = context_blocks
472        .iter()
473        .filter(|block| block.kind == "container")
474        .cloned()
475        .collect::<Vec<_>>();
476    let scopes = context_blocks
477        .iter()
478        .filter(|block| block.kind == "scope")
479        .cloned()
480        .collect::<Vec<_>>();
481    let layer_memberships = memberships
482        .iter()
483        .filter(|membership| membership.context_kind == "layer")
484        .cloned()
485        .collect::<Vec<_>>();
486    let container_memberships = memberships
487        .iter()
488        .filter(|membership| membership.context_kind == "container")
489        .cloned()
490        .collect::<Vec<_>>();
491    let scope_memberships = memberships
492        .iter()
493        .filter(|membership| membership.context_kind == "scope")
494        .cloned()
495        .collect::<Vec<_>>();
496    let named_layer_count = layer_statements
497        .iter()
498        .map(|statement| statement.name.clone())
499        .chain(block_layers.iter().filter_map(|block| block.name.clone()))
500        .collect::<BTreeSet<_>>()
501        .len();
502
503    StyleContextIndexV0 {
504        schema_version: "0",
505        product: "omena-semantic.style-context-index",
506        layer_index: StyleLayerIndexV0 {
507            statement_layers: layer_statements,
508            anonymous_layer_block_count: block_layers
509                .iter()
510                .filter(|block| block.name.is_none())
511                .count(),
512            block_layers,
513            selector_memberships: layer_memberships,
514            named_layer_count,
515        },
516        container_index: StyleContainerIndexV0 {
517            named_container_count: containers
518                .iter()
519                .filter(|block| block.name.is_some())
520                .count(),
521            anonymous_container_count: containers
522                .iter()
523                .filter(|block| block.name.is_none())
524                .count(),
525            containers,
526            selector_memberships: container_memberships,
527        },
528        scope_index: StyleScopeIndexV0 {
529            scoped_selector_count: scope_memberships
530                .iter()
531                .map(|membership| membership.selector_name.as_str())
532                .collect::<BTreeSet<_>>()
533                .len(),
534            scopes,
535            selector_memberships: scope_memberships,
536        },
537        selector_context_count: memberships.len(),
538        ready_surfaces: vec![
539            "layerIndex",
540            "containerIndex",
541            "scopeIndex",
542            "selectorContextMembership",
543        ],
544    }
545}
546
547fn collect_layer_statement_facts(source: &str) -> Vec<StyleLayerStatementV0> {
548    let mut statements = Vec::new();
549    let mut search_start = 0usize;
550    while let Some(relative_start) = source
551        .get(search_start..)
552        .and_then(|tail| tail.find("@layer"))
553    {
554        let at_index = search_start + relative_start;
555        let prelude_start = at_index + "@layer".len();
556        let tail = source.get(prelude_start..).unwrap_or_default();
557        let semicolon = tail.find(';');
558        let open_brace = tail.find('{');
559        let Some(semicolon) = semicolon else {
560            break;
561        };
562        if open_brace.is_some_and(|open| open < semicolon) {
563            search_start = prelude_start + open_brace.unwrap_or(0) + 1;
564            continue;
565        }
566
567        let prelude_end = prelude_start + semicolon;
568        let prelude = source.get(prelude_start..prelude_end).unwrap_or_default();
569        let byte_span = ParserByteSpanV0 {
570            start: at_index,
571            end: prelude_end + 1,
572        };
573        for name in split_layer_names(prelude) {
574            statements.push(StyleLayerStatementV0 {
575                name,
576                source_order: statements.len(),
577                byte_span,
578                range: parser_range_for_byte_span(source, byte_span),
579            });
580        }
581        search_start = prelude_end + 1;
582    }
583    statements
584}
585
586fn collect_style_context_blocks_and_memberships(
587    source: &str,
588) -> (
589    Vec<StyleContextBlockV0>,
590    Vec<StyleContextSelectorMembershipV0>,
591) {
592    let bytes = source.as_bytes();
593    let mut blocks = Vec::new();
594    let mut memberships = Vec::new();
595    let mut active_contexts = Vec::<StyleContextBlockV0>::new();
596    let mut block_stack = Vec::<Option<String>>::new();
597    let mut index = 0usize;
598
599    while index < bytes.len() {
600        match bytes[index] {
601            b'{' => {
602                let (header, header_start) =
603                    block_header_and_start_before_open_brace(source, index);
604                if let Some(context) = style_context_block_for_header(
605                    source,
606                    &header,
607                    header_start,
608                    index,
609                    blocks.len(),
610                ) {
611                    block_stack.push(Some(context.id.clone()));
612                    active_contexts.push(context.clone());
613                    blocks.push(context);
614                } else {
615                    for selector_name in selector_class_names(&header) {
616                        for context in &active_contexts {
617                            memberships.push(StyleContextSelectorMembershipV0 {
618                                selector_name: selector_name.clone(),
619                                context_id: context.id.clone(),
620                                context_kind: context.kind,
621                                source_order: memberships.len(),
622                            });
623                        }
624                    }
625                    block_stack.push(None);
626                }
627            }
628            b'}' => {
629                if let Some(Some(context_id)) = block_stack.pop() {
630                    if active_contexts
631                        .last()
632                        .is_some_and(|context| context.id == context_id)
633                    {
634                        active_contexts.pop();
635                    } else {
636                        active_contexts.retain(|context| context.id != context_id);
637                    }
638                }
639            }
640            _ => {}
641        }
642        index += 1;
643    }
644
645    (blocks, memberships)
646}
647
648fn style_context_block_for_header(
649    source: &str,
650    header: &str,
651    header_start: usize,
652    open_brace_index: usize,
653    source_order: usize,
654) -> Option<StyleContextBlockV0> {
655    let header = header.trim();
656    let (kind, raw_prelude) = if let Some(prelude) = header.strip_prefix("@layer") {
657        ("layer", prelude)
658    } else if let Some(prelude) = header.strip_prefix("@container") {
659        ("container", prelude)
660    } else if let Some(prelude) = header.strip_prefix("@scope") {
661        ("scope", prelude)
662    } else {
663        return None;
664    };
665    let prelude = raw_prelude.trim().to_string();
666    let name = match kind {
667        "layer" => split_layer_names(&prelude).into_iter().next(),
668        "container" => container_name_from_prelude(&prelude),
669        "scope" => None,
670        _ => None,
671    };
672    let byte_span = ParserByteSpanV0 {
673        start: header_start,
674        end: open_brace_index + 1,
675    };
676
677    Some(StyleContextBlockV0 {
678        id: format!("{kind}:{source_order}"),
679        kind,
680        name,
681        prelude,
682        source_order,
683        byte_span,
684        range: parser_range_for_byte_span(source, byte_span),
685    })
686}
687
688fn split_layer_names(prelude: &str) -> Vec<String> {
689    prelude
690        .split(',')
691        .filter_map(|name| {
692            let name = name.trim();
693            if name.is_empty() || name == "{" {
694                None
695            } else {
696                Some(name.to_string())
697            }
698        })
699        .collect()
700}
701
702fn container_name_from_prelude(prelude: &str) -> Option<String> {
703    let trimmed = prelude.trim();
704    if trimmed.is_empty() || trimmed.starts_with('(') || trimmed.starts_with("style(") {
705        return None;
706    }
707    let name = trimmed.split_whitespace().next().unwrap_or_default().trim();
708    if css_identifier_text_is_plain(name) {
709        Some(name.to_string())
710    } else {
711        None
712    }
713}
714
715fn css_identifier_text_is_plain(value: &str) -> bool {
716    let mut chars = value.chars();
717    let Some(first) = chars.next() else {
718        return false;
719    };
720    (first.is_ascii_alphabetic() || matches!(first, '_' | '-'))
721        && chars.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
722}
723
724fn block_header_and_start_before_open_brace(
725    source: &str,
726    open_brace_index: usize,
727) -> (String, usize) {
728    let bytes = source.as_bytes();
729    let mut start = 0usize;
730    let mut index = open_brace_index;
731    while let Some(previous) = index.checked_sub(1) {
732        index = previous;
733        if matches!(bytes[index], b'{' | b'}' | b';') {
734            start = index + 1;
735            break;
736        }
737        if index == 0 {
738            break;
739        }
740    }
741    let raw = source.get(start..open_brace_index).unwrap_or_default();
742    let trimmed_start_delta = raw.len().saturating_sub(raw.trim_start().len());
743    (raw.trim().to_string(), start + trimmed_start_delta)
744}
745
746fn selector_class_names(selector: &str) -> Vec<String> {
747    let bytes = selector.as_bytes();
748    let mut names = BTreeSet::new();
749    let mut index = 0usize;
750    while index < bytes.len() {
751        if bytes[index] == b'.' {
752            let start = index + 1;
753            let mut end = start;
754            while end < bytes.len()
755                && (bytes[end].is_ascii_alphanumeric() || matches!(bytes[end], b'_' | b'-'))
756            {
757                end += 1;
758            }
759            if end > start
760                && let Some(name) = selector.get(start..end)
761            {
762                names.insert(name.to_string());
763            }
764            index = end;
765            continue;
766        }
767        index += 1;
768    }
769    names.into_iter().collect()
770}
771
772fn summarize_omena_parser_selector_facts(
773    source: &str,
774    facts: &ParsedStyleFacts,
775) -> ParserIndexSelectorFactsV0 {
776    let mut names = Vec::new();
777    let mut definition_facts = Vec::new();
778    let mut bem_suffix_parent_names = BTreeSet::new();
779    let mut bem_suffix_safe_names = BTreeSet::new();
780    let mut nested_unsafe_names = BTreeSet::new();
781    let mut source_order = 0usize;
782
783    for selector in &facts.selectors {
784        if selector.kind != ParsedSelectorFactKind::Class {
785            continue;
786        }
787        let byte_span = parser_byte_span_for_offsets(
788            u32::from(selector.range.start()) as usize,
789            u32::from(selector.range.end()) as usize,
790        );
791        let parent_name = bem_suffix_parent_name(selector.name.as_str());
792        let nested_safety_kind = if let Some(parent) = parent_name.clone() {
793            bem_suffix_parent_names.insert(parent);
794            bem_suffix_safe_names.insert(selector.name.clone());
795            "bemSuffixSafe"
796        } else if selector_has_parent_ampersand_class_prefix(source, byte_span.start) {
797            nested_unsafe_names.insert(selector.name.clone());
798            "nestedUnsafe"
799        } else {
800            "flat"
801        };
802        names.push(selector.name.clone());
803        definition_facts.push(ParserIndexSelectorDefinitionFactV0 {
804            name: selector.name.clone(),
805            source_order,
806            byte_span,
807            range: parser_range_for_byte_span(source, byte_span),
808            nested_safety_kind,
809            bem_suffix_parent_name: parent_name,
810            under_media: false,
811            under_supports: false,
812            under_layer: false,
813        });
814        source_order += 1;
815    }
816
817    names.sort();
818    names.dedup();
819    definition_facts.sort();
820    let bem_suffix_safe_names = bem_suffix_safe_names.into_iter().collect::<Vec<_>>();
821    let nested_unsafe_names = nested_unsafe_names.into_iter().collect::<Vec<_>>();
822    ParserIndexSelectorFactsV0 {
823        names,
824        definition_facts,
825        bem_suffix_parent_names: bem_suffix_parent_names.into_iter().collect(),
826        bem_suffix_safe_names: bem_suffix_safe_names.clone(),
827        nested_unsafe_names: nested_unsafe_names.clone(),
828        selectors_with_value_refs_names: Vec::new(),
829        selectors_with_animation_ref_names: Vec::new(),
830        selectors_with_animation_name_ref_names: Vec::new(),
831        bem_suffix_count: bem_suffix_safe_names.len(),
832        nested_safety_counts: NestedSafetyCountsV0 {
833            flat: source_order
834                .saturating_sub(bem_suffix_safe_names.len())
835                .saturating_sub(nested_unsafe_names.len()),
836            bem_suffix_safe: bem_suffix_safe_names.len(),
837            nested_unsafe: nested_unsafe_names.len(),
838        },
839    }
840}
841
842fn summarize_omena_parser_value_facts(facts: &ParsedStyleFacts) -> ParserIndexValueFactsV0 {
843    let mut decl_names = BTreeSet::new();
844    let mut ref_names = BTreeSet::new();
845    let mut import_sources = BTreeSet::new();
846    for value in &facts.css_module_values {
847        match value.kind {
848            ParsedCssModuleValueFactKind::Definition => {
849                decl_names.insert(value.name.clone());
850            }
851            ParsedCssModuleValueFactKind::Reference => {
852                ref_names.insert(value.name.clone());
853            }
854            ParsedCssModuleValueFactKind::ImportSource => {
855                import_sources.insert(value.name.clone());
856            }
857        }
858    }
859    ParserIndexValueFactsV0 {
860        decl_names: decl_names.into_iter().collect(),
861        import_sources: import_sources.into_iter().collect(),
862        import_alias_count: facts.css_module_value_import_edge_count,
863        ref_names: ref_names.clone().into_iter().collect(),
864        local_ref_names: ref_names.into_iter().collect(),
865        ..ParserIndexValueFactsV0::default()
866    }
867}
868
869fn summarize_omena_parser_custom_property_facts(
870    source: &str,
871    facts: &ParsedStyleFacts,
872) -> ParserIndexCustomPropertyFactsV0 {
873    let mut decl_names = BTreeSet::new();
874    let mut ref_names = BTreeSet::new();
875    let mut decl_facts = Vec::new();
876    let mut ref_facts = Vec::new();
877    for variable in &facts.variables {
878        match variable.kind {
879            ParsedVariableFactKind::CustomPropertyDeclaration => {
880                let byte_span = parser_byte_span_for_offsets(
881                    u32::from(variable.range.start()) as usize,
882                    u32::from(variable.range.end()) as usize,
883                );
884                decl_names.insert(variable.name.clone());
885                let (
886                    selector_contexts,
887                    under_media,
888                    under_supports,
889                    under_layer,
890                    layer_names,
891                    condition_context,
892                ) = style_context_with_layers_for_byte_offset(source, byte_span.start);
893                decl_facts.push(ParserIndexCustomPropertyDeclFactV0 {
894                    name: variable.name.clone(),
895                    value: declaration_value_text(source, byte_span.start),
896                    source_order: decl_facts.len(),
897                    byte_span,
898                    range: parser_range_for_byte_span(source, byte_span),
899                    selector_contexts,
900                    condition_context,
901                    layer_names,
902                    under_media,
903                    under_supports,
904                    under_layer,
905                });
906            }
907            ParsedVariableFactKind::CustomPropertyReference => {
908                let byte_offset = u32::from(variable.range.start()) as usize;
909                let (
910                    selector_contexts,
911                    under_media,
912                    under_supports,
913                    under_layer,
914                    layer_names,
915                    condition_context,
916                ) = style_context_with_layers_for_byte_offset(source, byte_offset);
917                ref_names.insert(variable.name.clone());
918                ref_facts.push(ParserIndexCustomPropertyRefFactV0 {
919                    name: variable.name.clone(),
920                    source_order: ref_facts.len(),
921                    selector_contexts,
922                    condition_context,
923                    layer_names,
924                    under_media,
925                    under_supports,
926                    under_layer,
927                });
928            }
929            _ => {}
930        }
931    }
932    let selectors_with_refs_names = ref_facts
933        .iter()
934        .flat_map(|reference| reference.selector_contexts.iter().cloned())
935        .collect::<BTreeSet<_>>();
936    let selectors_with_refs_under_media_names = ref_facts
937        .iter()
938        .filter(|reference| reference.under_media)
939        .flat_map(|reference| reference.selector_contexts.iter().cloned())
940        .collect::<BTreeSet<_>>();
941    let selectors_with_refs_under_supports_names = ref_facts
942        .iter()
943        .filter(|reference| reference.under_supports)
944        .flat_map(|reference| reference.selector_contexts.iter().cloned())
945        .collect::<BTreeSet<_>>();
946    let selectors_with_refs_under_layer_names = ref_facts
947        .iter()
948        .filter(|reference| reference.under_layer)
949        .flat_map(|reference| reference.selector_contexts.iter().cloned())
950        .collect::<BTreeSet<_>>();
951    let decl_context_selectors = decl_facts
952        .iter()
953        .flat_map(|declaration| declaration.selector_contexts.iter().cloned())
954        .collect::<BTreeSet<_>>();
955    let decl_names_under_media = decl_facts
956        .iter()
957        .filter(|declaration| declaration.under_media)
958        .map(|declaration| declaration.name.clone())
959        .collect::<BTreeSet<_>>();
960    let decl_names_under_supports = decl_facts
961        .iter()
962        .filter(|declaration| declaration.under_supports)
963        .map(|declaration| declaration.name.clone())
964        .collect::<BTreeSet<_>>();
965    let decl_names_under_layer = decl_facts
966        .iter()
967        .filter(|declaration| declaration.under_layer)
968        .map(|declaration| declaration.name.clone())
969        .collect::<BTreeSet<_>>();
970
971    ParserIndexCustomPropertyFactsV0 {
972        decl_names: decl_names.into_iter().collect(),
973        decl_facts,
974        decl_context_selectors: decl_context_selectors.into_iter().collect(),
975        decl_names_under_media: decl_names_under_media.into_iter().collect(),
976        decl_names_under_supports: decl_names_under_supports.into_iter().collect(),
977        decl_names_under_layer: decl_names_under_layer.into_iter().collect(),
978        ref_names: ref_names.into_iter().collect(),
979        ref_facts,
980        selectors_with_refs_names: selectors_with_refs_names.into_iter().collect(),
981        selectors_with_refs_under_media_names: selectors_with_refs_under_media_names
982            .into_iter()
983            .collect(),
984        selectors_with_refs_under_supports_names: selectors_with_refs_under_supports_names
985            .into_iter()
986            .collect(),
987        selectors_with_refs_under_layer_names: selectors_with_refs_under_layer_names
988            .into_iter()
989            .collect(),
990    }
991}
992
993fn summarize_omena_parser_sass_syntax_facts(facts: &ParsedStyleFacts) -> ParserSassSyntaxFactsV0 {
994    let mut variable_decl_names = BTreeSet::new();
995    let mut variable_ref_names = BTreeSet::new();
996    let mut mixin_decl_names = BTreeSet::new();
997    let mut mixin_include_names = BTreeSet::new();
998    let mut function_decl_names = BTreeSet::new();
999    let mut function_call_names = BTreeSet::new();
1000    for symbol in &facts.sass_symbols {
1001        match symbol.kind {
1002            ParsedSassSymbolFactKind::VariableDeclaration => {
1003                variable_decl_names.insert(symbol.name.clone());
1004            }
1005            ParsedSassSymbolFactKind::VariableReference => {
1006                variable_ref_names.insert(symbol.name.clone());
1007            }
1008            ParsedSassSymbolFactKind::MixinDeclaration => {
1009                mixin_decl_names.insert(symbol.name.clone());
1010            }
1011            ParsedSassSymbolFactKind::MixinInclude => {
1012                mixin_include_names.insert(symbol.name.clone());
1013            }
1014            ParsedSassSymbolFactKind::FunctionDeclaration => {
1015                function_decl_names.insert(symbol.name.clone());
1016            }
1017            ParsedSassSymbolFactKind::FunctionCall => {
1018                function_call_names.insert(symbol.name.clone());
1019            }
1020        }
1021    }
1022    let mut module_use_sources = BTreeSet::new();
1023    let mut module_use_edges = Vec::new();
1024    let mut module_forward_sources = BTreeSet::new();
1025    let mut module_import_sources = BTreeSet::new();
1026    for edge in &facts.sass_module_edges {
1027        match edge.kind {
1028            ParsedSassModuleEdgeFactKind::Use => {
1029                module_use_sources.insert(edge.source.clone());
1030                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1031                    source: edge.source.clone(),
1032                    namespace_kind: edge.namespace_kind.unwrap_or("default"),
1033                    namespace: edge.namespace.clone(),
1034                });
1035            }
1036            ParsedSassModuleEdgeFactKind::Forward => {
1037                module_forward_sources.insert(edge.source.clone());
1038            }
1039            ParsedSassModuleEdgeFactKind::Import => {
1040                module_import_sources.insert(edge.source.clone());
1041                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1042                    source: edge.source.clone(),
1043                    namespace_kind: "wildcard",
1044                    namespace: None,
1045                });
1046            }
1047        }
1048    }
1049    ParserSassSyntaxFactsV0 {
1050        variable_decl_names: variable_decl_names.into_iter().collect(),
1051        variable_parameter_names: Vec::new(),
1052        variable_ref_names: variable_ref_names.into_iter().collect(),
1053        mixin_decl_names: mixin_decl_names.into_iter().collect(),
1054        mixin_include_names: mixin_include_names.into_iter().collect(),
1055        function_decl_names: function_decl_names.into_iter().collect(),
1056        function_call_names: function_call_names.into_iter().collect(),
1057        module_use_sources: module_use_sources.into_iter().collect(),
1058        module_use_edges,
1059        module_forward_sources: module_forward_sources.into_iter().collect(),
1060        module_import_sources: module_import_sources.into_iter().collect(),
1061    }
1062}
1063
1064fn summarize_omena_parser_keyframe_facts(facts: &ParsedStyleFacts) -> ParserIndexKeyframesFactsV0 {
1065    let mut names = BTreeSet::new();
1066    let mut animation_ref_names = BTreeSet::new();
1067    for animation in &facts.animations {
1068        match animation.kind {
1069            ParsedAnimationFactKind::KeyframesDeclaration => {
1070                names.insert(animation.name.clone());
1071            }
1072            ParsedAnimationFactKind::AnimationNameReference => {
1073                animation_ref_names.insert(animation.name.clone());
1074            }
1075        }
1076    }
1077    ParserIndexKeyframesFactsV0 {
1078        names: names.into_iter().collect(),
1079        animation_ref_names: animation_ref_names.clone().into_iter().collect(),
1080        animation_name_ref_names: animation_ref_names.into_iter().collect(),
1081        ..ParserIndexKeyframesFactsV0::default()
1082    }
1083}
1084
1085fn summarize_omena_parser_composes_facts(facts: &ParsedStyleFacts) -> ParserIndexComposesFactsV0 {
1086    let mut local_selector_names = BTreeSet::new();
1087    let mut imported_selector_names = BTreeSet::new();
1088    let mut global_selector_names = BTreeSet::new();
1089    let mut import_sources = BTreeSet::new();
1090    for edge in &facts.css_module_composes_edges {
1091        match edge.kind {
1092            ParsedCssModuleComposesEdgeKind::Local => {
1093                local_selector_names.extend(edge.target_names.iter().cloned());
1094            }
1095            ParsedCssModuleComposesEdgeKind::External => {
1096                imported_selector_names.extend(edge.target_names.iter().cloned());
1097                if let Some(source) = &edge.import_source {
1098                    import_sources.insert(source.clone());
1099                }
1100            }
1101            ParsedCssModuleComposesEdgeKind::Global => {
1102                global_selector_names.extend(edge.target_names.iter().cloned());
1103            }
1104        }
1105    }
1106    for composes in &facts.css_module_composes {
1107        if composes.kind == ParsedCssModuleComposesFactKind::ImportSource {
1108            import_sources.insert(composes.name.clone());
1109        }
1110    }
1111    let local_selector_names = local_selector_names.into_iter().collect::<Vec<_>>();
1112    let imported_selector_names = imported_selector_names.into_iter().collect::<Vec<_>>();
1113    let global_selector_names = global_selector_names.into_iter().collect::<Vec<_>>();
1114    ParserIndexComposesFactsV0 {
1115        class_name_count: local_selector_names.len()
1116            + imported_selector_names.len()
1117            + global_selector_names.len(),
1118        local_class_name_count: local_selector_names.len(),
1119        imported_class_name_count: imported_selector_names.len(),
1120        global_class_name_count: global_selector_names.len(),
1121        local_selector_names,
1122        imported_selector_names,
1123        global_selector_names,
1124        import_sources: import_sources.into_iter().collect(),
1125        ..ParserIndexComposesFactsV0::default()
1126    }
1127}
1128
1129fn summarize_omena_parser_custom_property_semantic_facts(
1130    facts: &ParserIndexCustomPropertyFactsV0,
1131) -> StyleCustomPropertySemanticFactsV0 {
1132    let mut resolved_ref_names = BTreeSet::new();
1133    let mut unresolved_ref_names = BTreeSet::new();
1134    for reference in &facts.ref_facts {
1135        if facts
1136            .decl_facts
1137            .iter()
1138            .any(|declaration| custom_property_context_matches(declaration, reference))
1139        {
1140            resolved_ref_names.insert(reference.name.clone());
1141        } else {
1142            unresolved_ref_names.insert(reference.name.clone());
1143        }
1144    }
1145    StyleCustomPropertySemanticFactsV0 {
1146        decl_names: facts.decl_names.clone(),
1147        ref_names: facts.ref_names.clone(),
1148        resolved_ref_names: resolved_ref_names.into_iter().collect(),
1149        unresolved_ref_names: unresolved_ref_names.into_iter().collect(),
1150        selectors_with_refs_names: facts.selectors_with_refs_names.clone(),
1151    }
1152}
1153
1154struct SassSelectorResolution {
1155    resolved_variable_ref_selectors: Vec<String>,
1156    unresolved_variable_ref_selectors: Vec<String>,
1157    resolved_mixin_include_selectors: Vec<String>,
1158    unresolved_mixin_include_selectors: Vec<String>,
1159}
1160
1161fn summarize_omena_parser_sass_selector_resolution(
1162    source: &str,
1163    facts: &ParsedStyleFacts,
1164    resolution: &ParserIndexSassSameFileResolutionFactsV0,
1165) -> SassSelectorResolution {
1166    let resolved_variables = resolution
1167        .resolved_variable_ref_names
1168        .iter()
1169        .cloned()
1170        .collect::<BTreeSet<_>>();
1171    let resolved_mixins = resolution
1172        .resolved_mixin_include_names
1173        .iter()
1174        .cloned()
1175        .collect::<BTreeSet<_>>();
1176    let mut resolved_variable_ref_selectors = BTreeSet::new();
1177    let mut unresolved_variable_ref_selectors = BTreeSet::new();
1178    let mut resolved_mixin_include_selectors = BTreeSet::new();
1179    let mut unresolved_mixin_include_selectors = BTreeSet::new();
1180
1181    for symbol in &facts.sass_symbols {
1182        match symbol.kind {
1183            ParsedSassSymbolFactKind::VariableReference => {
1184                let selector = semantic_selector_name_for_byte_offset(
1185                    source,
1186                    u32::from(symbol.range.start()) as usize,
1187                );
1188                let Some(selector) = selector else {
1189                    continue;
1190                };
1191                if resolved_variables.contains(&symbol.name) {
1192                    resolved_variable_ref_selectors.insert(selector);
1193                } else {
1194                    unresolved_variable_ref_selectors.insert(selector);
1195                }
1196            }
1197            ParsedSassSymbolFactKind::MixinInclude => {
1198                let selector = semantic_selector_name_for_byte_offset(
1199                    source,
1200                    u32::from(symbol.range.start()) as usize,
1201                );
1202                let Some(selector) = selector else {
1203                    continue;
1204                };
1205                if resolved_mixins.contains(&symbol.name) {
1206                    resolved_mixin_include_selectors.insert(selector);
1207                } else {
1208                    unresolved_mixin_include_selectors.insert(selector);
1209                }
1210            }
1211            _ => {}
1212        }
1213    }
1214
1215    SassSelectorResolution {
1216        resolved_variable_ref_selectors: resolved_variable_ref_selectors.into_iter().collect(),
1217        unresolved_variable_ref_selectors: unresolved_variable_ref_selectors.into_iter().collect(),
1218        resolved_mixin_include_selectors: resolved_mixin_include_selectors.into_iter().collect(),
1219        unresolved_mixin_include_selectors: unresolved_mixin_include_selectors
1220            .into_iter()
1221            .collect(),
1222    }
1223}
1224
1225fn custom_property_context_matches(
1226    declaration: &ParserIndexCustomPropertyDeclFactV0,
1227    reference: &ParserIndexCustomPropertyRefFactV0,
1228) -> bool {
1229    if declaration.name != reference.name {
1230        return false;
1231    }
1232    if declaration.under_media && !reference.under_media {
1233        return false;
1234    }
1235    if declaration.under_supports && !reference.under_supports {
1236        return false;
1237    }
1238    if declaration.under_layer && !reference.under_layer {
1239        return false;
1240    }
1241    if declaration.selector_contexts.is_empty() {
1242        return true;
1243    }
1244    declaration.selector_contexts.iter().any(|selector| {
1245        selector_context_witness_for_declaration(selector, &reference.selector_contexts).matched
1246    })
1247}
1248
1249fn summarize_omena_parser_sass_same_file_resolution(
1250    facts: &ParserSassSyntaxFactsV0,
1251) -> ParserIndexSassSameFileResolutionFactsV0 {
1252    let variable_targets = facts
1253        .variable_decl_names
1254        .iter()
1255        .chain(facts.variable_parameter_names.iter())
1256        .cloned()
1257        .collect::<BTreeSet<_>>();
1258    let mixin_targets = facts
1259        .mixin_decl_names
1260        .iter()
1261        .cloned()
1262        .collect::<BTreeSet<_>>();
1263    let function_targets = facts
1264        .function_decl_names
1265        .iter()
1266        .cloned()
1267        .collect::<BTreeSet<_>>();
1268
1269    ParserIndexSassSameFileResolutionFactsV0 {
1270        resolved_variable_ref_names: names_matching(&facts.variable_ref_names, &variable_targets),
1271        unresolved_variable_ref_names: names_not_matching(
1272            &facts.variable_ref_names,
1273            &variable_targets,
1274        ),
1275        resolved_mixin_include_names: names_matching(&facts.mixin_include_names, &mixin_targets),
1276        unresolved_mixin_include_names: names_not_matching(
1277            &facts.mixin_include_names,
1278            &mixin_targets,
1279        ),
1280        resolved_function_call_names: names_matching(&facts.function_call_names, &function_targets),
1281    }
1282}
1283
1284fn names_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1285    names
1286        .iter()
1287        .filter(|name| targets.contains(*name))
1288        .cloned()
1289        .collect()
1290}
1291
1292fn names_not_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1293    names
1294        .iter()
1295        .filter(|name| !targets.contains(*name))
1296        .cloned()
1297        .collect()
1298}
1299
1300fn bem_suffix_parent_name(name: &str) -> Option<String> {
1301    let marker = name.find("__").or_else(|| name.find("--"))?;
1302    (marker > 0).then(|| name[..marker].to_string())
1303}
1304
1305fn selector_has_parent_ampersand_class_prefix(source: &str, selector_start: usize) -> bool {
1306    let bytes = source.as_bytes();
1307    if selector_start >= bytes.len() {
1308        return false;
1309    }
1310    let dot_index = if bytes[selector_start] == b'.' {
1311        selector_start
1312    } else {
1313        match previous_non_whitespace_byte_index(bytes, selector_start) {
1314            Some(index) if bytes[index] == b'.' => index,
1315            _ => return false,
1316        }
1317    };
1318    matches!(
1319        previous_non_whitespace_byte_index(bytes, dot_index),
1320        Some(index) if bytes[index] == b'&'
1321    )
1322}
1323
1324fn style_context_for_byte_offset(
1325    source: &str,
1326    byte_offset: usize,
1327) -> (Vec<String>, bool, bool, bool) {
1328    let (selector_contexts, under_media, under_supports, under_layer, _, _) =
1329        style_context_with_layers_for_byte_offset(source, byte_offset);
1330    (selector_contexts, under_media, under_supports, under_layer)
1331}
1332
1333fn style_context_with_layers_for_byte_offset(
1334    source: &str,
1335    byte_offset: usize,
1336) -> (Vec<String>, bool, bool, bool, Vec<String>, Vec<String>) {
1337    let contexts = block_contexts_for_byte_offset(source, byte_offset);
1338    let selector_contexts = contexts
1339        .iter()
1340        .filter_map(|context| match context {
1341            StyleBlockContext::Selector(selector) => Some(selector.clone()),
1342            StyleBlockContext::Media(_)
1343            | StyleBlockContext::Supports(_)
1344            | StyleBlockContext::Layer(_)
1345            | StyleBlockContext::OtherAtRule(_) => None,
1346        })
1347        .collect::<Vec<_>>();
1348    let under_media = contexts
1349        .iter()
1350        .any(|context| matches!(context, StyleBlockContext::Media(_)));
1351    let under_supports = contexts
1352        .iter()
1353        .any(|context| matches!(context, StyleBlockContext::Supports(_)));
1354    let under_layer = contexts
1355        .iter()
1356        .any(|context| matches!(context, StyleBlockContext::Layer(_)));
1357    let layer_names = contexts
1358        .iter()
1359        .filter_map(|context| match context {
1360            StyleBlockContext::Layer(Some(name)) => Some(name.clone()),
1361            _ => None,
1362        })
1363        .collect::<Vec<_>>();
1364    let condition_context = contexts
1365        .iter()
1366        .filter_map(|context| match context {
1367            StyleBlockContext::Media(header)
1368            | StyleBlockContext::Supports(header)
1369            | StyleBlockContext::OtherAtRule(header) => Some(header.clone()),
1370            StyleBlockContext::Selector(_) | StyleBlockContext::Layer(_) => None,
1371        })
1372        .collect::<Vec<_>>();
1373
1374    (
1375        selector_contexts,
1376        under_media,
1377        under_supports,
1378        under_layer,
1379        layer_names,
1380        condition_context,
1381    )
1382}
1383
1384fn declaration_value_text(source: &str, offset: usize) -> String {
1385    let span = declaration_statement_byte_span_for_offset(source, offset);
1386    let Some(statement) = source.get(span.start..span.end) else {
1387        return String::new();
1388    };
1389    let Some(colon) = statement.find(':') else {
1390        return String::new();
1391    };
1392    statement[colon + 1..]
1393        .trim()
1394        .trim_end_matches(';')
1395        .trim()
1396        .to_string()
1397}
1398
1399fn declaration_statement_byte_span_for_offset(source: &str, offset: usize) -> ParserByteSpanV0 {
1400    let start = source
1401        .get(..offset)
1402        .and_then(|before| before.rfind(['{', ';']).map(|index| index + 1))
1403        .unwrap_or(offset);
1404    let end = source
1405        .get(offset..)
1406        .and_then(|rest| {
1407            let semicolon = rest.find(';');
1408            let close = rest.find('}');
1409            match (semicolon, close) {
1410                (Some(semicolon), Some(close)) => Some(offset + semicolon.min(close)),
1411                (Some(semicolon), None) => Some(offset + semicolon + 1),
1412                (None, Some(close)) => Some(offset + close),
1413                (None, None) => None,
1414            }
1415        })
1416        .unwrap_or(source.len());
1417    ParserByteSpanV0 { start, end }
1418}
1419
1420fn semantic_selector_name_for_byte_offset(source: &str, byte_offset: usize) -> Option<String> {
1421    let (selector_contexts, _, _, _) = style_context_for_byte_offset(source, byte_offset);
1422    selector_contexts
1423        .last()
1424        .and_then(|selector| selector_class_name(selector))
1425}
1426
1427#[derive(Debug, Clone, PartialEq, Eq)]
1428enum StyleBlockContext {
1429    Selector(String),
1430    Media(String),
1431    Supports(String),
1432    Layer(Option<String>),
1433    OtherAtRule(String),
1434}
1435
1436fn block_contexts_for_byte_offset(source: &str, byte_offset: usize) -> Vec<StyleBlockContext> {
1437    let bytes = source.as_bytes();
1438    let mut contexts = Vec::new();
1439    let limit = byte_offset.min(bytes.len());
1440    let mut index = 0usize;
1441    while index < limit {
1442        match bytes[index] {
1443            b'{' => {
1444                let header = block_header_before_open_brace(source, index);
1445                contexts.push(style_block_context_for_header(&header));
1446            }
1447            b'}' => {
1448                contexts.pop();
1449            }
1450            _ => {}
1451        }
1452        index += 1;
1453    }
1454    contexts
1455}
1456
1457fn block_header_before_open_brace(source: &str, open_brace_index: usize) -> String {
1458    let bytes = source.as_bytes();
1459    let mut start = 0usize;
1460    let mut index = open_brace_index;
1461    while let Some(previous) = index.checked_sub(1) {
1462        index = previous;
1463        if matches!(bytes[index], b'{' | b'}' | b';') {
1464            start = index + 1;
1465            break;
1466        }
1467        if index == 0 {
1468            break;
1469        }
1470    }
1471    source
1472        .get(start..open_brace_index)
1473        .unwrap_or_default()
1474        .trim()
1475        .to_string()
1476}
1477
1478fn style_block_context_for_header(header: &str) -> StyleBlockContext {
1479    let header = header.trim();
1480    if header.starts_with("@media") {
1481        StyleBlockContext::Media(normalized_condition_header(header))
1482    } else if header.starts_with("@supports") {
1483        StyleBlockContext::Supports(normalized_condition_header(header))
1484    } else if header.starts_with("@layer") {
1485        StyleBlockContext::Layer(
1486            header
1487                .strip_prefix("@layer")
1488                .and_then(|prelude| split_layer_names(prelude).into_iter().next()),
1489        )
1490    } else if header.starts_with('@') {
1491        StyleBlockContext::OtherAtRule(normalized_condition_header(header))
1492    } else {
1493        StyleBlockContext::Selector(header.to_string())
1494    }
1495}
1496
1497fn normalized_condition_header(header: &str) -> String {
1498    header.split_whitespace().collect::<Vec<_>>().join(" ")
1499}
1500
1501fn selector_class_name(selector: &str) -> Option<String> {
1502    let bytes = selector.as_bytes();
1503    let mut index = 0usize;
1504    let mut last = None;
1505    while index < bytes.len() {
1506        if bytes[index] == b'.' {
1507            let start = index + 1;
1508            let mut end = start;
1509            while end < bytes.len()
1510                && (bytes[end].is_ascii_alphanumeric() || matches!(bytes[end], b'_' | b'-'))
1511            {
1512                end += 1;
1513            }
1514            if end > start {
1515                last = selector.get(start..end).map(ToString::to_string);
1516            }
1517            index = end;
1518        } else {
1519            index += 1;
1520        }
1521    }
1522    last
1523}
1524
1525fn previous_non_whitespace_byte_index(bytes: &[u8], before: usize) -> Option<usize> {
1526    let mut index = before.checked_sub(1)?;
1527    loop {
1528        if !bytes[index].is_ascii_whitespace() {
1529            return Some(index);
1530        }
1531        index = index.checked_sub(1)?;
1532    }
1533}
1534
1535fn parser_byte_span_for_offsets(start: usize, end: usize) -> ParserByteSpanV0 {
1536    ParserByteSpanV0 { start, end }
1537}
1538
1539fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
1540    ParserRangeV0 {
1541        start: parser_position_for_byte_offset(source, span.start),
1542        end: parser_position_for_byte_offset(source, span.end),
1543    }
1544}
1545
1546fn parser_position_for_byte_offset(source: &str, byte_offset: usize) -> ParserPositionV0 {
1547    // LSP positions count UTF-16 code units per line, not raw bytes. Walk chars
1548    // and accumulate `len_utf16()` so non-ASCII source produces correct columns,
1549    // matching the canonical helpers in omena-query/src/style.rs and
1550    // omena-lsp-server/src/protocol.rs (previously this used a raw byte offset,
1551    // which diverged on multi-byte characters).
1552    let clamped_offset = byte_offset.min(source.len());
1553    let mut line = 0usize;
1554    let mut character = 0usize;
1555
1556    for (index, ch) in source.char_indices() {
1557        if index >= clamped_offset {
1558            break;
1559        }
1560        if ch == '\n' {
1561            line += 1;
1562            character = 0;
1563        } else {
1564            character += ch.len_utf16();
1565        }
1566    }
1567
1568    ParserPositionV0 { line, character }
1569}
1570
1571fn dialect_for_style_path(style_path: &str) -> Option<StyleDialect> {
1572    if style_path.ends_with(".sass") {
1573        Some(StyleDialect::Sass)
1574    } else if style_path.ends_with(".scss") {
1575        Some(StyleDialect::Scss)
1576    } else if style_path.ends_with(".less") {
1577        Some(StyleDialect::Less)
1578    } else if style_path.ends_with(".css") {
1579        Some(StyleDialect::Css)
1580    } else {
1581        None
1582    }
1583}
1584
1585fn omena_parser_dialect_for_style_path(style_path: &str) -> StyleDialect {
1586    dialect_for_style_path(style_path).unwrap_or(StyleDialect::Css)
1587}
1588
1589fn omena_parser_dialect_label(dialect: StyleDialect) -> &'static str {
1590    match dialect {
1591        StyleDialect::Css => "css",
1592        StyleDialect::Scss => "scss",
1593        StyleDialect::Sass => "sass",
1594        StyleDialect::Less => "less",
1595    }
1596}
1597
1598#[cfg(test)]
1599mod tests;