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::{SelectorMatchVerdict, 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    ParseResult, ParsedAnimationFactKind, ParsedCssModuleComposesEdgeKind,
16    ParsedCssModuleComposesFactKind, ParsedCssModuleValueFactKind, ParsedCst,
17    ParsedSassModuleEdgeFactKind, ParsedSassSymbolFactKind, ParsedSelectorFactKind,
18    ParsedStyleFacts, ParsedVariableFactKind, ParserDeclarationSyntaxFactV0, ProductSyntaxIndexV0,
19    StyleDialect, facts_from_cst, parse,
20};
21use omena_syntax::{SyntaxKind, SyntaxNode, css_keyword};
22use serde::Serialize;
23use std::collections::BTreeSet;
24
25mod css_modules;
26mod css_modules_cross_file;
27mod design_tokens;
28mod evidence;
29mod layer_tree;
30mod lossless_cst;
31mod observation;
32mod sass_module_graph;
33mod selector_identity;
34mod selector_references;
35mod source_evidence;
36mod types;
37
38pub use css_modules::{
39    CssModulesSemanticCapabilitiesV0, CssModulesSemanticSummaryV0, summarize_css_modules_semantics,
40    summarize_css_modules_semantics_from_source,
41};
42pub use css_modules_cross_file::{
43    CssModulesComposesClosureEdgeV0, CssModulesComposesEdgeFactV0,
44    CssModulesCrossFileClosureCapabilitiesV0, CssModulesCrossFileClosureSummaryV0,
45    CssModulesCrossFileResolutionCapabilitiesV0, CssModulesCrossFileResolutionSummaryV0,
46    CssModulesCrossFileStyleFactsV0, CssModulesCycleV0, CssModulesIcssClosureEdgeV0,
47    CssModulesIcssExportEdgeFactV0, CssModulesIcssImportEdgeFactV0,
48    CssModulesImportEdgeResolutionV0, CssModulesValueClosureEdgeV0,
49    CssModulesValueDefinitionEdgeFactV0, CssModulesValueImportEdgeFactV0,
50    summarize_css_modules_cross_file_closure, summarize_css_modules_cross_file_resolution,
51};
52pub use design_tokens::{
53    DesignTokenCascadeRankingSignalV0, DesignTokenContextSignalV0,
54    DesignTokenExternalDeclarationCandidateScopeV0, DesignTokenRankedReferenceV0,
55    DesignTokenResolutionSignalV0, DesignTokenSemanticCapabilitiesV0, DesignTokenSemanticSummaryV0,
56    DesignTokenWorkspaceDeclarationFactV0, collect_design_token_workspace_declarations,
57    summarize_design_token_semantics,
58    summarize_design_token_semantics_with_scoped_workspace_declarations,
59    summarize_design_token_semantics_with_workspace_declarations,
60};
61pub use evidence::{
62    SemanticPromotionEvidenceItemV0, SemanticPromotionEvidenceSummaryV0,
63    summarize_semantic_promotion_evidence, summarize_semantic_promotion_evidence_with_source_input,
64};
65pub use layer_tree::{LayerBindingResolutionV0, layer_ordinal_for_byte_span};
66pub use lossless_cst::{
67    LosslessCstConsumerReadinessV0, LosslessCstContractV0, LosslessCstSpanInvariantsV0,
68    summarize_lossless_cst_contract,
69};
70pub use observation::{
71    SelectorIdentityObservationV0, SemanticCouplingBoundaryObservationV0,
72    SemanticGraphDownstreamReadinessV0, SourceEvidenceObservationV0, TheoryObservationContractV0,
73    TheoryObservationHarnessInput, TheoryObservationHarnessSummaryV0,
74    summarize_theory_observation_contract, summarize_theory_observation_harness,
75};
76pub use sass_module_graph::{
77    SassModuleConfigurableNamesResolverV0, SassModuleCycleV0,
78    SassModuleForwardConfigurationRequestV0, SassModuleGraphClosureCapabilitiesV0,
79    SassModuleGraphClosureEdgeV0, SassModuleGraphClosureSummaryV0,
80    SassModuleGraphConfigurationResolverV0, SassModuleGraphEdgeFactV0,
81    SassModuleGraphResolutionCapabilitiesV0, SassModuleGraphResolutionSummaryV0,
82    SassModuleUseConfigurationRequestV0, SassModuleVariableOverrideV0,
83    SassModuleVisibleSymbolsResolverV0, SassSymbolKeyV0, StyleImportReachabilityCapabilitiesV0,
84    StyleImportReachabilityEdgeFactV0, StyleImportReachabilityFactV0,
85    StyleImportReachabilitySummaryV0, apply_sass_forward_prefix, collect_visible_sass_symbol_keys,
86    derive_sass_forward_effective_variable_overrides, derive_sass_forward_export_prefix_at_ordinal,
87    derive_sass_module_configurable_variable_names,
88    derive_sass_module_forward_effective_variable_overrides_at_ordinal,
89    derive_sass_module_forward_variable_override_values_at_ordinal,
90    derive_sass_module_forward_variable_overrides_at_ordinal,
91    derive_sass_module_rule_variable_overrides_at_ordinal,
92    filter_sass_forward_configurable_variable_names, filter_sass_forward_exports,
93    fold_sass_symbol_name, prefix_sass_forward_exports,
94    resolve_sass_module_effective_variable_overrides, sass_forward_filter_name_matches_symbol,
95    sass_module_configuration_variables_are_valid, sass_symbol_key,
96    summarize_sass_module_configuration_signature, summarize_sass_module_graph_closure,
97    summarize_sass_module_graph_resolution, summarize_sass_module_instance_identity_key,
98    summarize_style_import_reachability, with_sass_module_rawallpaths_closure_for_test,
99};
100pub use selector_identity::{
101    SelectorCanonicalIdentityV0, SelectorIdentityEngineSummaryV0, SelectorIdentityRewriteSafetyV0,
102    summarize_selector_identity_engine,
103};
104pub use selector_references::{
105    SelectorEditableDirectReferenceSiteV0, SelectorReferenceEngineSummaryV0,
106    SelectorReferenceSiteV0, SelectorReferenceSummaryV0, summarize_selector_reference_engine,
107};
108pub use source_evidence::{
109    BindingOriginEvidenceV0, CertaintyReasonEvidenceV0, ReferenceSiteIdentityEvidenceV0,
110    SourceInputPromotionEvidenceSummaryV0, StyleModuleEdgeEvidenceV0,
111    ValueDomainExplanationEvidenceV0, summarize_source_input_evidence,
112};
113pub use types::{
114    NestedSafetyCountsV0, ParserBoundarySyntaxFactsV0, ParserByteSpanV0,
115    ParserIndexComposesFactsV0, ParserIndexCustomPropertyDeclFactV0,
116    ParserIndexCustomPropertyFactsV0, ParserIndexCustomPropertyRefFactV0,
117    ParserIndexKeyframesFactsV0, ParserIndexSassModuleUseFactV0,
118    ParserIndexSassSameFileResolutionFactsV0, ParserIndexSelectorDefinitionFactV0,
119    ParserIndexSelectorFactsV0, ParserIndexValueFactsV0, ParserIndexWrapperFactsV0,
120    ParserLosslessCstFactsV0, ParserPositionV0, ParserRangeV0, ParserSassSyntaxFactsV0,
121    StyleContainerIndexV0, StyleContextBlockV0, StyleContextIndexV0,
122    StyleContextSelectorMembershipV0, StyleCustomPropertySemanticFactsV0, StyleLayerBlockBindingV0,
123    StyleLayerIndexV0, StyleLayerOrderNodeV0, StyleLayerStatementV0, StyleSassSemanticFactsV0,
124    StyleScopeIndexV0, StyleScopeRangeV0, StyleSelectorIdentityFactsV0, StyleSemanticFactsV0,
125    Stylesheet,
126};
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
129#[serde(rename_all = "camelCase")]
130pub struct StyleSemanticBoundarySummaryV0 {
131    pub schema_version: &'static str,
132    pub language: &'static str,
133    pub parser_facts: ParserBoundarySyntaxFactsV0,
134    pub semantic_facts: StyleSemanticFactsV0,
135    pub design_token_semantics: DesignTokenSemanticSummaryV0,
136    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
137    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
138    pub lossless_cst_contract: LosslessCstContractV0,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
142#[serde(rename_all = "camelCase")]
143pub struct StyleSemanticGraphSummaryV0 {
144    pub schema_version: &'static str,
145    pub product: &'static str,
146    pub language: &'static str,
147    pub parser_facts: ParserBoundarySyntaxFactsV0,
148    pub semantic_facts: StyleSemanticFactsV0,
149    pub css_modules_semantics: CssModulesSemanticSummaryV0,
150    pub design_token_semantics: DesignTokenSemanticSummaryV0,
151    pub selector_identity_engine: SelectorIdentityEngineSummaryV0,
152    pub selector_reference_engine: SelectorReferenceEngineSummaryV0,
153    pub source_input_evidence: SourceInputPromotionEvidenceSummaryV0,
154    pub promotion_evidence: SemanticPromotionEvidenceSummaryV0,
155    pub lossless_cst_contract: LosslessCstContractV0,
156}
157
158#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
159#[serde(rename_all = "camelCase")]
160pub struct StyleSemanticSoaTablesV0 {
161    pub schema_version: &'static str,
162    pub product: &'static str,
163    pub selector_names: SemanticNameSoaTableV0,
164    pub custom_property_names: SemanticNameSoaTableV0,
165    pub sass_names: SemanticNameSoaTableV0,
166    pub total_row_count: usize,
167    pub interned_row_count: usize,
168    pub ready_surfaces: Vec<&'static str>,
169}
170
171#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "camelCase")]
173pub struct StyleRuntimeIndexFactsV0 {
174    pub schema_version: &'static str,
175    pub product: &'static str,
176    pub style_path: String,
177    pub language: &'static str,
178    pub class_selector_names: Vec<String>,
179    pub custom_property_names: Vec<String>,
180    pub custom_property_decl_names: Vec<String>,
181    pub custom_property_ref_names: Vec<String>,
182    pub keyframe_names: Vec<String>,
183    pub animation_reference_names: Vec<String>,
184    pub ready_surfaces: Vec<&'static str>,
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
188#[serde(rename_all = "camelCase")]
189pub struct SemanticNameSoaTableV0 {
190    pub table_name: &'static str,
191    pub name_kind: &'static str,
192    pub row_indices: Vec<usize>,
193    pub names: Vec<String>,
194    pub interned_row_count: usize,
195    pub unique_name_count: usize,
196}
197
198pub fn summarize_style_semantic_boundary(sheet: &Stylesheet) -> StyleSemanticBoundarySummaryV0 {
199    summarize_omena_parser_style_semantic_boundary_from_source(&sheet.path, &sheet.source)
200}
201
202pub fn summarize_style_semantic_graph(
203    sheet: &Stylesheet,
204    input: &EngineInputV2,
205) -> StyleSemanticGraphSummaryV0 {
206    summarize_style_semantic_graph_for_path(sheet, input, None)
207}
208
209pub fn summarize_style_semantic_graph_for_path(
210    sheet: &Stylesheet,
211    input: &EngineInputV2,
212    style_path: Option<&str>,
213) -> StyleSemanticGraphSummaryV0 {
214    summarize_style_semantic_graph_for_path_with_workspace_declarations(
215        sheet,
216        input,
217        style_path,
218        &[],
219    )
220}
221
222pub fn summarize_style_semantic_graph_for_path_with_workspace_declarations(
223    sheet: &Stylesheet,
224    input: &EngineInputV2,
225    style_path: Option<&str>,
226    workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
227) -> StyleSemanticGraphSummaryV0 {
228    let (boundary, facts) = summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
229        &sheet.path,
230        &sheet.source,
231    );
232    let parser_facts = boundary.parser_facts;
233    let semantic_facts = boundary.semantic_facts;
234    let effective_style_path = style_path.or(Some(sheet.path.as_str()));
235    let design_token_semantics = summarize_design_token_semantics_with_workspace_declarations(
236        &parser_facts,
237        &semantic_facts,
238        effective_style_path,
239        workspace_declarations,
240    );
241    let css_modules_semantics = css_modules::summarize_css_modules_semantics_from_facts(&facts);
242    let selector_identity_engine =
243        summarize_selector_identity_engine(&semantic_facts.selector_identity);
244    let selector_reference_engine = summarize_selector_reference_engine(input, style_path);
245    let source_input_evidence = summarize_source_input_evidence(input);
246    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
247        &parser_facts,
248        &semantic_facts,
249        input,
250    );
251    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
252
253    StyleSemanticGraphSummaryV0 {
254        schema_version: "0",
255        product: "omena-semantic.style-semantic-graph",
256        language: boundary.language,
257        parser_facts,
258        semantic_facts,
259        css_modules_semantics,
260        design_token_semantics,
261        selector_identity_engine,
262        selector_reference_engine,
263        source_input_evidence,
264        promotion_evidence,
265        lossless_cst_contract,
266    }
267}
268
269pub fn summarize_style_semantic_graph_from_source(
270    style_path: &str,
271    style_source: &str,
272    input: &EngineInputV2,
273) -> Option<StyleSemanticGraphSummaryV0> {
274    dialect_for_style_path(style_path)?;
275    let (boundary, facts) = summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
276        style_path,
277        style_source,
278    );
279    let css_modules_semantics = css_modules::summarize_css_modules_semantics_from_facts(&facts);
280    let parser_facts = boundary.parser_facts;
281    let semantic_facts = boundary.semantic_facts;
282    let selector_reference_engine = summarize_selector_reference_engine(input, Some(style_path));
283    let source_input_evidence = summarize_source_input_evidence(input);
284    let promotion_evidence = summarize_semantic_promotion_evidence_with_source_input(
285        &parser_facts,
286        &semantic_facts,
287        input,
288    );
289
290    Some(StyleSemanticGraphSummaryV0 {
291        schema_version: "0",
292        product: "omena-semantic.style-semantic-graph",
293        language: boundary.language,
294        parser_facts,
295        semantic_facts,
296        css_modules_semantics,
297        design_token_semantics: boundary.design_token_semantics,
298        selector_identity_engine: boundary.selector_identity_engine,
299        selector_reference_engine,
300        source_input_evidence,
301        promotion_evidence,
302        lossless_cst_contract: boundary.lossless_cst_contract,
303    })
304}
305
306pub fn summarize_style_semantic_facts(sheet: &Stylesheet) -> StyleSemanticFactsV0 {
307    summarize_style_semantic_boundary(sheet).semantic_facts
308}
309
310pub fn summarize_style_runtime_index_facts_from_source(
311    style_path: &str,
312    style_source: &str,
313) -> Option<StyleRuntimeIndexFactsV0> {
314    dialect_for_style_path(style_path)?;
315    let boundary =
316        summarize_omena_parser_style_semantic_boundary_from_source(style_path, style_source);
317    let custom_property_names = boundary
318        .semantic_facts
319        .custom_properties
320        .decl_names
321        .iter()
322        .chain(boundary.semantic_facts.custom_properties.ref_names.iter())
323        .cloned()
324        .collect::<BTreeSet<_>>()
325        .into_iter()
326        .collect();
327
328    Some(StyleRuntimeIndexFactsV0 {
329        schema_version: "0",
330        product: "omena-semantic.style-runtime-index-facts",
331        style_path: style_path.to_string(),
332        language: boundary.language,
333        class_selector_names: boundary.parser_facts.selectors.names,
334        custom_property_names,
335        custom_property_decl_names: boundary.semantic_facts.custom_properties.decl_names,
336        custom_property_ref_names: boundary.semantic_facts.custom_properties.ref_names,
337        keyframe_names: boundary.parser_facts.keyframes.names,
338        animation_reference_names: boundary.parser_facts.keyframes.animation_ref_names,
339        ready_surfaces: vec![
340            "semanticRuntimeIndexFacts",
341            "customPropertyRuntimeIndex",
342            "keyframeRuntimeIndex",
343        ],
344    })
345}
346
347pub fn summarize_style_semantic_soa_tables(
348    semantic_facts: &StyleSemanticFactsV0,
349    db: &dyn salsa::Database,
350) -> StyleSemanticSoaTablesV0 {
351    let selector_names = semantic_name_soa_table(
352        "selectors",
353        "className",
354        semantic_facts.selector_identity.canonical_names.as_slice(),
355        |name| intern_class_name(db, name).is_ok(),
356    );
357    let custom_property_names = semantic_name_soa_table(
358        "customProperties",
359        "customPropertyName",
360        semantic_facts.custom_properties.decl_names.as_slice(),
361        |name| intern_custom_property_name(db, name).is_ok(),
362    );
363    let mut sass_name_sources = Vec::new();
364    sass_name_sources.extend(
365        semantic_facts
366            .sass
367            .same_file_resolution
368            .resolved_variable_ref_names
369            .iter()
370            .cloned(),
371    );
372    sass_name_sources.extend(
373        semantic_facts
374            .sass
375            .same_file_resolution
376            .unresolved_variable_ref_names
377            .iter()
378            .cloned(),
379    );
380    sass_name_sources.extend(
381        semantic_facts
382            .sass
383            .same_file_resolution
384            .resolved_mixin_include_names
385            .iter()
386            .cloned(),
387    );
388    sass_name_sources.extend(
389        semantic_facts
390            .sass
391            .same_file_resolution
392            .unresolved_mixin_include_names
393            .iter()
394            .cloned(),
395    );
396    sass_name_sources.extend(
397        semantic_facts
398            .sass
399            .same_file_resolution
400            .resolved_function_call_names
401            .iter()
402            .cloned(),
403    );
404    let sass_names =
405        semantic_name_soa_table("sass", "cssIdentOrMixinName", &sass_name_sources, |name| {
406            intern_css_ident(db, name).is_ok()
407                || intern_mixin_name(db, name).is_ok()
408                || intern_keyframes_name(db, name).is_ok()
409        });
410    let total_row_count = selector_names.row_indices.len()
411        + custom_property_names.row_indices.len()
412        + sass_names.row_indices.len();
413    let interned_row_count = selector_names.interned_row_count
414        + custom_property_names.interned_row_count
415        + sass_names.interned_row_count;
416
417    StyleSemanticSoaTablesV0 {
418        schema_version: "0",
419        product: "omena-semantic.soa-tables",
420        selector_names,
421        custom_property_names,
422        sass_names,
423        total_row_count,
424        interned_row_count,
425        ready_surfaces: vec!["semanticSoaTables", "semanticSoaNameTables"],
426    }
427}
428
429fn semantic_name_soa_table(
430    table_name: &'static str,
431    name_kind: &'static str,
432    names: &[String],
433    mut intern: impl FnMut(&str) -> bool,
434) -> SemanticNameSoaTableV0 {
435    let mut unique_names = BTreeSet::new();
436    let mut interned_row_count = 0usize;
437    for name in names {
438        unique_names.insert(name.clone());
439        if intern(name) {
440            interned_row_count += 1;
441        }
442    }
443
444    SemanticNameSoaTableV0 {
445        table_name,
446        name_kind,
447        row_indices: (0..names.len()).collect(),
448        names: names.to_vec(),
449        interned_row_count,
450        unique_name_count: unique_names.len(),
451    }
452}
453
454pub fn summarize_parser_contract_facts(sheet: &Stylesheet) -> ParserBoundarySyntaxFactsV0 {
455    summarize_style_semantic_boundary(sheet).parser_facts
456}
457
458pub fn parse_style_module(path: &str, source: &str) -> Option<Stylesheet> {
459    Some(Stylesheet {
460        path: path.to_string(),
461        language: dialect_for_style_path(path)?,
462        source: source.to_string(),
463    })
464}
465
466pub fn summarize_omena_parser_style_semantic_boundary_from_source(
467    style_path: &str,
468    style_source: &str,
469) -> StyleSemanticBoundarySummaryV0 {
470    summarize_omena_parser_style_semantic_boundary_with_facts_from_source(style_path, style_source)
471        .0
472}
473
474/// Project declaration syntax and semantic wrapper context from one parser CST.
475///
476/// Consumers that need both inputs should use this boundary instead of parsing
477/// once for declarations and again for the semantic context index.
478pub fn collect_parser_declaration_syntax_and_style_context_from_source(
479    source: &str,
480    dialect: StyleDialect,
481) -> (Vec<ParserDeclarationSyntaxFactV0>, StyleContextIndexV0) {
482    let parsed = parse(source, dialect);
483    collect_parser_declaration_syntax_and_style_context_from_parse(source, &parsed)
484}
485
486/// Project declaration syntax and semantic wrapper context from an existing
487/// parser result.
488///
489/// This boundary lets tracked query owners share one CST materialization with
490/// their other parser-fact projections.
491pub fn collect_parser_declaration_syntax_and_style_context_from_parse(
492    source: &str,
493    parsed: &ParseResult,
494) -> (Vec<ParserDeclarationSyntaxFactV0>, StyleContextIndexV0) {
495    let declarations = ProductSyntaxIndexV0::declarations_from_parse(source, parsed);
496    let cst = parsed.cst();
497    let context_index = summarize_style_context_index(source, &cst);
498    (declarations, context_index)
499}
500
501fn summarize_omena_parser_style_semantic_boundary_with_facts_from_source(
502    style_path: &str,
503    style_source: &str,
504) -> (StyleSemanticBoundarySummaryV0, ParsedStyleFacts) {
505    let dialect = omena_parser_dialect_for_style_path(style_path);
506    let parsed = parse(style_source, dialect);
507    let facts = facts_from_cst(style_source, &parsed);
508    let cst = parsed.cst();
509    let parser_facts = summarize_omena_parser_contract_facts(
510        style_source,
511        parsed.token_count(),
512        parsed.syntax().children().count(),
513        parsed.errors().len(),
514        &facts,
515        &cst,
516    );
517    let semantic_facts =
518        summarize_omena_parser_semantic_facts(style_source, &facts, &parser_facts, &cst);
519    let design_token_semantics = summarize_design_token_semantics(&parser_facts, &semantic_facts);
520    let selector_identity_engine =
521        summarize_selector_identity_engine(&semantic_facts.selector_identity);
522    let promotion_evidence = summarize_semantic_promotion_evidence(&parser_facts, &semantic_facts);
523    let lossless_cst_contract = summarize_lossless_cst_contract(&parser_facts.lossless_cst);
524
525    (
526        StyleSemanticBoundarySummaryV0 {
527            schema_version: "0",
528            language: omena_parser_dialect_label(dialect),
529            parser_facts,
530            semantic_facts,
531            design_token_semantics,
532            selector_identity_engine,
533            promotion_evidence,
534            lossless_cst_contract,
535        },
536        facts,
537    )
538}
539
540fn summarize_omena_parser_contract_facts(
541    source: &str,
542    token_count: usize,
543    root_node_count: usize,
544    diagnostic_count: usize,
545    facts: &ParsedStyleFacts,
546    cst: &ParsedCst,
547) -> ParserBoundarySyntaxFactsV0 {
548    let (all_token_spans_within_source, all_node_spans_within_source) =
549        cst_span_bounds_within_source(cst, source.len());
550    ParserBoundarySyntaxFactsV0 {
551        lossless_cst: ParserLosslessCstFactsV0 {
552            source_byte_len: source.len(),
553            token_count,
554            root_node_count,
555            diagnostic_count,
556            all_token_spans_within_source,
557            all_node_spans_within_source,
558        },
559        selectors: summarize_omena_parser_selector_facts(source, facts),
560        values: summarize_omena_parser_value_facts(facts),
561        custom_properties: summarize_omena_parser_custom_property_facts(source, facts, cst),
562        sass: summarize_omena_parser_sass_syntax_facts(facts),
563        keyframes: summarize_omena_parser_keyframe_facts(facts),
564        composes: summarize_omena_parser_composes_facts(facts),
565        wrappers: ParserIndexWrapperFactsV0::default(),
566    }
567}
568
569fn cst_span_bounds_within_source(cst: &ParsedCst, source_byte_len: usize) -> (bool, bool) {
570    let all_token_spans_within_source = cst
571        .root()
572        .descendants_with_tokens()
573        .filter_map(|element| element.into_token())
574        .all(|token| {
575            let range = token.text_range();
576            byte_offsets_within_source(
577                u32::from(range.start()) as usize,
578                u32::from(range.end()) as usize,
579                source_byte_len,
580            )
581        });
582    let all_node_spans_within_source = std::iter::once(cst.root())
583        .chain(cst.root().descendants())
584        .all(|node| {
585            let range = node.text_range();
586            byte_offsets_within_source(
587                u32::from(range.start()) as usize,
588                u32::from(range.end()) as usize,
589                source_byte_len,
590            )
591        });
592    (all_token_spans_within_source, all_node_spans_within_source)
593}
594
595fn byte_offsets_within_source(start: usize, end: usize, source_byte_len: usize) -> bool {
596    start <= end && end <= source_byte_len
597}
598
599fn summarize_omena_parser_semantic_facts(
600    source: &str,
601    facts: &ParsedStyleFacts,
602    parser_facts: &ParserBoundarySyntaxFactsV0,
603    cst: &ParsedCst,
604) -> StyleSemanticFactsV0 {
605    let custom_properties =
606        summarize_omena_parser_custom_property_semantic_facts(&parser_facts.custom_properties);
607    let sass_same_file_resolution =
608        summarize_omena_parser_sass_same_file_resolution(&parser_facts.sass);
609    let sass_selector_resolution =
610        summarize_omena_parser_sass_selector_resolution(facts, &sass_same_file_resolution, cst);
611    StyleSemanticFactsV0 {
612        selector_identity: StyleSelectorIdentityFactsV0 {
613            canonical_names: parser_facts.selectors.names.clone(),
614            bem_suffix_safe_names: parser_facts.selectors.bem_suffix_safe_names.clone(),
615            bem_suffix_parent_names: parser_facts.selectors.bem_suffix_parent_names.clone(),
616            nested_unsafe_names: parser_facts.selectors.nested_unsafe_names.clone(),
617            nested_safety_counts: parser_facts.selectors.nested_safety_counts.clone(),
618        },
619        custom_properties,
620        sass: StyleSassSemanticFactsV0 {
621            selector_symbol_facts: Vec::new(),
622            selectors_with_resolved_variable_refs_names: sass_selector_resolution
623                .resolved_variable_ref_selectors,
624            selectors_with_unresolved_variable_refs_names: sass_selector_resolution
625                .unresolved_variable_ref_selectors,
626            selectors_with_resolved_mixin_includes_names: sass_selector_resolution
627                .resolved_mixin_include_selectors,
628            selectors_with_unresolved_mixin_includes_names: sass_selector_resolution
629                .unresolved_mixin_include_selectors,
630            selectors_with_function_calls_names: parser_facts.sass.function_call_names.clone(),
631            same_file_resolution: sass_same_file_resolution,
632        },
633        context_index: summarize_style_context_index(source, cst),
634    }
635}
636
637fn summarize_style_context_index(source: &str, cst: &ParsedCst) -> StyleContextIndexV0 {
638    if !source_may_have_style_context(source) {
639        return empty_style_context_index();
640    }
641    let layer_statements = layer_statement_facts_from_cst(source, cst);
642    let (context_blocks, memberships) = style_context_blocks_and_memberships_from_cst(source, cst);
643    let block_layers = context_blocks
644        .iter()
645        .filter(|block| block.kind == "layer")
646        .cloned()
647        .collect::<Vec<_>>();
648    let containers = context_blocks
649        .iter()
650        .filter(|block| block.kind == "container")
651        .cloned()
652        .collect::<Vec<_>>();
653    let scopes = context_blocks
654        .iter()
655        .filter(|block| block.kind == "scope")
656        .cloned()
657        .collect::<Vec<_>>();
658    let layer_memberships = memberships
659        .iter()
660        .filter(|membership| membership.context_kind == "layer")
661        .cloned()
662        .collect::<Vec<_>>();
663    let container_memberships = memberships
664        .iter()
665        .filter(|membership| membership.context_kind == "container")
666        .cloned()
667        .collect::<Vec<_>>();
668    let scope_memberships = memberships
669        .iter()
670        .filter(|membership| membership.context_kind == "scope")
671        .cloned()
672        .collect::<Vec<_>>();
673    let scope_ranges = scopes
674        .iter()
675        .map(|scope| {
676            let parsed = parse_scope_range_prelude(scope.prelude.as_str());
677            let statically_derivable = parsed.is_some();
678            StyleScopeRangeV0 {
679                context_id: scope.id.clone(),
680                root_selector: parsed.as_ref().map(|(root, _)| root.clone()),
681                limit_selector: parsed.and_then(|(_, limit)| limit),
682                statically_derivable,
683            }
684        })
685        .collect::<Vec<_>>();
686    let layer_order = layer_tree::summarize_layer_order_from_cst(source, cst);
687
688    StyleContextIndexV0 {
689        schema_version: "0",
690        product: "omena-semantic.style-context-index",
691        layer_index: StyleLayerIndexV0 {
692            statement_layers: layer_statements,
693            anonymous_layer_block_count: block_layers
694                .iter()
695                .filter(|block| block.name.is_none())
696                .count(),
697            block_layers,
698            selector_memberships: layer_memberships,
699            named_layer_count: layer_order.order_nodes.len(),
700            order_nodes: layer_order.order_nodes,
701            block_bindings: layer_order.block_bindings,
702            unresolved_topology_count: layer_order.unresolved_topology_count,
703            topology_complete: layer_order.topology_complete,
704        },
705        container_index: StyleContainerIndexV0 {
706            named_container_count: containers
707                .iter()
708                .filter(|block| block.name.is_some())
709                .count(),
710            anonymous_container_count: containers
711                .iter()
712                .filter(|block| block.name.is_none())
713                .count(),
714            containers,
715            selector_memberships: container_memberships,
716        },
717        scope_index: StyleScopeIndexV0 {
718            scoped_selector_count: scope_memberships
719                .iter()
720                .map(|membership| membership.selector_name.as_str())
721                .collect::<BTreeSet<_>>()
722                .len(),
723            scopes,
724            unresolved_range_count: scope_ranges
725                .iter()
726                .filter(|range| !range.statically_derivable)
727                .count(),
728            ranges: scope_ranges,
729            selector_memberships: scope_memberships,
730        },
731        selector_context_count: memberships.len(),
732        ready_surfaces: vec![
733            "layerIndex",
734            "containerIndex",
735            "scopeIndex",
736            "selectorContextMembership",
737        ],
738    }
739}
740
741fn source_may_have_style_context(source: &str) -> bool {
742    if source.as_bytes().contains(&b'\\') {
743        return true;
744    }
745    source.match_indices('@').any(|(offset, _)| {
746        let suffix = &source[offset..];
747        ["@layer", "@container", "@scope"]
748            .into_iter()
749            .any(|keyword| css_keyword(suffix).strip_prefix(keyword).is_some())
750    })
751}
752
753fn empty_style_context_index() -> StyleContextIndexV0 {
754    StyleContextIndexV0 {
755        schema_version: "0",
756        product: "omena-semantic.style-context-index",
757        layer_index: StyleLayerIndexV0 {
758            topology_complete: true,
759            ..StyleLayerIndexV0::default()
760        },
761        container_index: StyleContainerIndexV0::default(),
762        scope_index: StyleScopeIndexV0::default(),
763        selector_context_count: 0,
764        ready_surfaces: vec![
765            "layerIndex",
766            "containerIndex",
767            "scopeIndex",
768            "selectorContextMembership",
769        ],
770    }
771}
772
773/// Build the canonical nested cascade-layer order from the parser CST.
774pub fn summarize_style_layer_order_from_source(
775    source: &str,
776    dialect: StyleDialect,
777) -> StyleLayerIndexV0 {
778    // A case-insensitive substring probe may produce false positives in comments
779    // or strings, which only fall through to the parser. It cannot skip a source
780    // containing a literal layer at-keyword.
781    if !css_keyword(source).contains("@layer") {
782        return StyleLayerIndexV0 {
783            topology_complete: true,
784            ..StyleLayerIndexV0::default()
785        };
786    }
787    let parsed = parse(source, dialect);
788    let cst = parsed.cst();
789    let context = summarize_style_context_index(source, &cst);
790    context.layer_index
791}
792
793fn layer_statement_facts_from_cst(source: &str, cst: &ParsedCst) -> Vec<StyleLayerStatementV0> {
794    let mut statements = Vec::new();
795    for node in cst
796        .root()
797        .descendants()
798        .filter(|node| node.kind() == SyntaxKind::LayerRule)
799    {
800        if cst_node_has_block(node) {
801            continue;
802        }
803        let range = node.text_range();
804        let byte_span = ParserByteSpanV0 {
805            start: u32::from(range.start()) as usize,
806            end: u32::from(range.end()) as usize,
807        };
808        for layer_name in node
809            .descendants()
810            .filter(|child| child.kind() == SyntaxKind::LayerName)
811            .flat_map(|child| split_layer_names(&syntax_node_text(child)))
812        {
813            statements.push(StyleLayerStatementV0 {
814                name: layer_name,
815                source_order: statements.len(),
816                byte_span,
817                range: parser_range_for_byte_span(source, byte_span),
818            });
819        }
820    }
821    statements
822}
823
824fn style_context_blocks_and_memberships_from_cst(
825    source: &str,
826    cst: &ParsedCst,
827) -> (
828    Vec<StyleContextBlockV0>,
829    Vec<StyleContextSelectorMembershipV0>,
830) {
831    let mut context_nodes = Vec::new();
832    let mut blocks = Vec::new();
833    for node in cst
834        .root()
835        .descendants()
836        .filter(|node| cst_context_kind(node.kind()).is_some() && cst_node_has_block(node))
837    {
838        let Some(context) = style_context_block_for_cst_node(source, node, blocks.len()) else {
839            continue;
840        };
841        context_nodes.push((node, context.clone()));
842        blocks.push(context);
843    }
844
845    let mut memberships = Vec::new();
846    for rule in cst
847        .root()
848        .descendants()
849        .filter(|node| node.kind() == SyntaxKind::Rule)
850    {
851        let selector_names = class_names_from_rule_node(rule);
852        if selector_names.is_empty() {
853            continue;
854        }
855        for context in cst_context_blocks_for_rule(rule, &context_nodes) {
856            for selector_name in &selector_names {
857                memberships.push(StyleContextSelectorMembershipV0 {
858                    selector_name: selector_name.clone(),
859                    context_id: context.id.clone(),
860                    context_kind: context.kind,
861                    source_order: memberships.len(),
862                });
863            }
864        }
865    }
866
867    (blocks, memberships)
868}
869
870fn style_context_block_for_cst_node(
871    source: &str,
872    node: &SyntaxNode,
873    source_order: usize,
874) -> Option<StyleContextBlockV0> {
875    let kind = cst_context_kind(node.kind())?;
876    let prelude = cst_context_prelude(node);
877    let name = match kind {
878        "layer" => split_layer_names(&prelude).into_iter().next(),
879        "container" => container_name_from_prelude(&prelude),
880        "scope" => None,
881        _ => None,
882    };
883    let header_end = cst_node_block_open_end(node)?;
884    let byte_span = ParserByteSpanV0 {
885        start: u32::from(node.text_range().start()) as usize,
886        end: header_end,
887    };
888
889    Some(StyleContextBlockV0 {
890        id: format!("{kind}:{source_order}"),
891        kind,
892        name,
893        prelude,
894        source_order,
895        byte_span,
896        range: parser_range_for_byte_span(source, byte_span),
897    })
898}
899
900fn cst_context_kind(kind: SyntaxKind) -> Option<&'static str> {
901    match kind {
902        SyntaxKind::LayerRule => Some("layer"),
903        SyntaxKind::ContainerRule => Some("container"),
904        SyntaxKind::ScopeRule => Some("scope"),
905        _ => None,
906    }
907}
908
909fn cst_context_prelude(node: &SyntaxNode) -> String {
910    if node.kind() == SyntaxKind::LayerRule {
911        return layer_rule_prelude(node);
912    }
913    let prelude_kind = match node.kind() {
914        SyntaxKind::ContainerRule => SyntaxKind::ContainerCondition,
915        SyntaxKind::ScopeRule => SyntaxKind::ScopeRange,
916        _ => return String::new(),
917    };
918    node.descendants()
919        .find(|child| child.kind() == prelude_kind)
920        .map(|child| syntax_node_text(child).trim().to_string())
921        .unwrap_or_default()
922}
923
924fn layer_rule_prelude(node: &SyntaxNode) -> String {
925    let text = syntax_node_text(node);
926    let Some(rest) = css_keyword(text.trim_start()).strip_prefix("@layer") else {
927        return String::new();
928    };
929    rest.split(['{', ';', '\n'])
930        .next()
931        .unwrap_or_default()
932        .trim()
933        .to_string()
934}
935
936fn cst_node_has_block(node: &SyntaxNode) -> bool {
937    node.descendants_with_tokens()
938        .filter_map(|element| element.into_token())
939        .any(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
940}
941
942fn cst_node_block_open_end(node: &SyntaxNode) -> Option<usize> {
943    node.descendants_with_tokens()
944        .filter_map(|element| element.into_token())
945        .find(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
946        .map(|token| u32::from(token.text_range().end()) as usize)
947}
948
949fn cst_context_blocks_for_rule<'a>(
950    rule: &SyntaxNode,
951    contexts: &'a [(&'a SyntaxNode, StyleContextBlockV0)],
952) -> Vec<&'a StyleContextBlockV0> {
953    contexts
954        .iter()
955        .filter(|(node, _)| {
956            rule.text_range().start() > node.text_range().start()
957                && rule.text_range().end() < node.text_range().end()
958        })
959        .map(|(_, context)| context)
960        .collect()
961}
962
963fn class_names_from_rule_node(rule: &SyntaxNode) -> Vec<String> {
964    let mut names = BTreeSet::new();
965    for child in rule.children() {
966        if matches!(
967            child.kind(),
968            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
969        ) {
970            break;
971        }
972        for class_node in child
973            .descendants()
974            .filter(|node| node.kind() == SyntaxKind::ClassSelector)
975        {
976            if let Some(name) = class_selector_name_from_cst_node(class_node) {
977                names.insert(name);
978            }
979        }
980    }
981    names.into_iter().collect()
982}
983
984fn class_selector_name_from_cst_node(node: &SyntaxNode) -> Option<String> {
985    syntax_node_text(node)
986        .trim()
987        .strip_prefix('.')
988        .filter(|name| !name.is_empty())
989        .map(ToString::to_string)
990}
991
992fn syntax_node_text(node: &SyntaxNode) -> String {
993    node.try_resolved()
994        .map(|resolved| resolved.text().to_string())
995        .unwrap_or_default()
996}
997
998fn split_layer_names(prelude: &str) -> Vec<String> {
999    prelude
1000        .split(',')
1001        .filter_map(|name| {
1002            let name = name.trim();
1003            if name.is_empty() || name == "{" {
1004                None
1005            } else {
1006                Some(name.to_string())
1007            }
1008        })
1009        .collect()
1010}
1011
1012fn container_name_from_prelude(prelude: &str) -> Option<String> {
1013    let trimmed = prelude.trim();
1014    if trimmed.is_empty() || trimmed.starts_with('(') || trimmed.starts_with("style(") {
1015        return None;
1016    }
1017    let name = trimmed.split_whitespace().next().unwrap_or_default().trim();
1018    if css_identifier_text_is_plain(name) {
1019        Some(name.to_string())
1020    } else {
1021        None
1022    }
1023}
1024
1025fn parse_scope_range_prelude(prelude: &str) -> Option<(String, Option<String>)> {
1026    let prelude = prelude.trim();
1027    if prelude.is_empty() {
1028        return Some((":scope".to_string(), None));
1029    }
1030    let (root, limit) = match prelude.split_once(" to ") {
1031        Some((root, limit)) => (root, Some(limit)),
1032        None => (prelude, None),
1033    };
1034    let unwrap_selector = |value: &str| {
1035        value
1036            .trim()
1037            .strip_prefix('(')
1038            .and_then(|value| value.strip_suffix(')'))
1039            .map(str::trim)
1040            .filter(|value| !value.is_empty())
1041            .map(str::to_string)
1042    };
1043    let limit = match limit {
1044        Some(limit) => Some(unwrap_selector(limit)?),
1045        None => None,
1046    };
1047    Some((unwrap_selector(root)?, limit))
1048}
1049
1050fn css_identifier_text_is_plain(value: &str) -> bool {
1051    let mut chars = value.chars();
1052    let Some(first) = chars.next() else {
1053        return false;
1054    };
1055    (first.is_ascii_alphabetic() || matches!(first, '_' | '-'))
1056        && chars.all(|char| char.is_ascii_alphanumeric() || matches!(char, '_' | '-'))
1057}
1058
1059fn summarize_omena_parser_selector_facts(
1060    source: &str,
1061    facts: &ParsedStyleFacts,
1062) -> ParserIndexSelectorFactsV0 {
1063    let mut names = Vec::new();
1064    let mut definition_facts = Vec::new();
1065    let mut bem_suffix_parent_names = BTreeSet::new();
1066    let mut bem_suffix_safe_names = BTreeSet::new();
1067    let mut nested_unsafe_names = BTreeSet::new();
1068    let mut source_order = 0usize;
1069
1070    for selector in &facts.selectors {
1071        if selector.kind != ParsedSelectorFactKind::Class {
1072            continue;
1073        }
1074        let byte_span = parser_byte_span_for_offsets(
1075            u32::from(selector.range.start()) as usize,
1076            u32::from(selector.range.end()) as usize,
1077        );
1078        let parent_name = bem_suffix_parent_name(selector.name.as_str());
1079        let nested_safety_kind = if let Some(parent) = parent_name.clone() {
1080            bem_suffix_parent_names.insert(parent);
1081            bem_suffix_safe_names.insert(selector.name.clone());
1082            "bemSuffixSafe"
1083        } else if selector_has_parent_ampersand_class_prefix(source, byte_span.start) {
1084            nested_unsafe_names.insert(selector.name.clone());
1085            "nestedUnsafe"
1086        } else {
1087            "flat"
1088        };
1089        names.push(selector.name.clone());
1090        definition_facts.push(ParserIndexSelectorDefinitionFactV0 {
1091            name: selector.name.clone(),
1092            source_order,
1093            byte_span,
1094            range: parser_range_for_byte_span(source, byte_span),
1095            nested_safety_kind,
1096            bem_suffix_parent_name: parent_name,
1097            under_media: false,
1098            under_supports: false,
1099            under_layer: false,
1100        });
1101        source_order += 1;
1102    }
1103
1104    names.sort();
1105    names.dedup();
1106    definition_facts.sort();
1107    let bem_suffix_safe_names = bem_suffix_safe_names.into_iter().collect::<Vec<_>>();
1108    let nested_unsafe_names = nested_unsafe_names.into_iter().collect::<Vec<_>>();
1109    ParserIndexSelectorFactsV0 {
1110        names,
1111        definition_facts,
1112        bem_suffix_parent_names: bem_suffix_parent_names.into_iter().collect(),
1113        bem_suffix_safe_names: bem_suffix_safe_names.clone(),
1114        nested_unsafe_names: nested_unsafe_names.clone(),
1115        selectors_with_value_refs_names: Vec::new(),
1116        selectors_with_animation_ref_names: Vec::new(),
1117        selectors_with_animation_name_ref_names: Vec::new(),
1118        bem_suffix_count: bem_suffix_safe_names.len(),
1119        nested_safety_counts: NestedSafetyCountsV0 {
1120            flat: source_order
1121                .saturating_sub(bem_suffix_safe_names.len())
1122                .saturating_sub(nested_unsafe_names.len()),
1123            bem_suffix_safe: bem_suffix_safe_names.len(),
1124            nested_unsafe: nested_unsafe_names.len(),
1125        },
1126    }
1127}
1128
1129fn summarize_omena_parser_value_facts(facts: &ParsedStyleFacts) -> ParserIndexValueFactsV0 {
1130    let mut decl_names = BTreeSet::new();
1131    let mut ref_names = BTreeSet::new();
1132    let mut import_sources = BTreeSet::new();
1133    for value in &facts.css_module_values {
1134        match value.kind {
1135            ParsedCssModuleValueFactKind::Definition => {
1136                decl_names.insert(value.name.clone());
1137            }
1138            ParsedCssModuleValueFactKind::Reference => {
1139                ref_names.insert(value.name.clone());
1140            }
1141            ParsedCssModuleValueFactKind::ImportSource => {
1142                import_sources.insert(value.name.clone());
1143            }
1144        }
1145    }
1146    ParserIndexValueFactsV0 {
1147        decl_names: decl_names.into_iter().collect(),
1148        import_sources: import_sources.into_iter().collect(),
1149        import_alias_count: facts.css_module_value_import_edge_count,
1150        ref_names: ref_names.clone().into_iter().collect(),
1151        local_ref_names: ref_names.into_iter().collect(),
1152        ..ParserIndexValueFactsV0::default()
1153    }
1154}
1155
1156fn summarize_omena_parser_custom_property_facts(
1157    source: &str,
1158    facts: &ParsedStyleFacts,
1159    cst: &ParsedCst,
1160) -> ParserIndexCustomPropertyFactsV0 {
1161    let mut decl_names = BTreeSet::new();
1162    let mut ref_names = BTreeSet::new();
1163    let mut decl_facts = Vec::new();
1164    let mut ref_facts = Vec::new();
1165    for variable in &facts.variables {
1166        match variable.kind {
1167            ParsedVariableFactKind::CustomPropertyDeclaration => {
1168                let byte_span = parser_byte_span_for_offsets(
1169                    u32::from(variable.range.start()) as usize,
1170                    u32::from(variable.range.end()) as usize,
1171                );
1172                decl_names.insert(variable.name.clone());
1173                let context = style_context_for_cst_offset(source, cst, byte_span.start);
1174                decl_facts.push(ParserIndexCustomPropertyDeclFactV0 {
1175                    name: variable.name.clone(),
1176                    value: declaration_value_text(source, byte_span.start),
1177                    source_order: decl_facts.len(),
1178                    byte_span,
1179                    range: parser_range_for_byte_span(source, byte_span),
1180                    selector_contexts: context.selector_contexts,
1181                    condition_context: context.condition_context,
1182                    layer_names: context.layer_names,
1183                    under_media: context.under_media,
1184                    under_supports: context.under_supports,
1185                    under_layer: context.under_layer,
1186                });
1187            }
1188            ParsedVariableFactKind::CustomPropertyReference => {
1189                let byte_offset = u32::from(variable.range.start()) as usize;
1190                let context = style_context_for_cst_offset(source, cst, byte_offset);
1191                ref_names.insert(variable.name.clone());
1192                ref_facts.push(ParserIndexCustomPropertyRefFactV0 {
1193                    name: variable.name.clone(),
1194                    source_order: ref_facts.len(),
1195                    selector_contexts: context.selector_contexts,
1196                    condition_context: context.condition_context,
1197                    layer_names: context.layer_names,
1198                    under_media: context.under_media,
1199                    under_supports: context.under_supports,
1200                    under_layer: context.under_layer,
1201                });
1202            }
1203            _ => {}
1204        }
1205    }
1206    let selectors_with_refs_names = ref_facts
1207        .iter()
1208        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1209        .collect::<BTreeSet<_>>();
1210    let selectors_with_refs_under_media_names = ref_facts
1211        .iter()
1212        .filter(|reference| reference.under_media)
1213        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1214        .collect::<BTreeSet<_>>();
1215    let selectors_with_refs_under_supports_names = ref_facts
1216        .iter()
1217        .filter(|reference| reference.under_supports)
1218        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1219        .collect::<BTreeSet<_>>();
1220    let selectors_with_refs_under_layer_names = ref_facts
1221        .iter()
1222        .filter(|reference| reference.under_layer)
1223        .flat_map(|reference| reference.selector_contexts.iter().cloned())
1224        .collect::<BTreeSet<_>>();
1225    let decl_context_selectors = decl_facts
1226        .iter()
1227        .flat_map(|declaration| declaration.selector_contexts.iter().cloned())
1228        .collect::<BTreeSet<_>>();
1229    let decl_names_under_media = decl_facts
1230        .iter()
1231        .filter(|declaration| declaration.under_media)
1232        .map(|declaration| declaration.name.clone())
1233        .collect::<BTreeSet<_>>();
1234    let decl_names_under_supports = decl_facts
1235        .iter()
1236        .filter(|declaration| declaration.under_supports)
1237        .map(|declaration| declaration.name.clone())
1238        .collect::<BTreeSet<_>>();
1239    let decl_names_under_layer = decl_facts
1240        .iter()
1241        .filter(|declaration| declaration.under_layer)
1242        .map(|declaration| declaration.name.clone())
1243        .collect::<BTreeSet<_>>();
1244
1245    ParserIndexCustomPropertyFactsV0 {
1246        decl_names: decl_names.into_iter().collect(),
1247        decl_facts,
1248        decl_context_selectors: decl_context_selectors.into_iter().collect(),
1249        decl_names_under_media: decl_names_under_media.into_iter().collect(),
1250        decl_names_under_supports: decl_names_under_supports.into_iter().collect(),
1251        decl_names_under_layer: decl_names_under_layer.into_iter().collect(),
1252        ref_names: ref_names.into_iter().collect(),
1253        ref_facts,
1254        selectors_with_refs_names: selectors_with_refs_names.into_iter().collect(),
1255        selectors_with_refs_under_media_names: selectors_with_refs_under_media_names
1256            .into_iter()
1257            .collect(),
1258        selectors_with_refs_under_supports_names: selectors_with_refs_under_supports_names
1259            .into_iter()
1260            .collect(),
1261        selectors_with_refs_under_layer_names: selectors_with_refs_under_layer_names
1262            .into_iter()
1263            .collect(),
1264    }
1265}
1266
1267fn summarize_omena_parser_sass_syntax_facts(facts: &ParsedStyleFacts) -> ParserSassSyntaxFactsV0 {
1268    let mut variable_decl_names = BTreeSet::new();
1269    let mut variable_ref_names = BTreeSet::new();
1270    let mut mixin_decl_names = BTreeSet::new();
1271    let mut mixin_include_names = BTreeSet::new();
1272    let mut function_decl_names = BTreeSet::new();
1273    let mut function_call_names = BTreeSet::new();
1274    for symbol in &facts.sass_symbols {
1275        match symbol.kind {
1276            ParsedSassSymbolFactKind::VariableDeclaration => {
1277                variable_decl_names.insert(symbol.name.clone());
1278            }
1279            ParsedSassSymbolFactKind::VariableReference => {
1280                variable_ref_names.insert(symbol.name.clone());
1281            }
1282            ParsedSassSymbolFactKind::MixinDeclaration => {
1283                mixin_decl_names.insert(symbol.name.clone());
1284            }
1285            ParsedSassSymbolFactKind::MixinInclude => {
1286                mixin_include_names.insert(symbol.name.clone());
1287            }
1288            ParsedSassSymbolFactKind::FunctionDeclaration => {
1289                function_decl_names.insert(symbol.name.clone());
1290            }
1291            ParsedSassSymbolFactKind::FunctionCall => {
1292                function_call_names.insert(symbol.name.clone());
1293            }
1294        }
1295    }
1296    let mut module_use_sources = BTreeSet::new();
1297    let mut module_use_edges = Vec::new();
1298    let mut module_forward_sources = BTreeSet::new();
1299    let mut module_import_sources = BTreeSet::new();
1300    for edge in &facts.sass_module_edges {
1301        match edge.kind {
1302            ParsedSassModuleEdgeFactKind::Use => {
1303                module_use_sources.insert(edge.source.clone());
1304                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1305                    source: edge.source.clone(),
1306                    namespace_kind: edge.namespace_kind.unwrap_or("default"),
1307                    namespace: edge.namespace.clone(),
1308                });
1309            }
1310            ParsedSassModuleEdgeFactKind::Forward => {
1311                module_forward_sources.insert(edge.source.clone());
1312            }
1313            ParsedSassModuleEdgeFactKind::Import => {
1314                module_import_sources.insert(edge.source.clone());
1315                module_use_edges.push(ParserIndexSassModuleUseFactV0 {
1316                    source: edge.source.clone(),
1317                    namespace_kind: "wildcard",
1318                    namespace: None,
1319                });
1320            }
1321        }
1322    }
1323    ParserSassSyntaxFactsV0 {
1324        variable_decl_names: variable_decl_names.into_iter().collect(),
1325        variable_parameter_names: Vec::new(),
1326        variable_ref_names: variable_ref_names.into_iter().collect(),
1327        mixin_decl_names: mixin_decl_names.into_iter().collect(),
1328        mixin_include_names: mixin_include_names.into_iter().collect(),
1329        function_decl_names: function_decl_names.into_iter().collect(),
1330        function_call_names: function_call_names.into_iter().collect(),
1331        module_use_sources: module_use_sources.into_iter().collect(),
1332        module_use_edges,
1333        module_forward_sources: module_forward_sources.into_iter().collect(),
1334        module_import_sources: module_import_sources.into_iter().collect(),
1335    }
1336}
1337
1338fn summarize_omena_parser_keyframe_facts(facts: &ParsedStyleFacts) -> ParserIndexKeyframesFactsV0 {
1339    let mut names = BTreeSet::new();
1340    let mut animation_ref_names = BTreeSet::new();
1341    for animation in &facts.animations {
1342        match animation.kind {
1343            ParsedAnimationFactKind::KeyframesDeclaration => {
1344                names.insert(animation.name.clone());
1345            }
1346            ParsedAnimationFactKind::AnimationNameReference => {
1347                animation_ref_names.insert(animation.name.clone());
1348            }
1349        }
1350    }
1351    ParserIndexKeyframesFactsV0 {
1352        names: names.into_iter().collect(),
1353        animation_ref_names: animation_ref_names.clone().into_iter().collect(),
1354        animation_name_ref_names: animation_ref_names.into_iter().collect(),
1355        ..ParserIndexKeyframesFactsV0::default()
1356    }
1357}
1358
1359fn summarize_omena_parser_composes_facts(facts: &ParsedStyleFacts) -> ParserIndexComposesFactsV0 {
1360    let mut local_selector_names = BTreeSet::new();
1361    let mut imported_selector_names = BTreeSet::new();
1362    let mut global_selector_names = BTreeSet::new();
1363    let mut import_sources = BTreeSet::new();
1364    for edge in &facts.css_module_composes_edges {
1365        match edge.kind {
1366            ParsedCssModuleComposesEdgeKind::Local => {
1367                local_selector_names.extend(edge.target_names.iter().cloned());
1368            }
1369            ParsedCssModuleComposesEdgeKind::External => {
1370                imported_selector_names.extend(edge.target_names.iter().cloned());
1371                if let Some(source) = &edge.import_source {
1372                    import_sources.insert(source.clone());
1373                }
1374            }
1375            ParsedCssModuleComposesEdgeKind::Global => {
1376                global_selector_names.extend(edge.target_names.iter().cloned());
1377            }
1378        }
1379    }
1380    for composes in &facts.css_module_composes {
1381        if composes.kind == ParsedCssModuleComposesFactKind::ImportSource {
1382            import_sources.insert(composes.name.clone());
1383        }
1384    }
1385    let local_selector_names = local_selector_names.into_iter().collect::<Vec<_>>();
1386    let imported_selector_names = imported_selector_names.into_iter().collect::<Vec<_>>();
1387    let global_selector_names = global_selector_names.into_iter().collect::<Vec<_>>();
1388    ParserIndexComposesFactsV0 {
1389        class_name_count: local_selector_names.len()
1390            + imported_selector_names.len()
1391            + global_selector_names.len(),
1392        local_class_name_count: local_selector_names.len(),
1393        imported_class_name_count: imported_selector_names.len(),
1394        global_class_name_count: global_selector_names.len(),
1395        local_selector_names,
1396        imported_selector_names,
1397        global_selector_names,
1398        import_sources: import_sources.into_iter().collect(),
1399        ..ParserIndexComposesFactsV0::default()
1400    }
1401}
1402
1403fn summarize_omena_parser_custom_property_semantic_facts(
1404    facts: &ParserIndexCustomPropertyFactsV0,
1405) -> StyleCustomPropertySemanticFactsV0 {
1406    let mut resolved_ref_names = BTreeSet::new();
1407    let mut unresolved_ref_names = BTreeSet::new();
1408    for reference in &facts.ref_facts {
1409        if facts
1410            .decl_facts
1411            .iter()
1412            .any(|declaration| custom_property_context_matches(declaration, reference))
1413        {
1414            resolved_ref_names.insert(reference.name.clone());
1415        } else {
1416            unresolved_ref_names.insert(reference.name.clone());
1417        }
1418    }
1419    StyleCustomPropertySemanticFactsV0 {
1420        decl_names: facts.decl_names.clone(),
1421        ref_names: facts.ref_names.clone(),
1422        resolved_ref_names: resolved_ref_names.into_iter().collect(),
1423        unresolved_ref_names: unresolved_ref_names.into_iter().collect(),
1424        selectors_with_refs_names: facts.selectors_with_refs_names.clone(),
1425    }
1426}
1427
1428struct SassSelectorResolution {
1429    resolved_variable_ref_selectors: Vec<String>,
1430    unresolved_variable_ref_selectors: Vec<String>,
1431    resolved_mixin_include_selectors: Vec<String>,
1432    unresolved_mixin_include_selectors: Vec<String>,
1433}
1434
1435fn summarize_omena_parser_sass_selector_resolution(
1436    facts: &ParsedStyleFacts,
1437    resolution: &ParserIndexSassSameFileResolutionFactsV0,
1438    cst: &ParsedCst,
1439) -> SassSelectorResolution {
1440    let resolved_variables = resolution
1441        .resolved_variable_ref_names
1442        .iter()
1443        .cloned()
1444        .collect::<BTreeSet<_>>();
1445    let resolved_mixins = resolution
1446        .resolved_mixin_include_names
1447        .iter()
1448        .cloned()
1449        .collect::<BTreeSet<_>>();
1450    let mut resolved_variable_ref_selectors = BTreeSet::new();
1451    let mut unresolved_variable_ref_selectors = BTreeSet::new();
1452    let mut resolved_mixin_include_selectors = BTreeSet::new();
1453    let mut unresolved_mixin_include_selectors = BTreeSet::new();
1454
1455    for symbol in &facts.sass_symbols {
1456        match symbol.kind {
1457            ParsedSassSymbolFactKind::VariableReference => {
1458                let selector = semantic_selector_name_for_cst_offset(
1459                    cst,
1460                    u32::from(symbol.range.start()) as usize,
1461                );
1462                let Some(selector) = selector else {
1463                    continue;
1464                };
1465                if resolved_variables.contains(&symbol.name) {
1466                    resolved_variable_ref_selectors.insert(selector);
1467                } else {
1468                    unresolved_variable_ref_selectors.insert(selector);
1469                }
1470            }
1471            ParsedSassSymbolFactKind::MixinInclude => {
1472                let selector = semantic_selector_name_for_cst_offset(
1473                    cst,
1474                    u32::from(symbol.range.start()) as usize,
1475                );
1476                let Some(selector) = selector else {
1477                    continue;
1478                };
1479                if resolved_mixins.contains(&symbol.name) {
1480                    resolved_mixin_include_selectors.insert(selector);
1481                } else {
1482                    unresolved_mixin_include_selectors.insert(selector);
1483                }
1484            }
1485            _ => {}
1486        }
1487    }
1488
1489    SassSelectorResolution {
1490        resolved_variable_ref_selectors: resolved_variable_ref_selectors.into_iter().collect(),
1491        unresolved_variable_ref_selectors: unresolved_variable_ref_selectors.into_iter().collect(),
1492        resolved_mixin_include_selectors: resolved_mixin_include_selectors.into_iter().collect(),
1493        unresolved_mixin_include_selectors: unresolved_mixin_include_selectors
1494            .into_iter()
1495            .collect(),
1496    }
1497}
1498
1499fn custom_property_context_matches(
1500    declaration: &ParserIndexCustomPropertyDeclFactV0,
1501    reference: &ParserIndexCustomPropertyRefFactV0,
1502) -> bool {
1503    if declaration.name != reference.name {
1504        return false;
1505    }
1506    if declaration.under_media && !reference.under_media {
1507        return false;
1508    }
1509    if declaration.under_supports && !reference.under_supports {
1510        return false;
1511    }
1512    if declaration.under_layer && !reference.under_layer {
1513        return false;
1514    }
1515    if declaration.selector_contexts.is_empty() {
1516        return true;
1517    }
1518    declaration.selector_contexts.iter().any(|selector| {
1519        !matches!(
1520            selector_context_witness_for_declaration(selector, &reference.selector_contexts)
1521                .verdict,
1522            SelectorMatchVerdict::No
1523        )
1524    })
1525}
1526
1527fn summarize_omena_parser_sass_same_file_resolution(
1528    facts: &ParserSassSyntaxFactsV0,
1529) -> ParserIndexSassSameFileResolutionFactsV0 {
1530    let variable_targets = facts
1531        .variable_decl_names
1532        .iter()
1533        .chain(facts.variable_parameter_names.iter())
1534        .cloned()
1535        .collect::<BTreeSet<_>>();
1536    let mixin_targets = facts
1537        .mixin_decl_names
1538        .iter()
1539        .cloned()
1540        .collect::<BTreeSet<_>>();
1541    let function_targets = facts
1542        .function_decl_names
1543        .iter()
1544        .cloned()
1545        .collect::<BTreeSet<_>>();
1546
1547    ParserIndexSassSameFileResolutionFactsV0 {
1548        resolved_variable_ref_names: names_matching(&facts.variable_ref_names, &variable_targets),
1549        unresolved_variable_ref_names: names_not_matching(
1550            &facts.variable_ref_names,
1551            &variable_targets,
1552        ),
1553        resolved_mixin_include_names: names_matching(&facts.mixin_include_names, &mixin_targets),
1554        unresolved_mixin_include_names: names_not_matching(
1555            &facts.mixin_include_names,
1556            &mixin_targets,
1557        ),
1558        resolved_function_call_names: names_matching(&facts.function_call_names, &function_targets),
1559    }
1560}
1561
1562fn names_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1563    names
1564        .iter()
1565        .filter(|name| targets.contains(*name))
1566        .cloned()
1567        .collect()
1568}
1569
1570fn names_not_matching(names: &[String], targets: &BTreeSet<String>) -> Vec<String> {
1571    names
1572        .iter()
1573        .filter(|name| !targets.contains(*name))
1574        .cloned()
1575        .collect()
1576}
1577
1578fn bem_suffix_parent_name(name: &str) -> Option<String> {
1579    let marker = name.find("__").or_else(|| name.find("--"))?;
1580    (marker > 0).then(|| name[..marker].to_string())
1581}
1582
1583fn selector_has_parent_ampersand_class_prefix(source: &str, selector_start: usize) -> bool {
1584    let bytes = source.as_bytes();
1585    if selector_start >= bytes.len() {
1586        return false;
1587    }
1588    let dot_index = if bytes[selector_start] == b'.' {
1589        selector_start
1590    } else {
1591        match previous_non_whitespace_byte_index(bytes, selector_start) {
1592            Some(index) if bytes[index] == b'.' => index,
1593            _ => return false,
1594        }
1595    };
1596    matches!(
1597        previous_non_whitespace_byte_index(bytes, dot_index),
1598        Some(index) if bytes[index] == b'&'
1599    )
1600}
1601
1602#[derive(Debug, Clone, Default)]
1603struct StyleOffsetContext {
1604    selector_contexts: Vec<String>,
1605    under_media: bool,
1606    under_supports: bool,
1607    under_layer: bool,
1608    layer_names: Vec<String>,
1609    condition_context: Vec<String>,
1610}
1611
1612fn style_context_for_cst_offset(
1613    source: &str,
1614    cst: &ParsedCst,
1615    byte_offset: usize,
1616) -> StyleOffsetContext {
1617    let mut context = StyleOffsetContext::default();
1618    for node in cst
1619        .root()
1620        .descendants()
1621        .filter(|node| cst_node_contains_byte_offset(node, byte_offset))
1622    {
1623        match node.kind() {
1624            SyntaxKind::Rule => {
1625                if let Some(selector) = rule_selector_text_from_cst_node(node) {
1626                    context.selector_contexts.push(selector);
1627                }
1628            }
1629            SyntaxKind::MediaRule => {
1630                context.under_media = true;
1631                if let Some(header) = cst_at_rule_header_text(source, node) {
1632                    context.condition_context.push(header);
1633                }
1634            }
1635            SyntaxKind::SupportsRule => {
1636                context.under_supports = true;
1637                if let Some(header) = cst_at_rule_header_text(source, node) {
1638                    context.condition_context.push(header);
1639                }
1640            }
1641            SyntaxKind::LayerRule => {
1642                if cst_node_has_block(node) {
1643                    context.under_layer = true;
1644                    context
1645                        .layer_names
1646                        .extend(split_layer_names(&cst_context_prelude(node)));
1647                }
1648            }
1649            kind if cst_non_layer_condition_kind(kind) => {
1650                if let Some(header) = cst_at_rule_header_text(source, node)
1651                    && css_keyword(&header).strip_prefix("@layer").is_none()
1652                {
1653                    context.condition_context.push(header);
1654                }
1655            }
1656            _ => {}
1657        }
1658    }
1659    context
1660}
1661
1662fn declaration_value_text(source: &str, offset: usize) -> String {
1663    let span = declaration_statement_byte_span_for_offset(source, offset);
1664    let Some(statement) = source.get(span.start..span.end) else {
1665        return String::new();
1666    };
1667    let Some(colon) = statement.find(':') else {
1668        return String::new();
1669    };
1670    statement[colon + 1..]
1671        .trim()
1672        .trim_end_matches(';')
1673        .trim()
1674        .to_string()
1675}
1676
1677fn declaration_statement_byte_span_for_offset(source: &str, offset: usize) -> ParserByteSpanV0 {
1678    let start = source
1679        .get(..offset)
1680        .and_then(|before| before.rfind(['{', ';']).map(|index| index + 1))
1681        .unwrap_or(offset);
1682    let end = source
1683        .get(offset..)
1684        .and_then(|rest| {
1685            let semicolon = rest.find(';');
1686            let close = rest.find('}');
1687            match (semicolon, close) {
1688                (Some(semicolon), Some(close)) => Some(offset + semicolon.min(close)),
1689                (Some(semicolon), None) => Some(offset + semicolon + 1),
1690                (None, Some(close)) => Some(offset + close),
1691                (None, None) => None,
1692            }
1693        })
1694        .unwrap_or(source.len());
1695    ParserByteSpanV0 { start, end }
1696}
1697
1698fn semantic_selector_name_for_cst_offset(cst: &ParsedCst, byte_offset: usize) -> Option<String> {
1699    cst.root()
1700        .descendants()
1701        .filter(|node| {
1702            node.kind() == SyntaxKind::Rule && cst_node_contains_byte_offset(node, byte_offset)
1703        })
1704        .filter_map(last_class_selector_name_from_rule_node)
1705        .last()
1706}
1707
1708fn cst_node_contains_byte_offset(node: &SyntaxNode, byte_offset: usize) -> bool {
1709    let range = node.text_range();
1710    let start = u32::from(range.start()) as usize;
1711    let end = u32::from(range.end()) as usize;
1712    start <= byte_offset && byte_offset < end
1713}
1714
1715fn cst_non_layer_condition_kind(kind: SyntaxKind) -> bool {
1716    matches!(
1717        kind,
1718        SyntaxKind::ContainerRule
1719            | SyntaxKind::ScopeRule
1720            | SyntaxKind::AtRule
1721            | SyntaxKind::KeyframesRule
1722            | SyntaxKind::FontFaceRule
1723            | SyntaxKind::PageRule
1724            | SyntaxKind::StartingStyleRule
1725            | SyntaxKind::PageMarginRule
1726            | SyntaxKind::CounterStyleRule
1727            | SyntaxKind::FontPaletteValuesRule
1728            | SyntaxKind::ColorProfileRule
1729            | SyntaxKind::PositionTryRule
1730            | SyntaxKind::FontFeatureValuesRule
1731            | SyntaxKind::FontFeatureValuesStylisticRule
1732            | SyntaxKind::FontFeatureValuesStylesetRule
1733            | SyntaxKind::FontFeatureValuesCharacterVariantRule
1734            | SyntaxKind::FontFeatureValuesSwashRule
1735            | SyntaxKind::FontFeatureValuesOrnamentsRule
1736            | SyntaxKind::FontFeatureValuesAnnotationRule
1737            | SyntaxKind::FontFeatureValuesHistoricalFormsRule
1738            | SyntaxKind::ViewTransitionRule
1739            | SyntaxKind::WhenRule
1740            | SyntaxKind::ElseRule
1741            | SyntaxKind::IfRule
1742    )
1743}
1744
1745fn cst_at_rule_header_text(source: &str, node: &SyntaxNode) -> Option<String> {
1746    let start = u32::from(node.text_range().start()) as usize;
1747    let end = cst_node_block_open_start(node)?;
1748    source
1749        .get(start..end)
1750        .map(normalized_condition_header)
1751        .filter(|header| !header.is_empty())
1752}
1753
1754fn cst_node_block_open_start(node: &SyntaxNode) -> Option<usize> {
1755    node.descendants_with_tokens()
1756        .filter_map(|element| element.into_token())
1757        .find(|token| matches!(token.kind(), SyntaxKind::LeftBrace | SyntaxKind::SassIndent))
1758        .map(|token| u32::from(token.text_range().start()) as usize)
1759}
1760
1761fn rule_selector_text_from_cst_node(node: &SyntaxNode) -> Option<String> {
1762    let mut selector = String::new();
1763    for child in node.children() {
1764        if matches!(
1765            child.kind(),
1766            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
1767        ) {
1768            break;
1769        }
1770        selector.push_str(&syntax_node_text(child));
1771    }
1772    let selector = selector.trim();
1773    (!selector.is_empty()).then(|| selector.to_string())
1774}
1775
1776fn last_class_selector_name_from_rule_node(node: &SyntaxNode) -> Option<String> {
1777    let mut last = None;
1778    for child in node.children() {
1779        if matches!(
1780            child.kind(),
1781            SyntaxKind::DeclarationList | SyntaxKind::RuleList | SyntaxKind::SassIndentedBlock
1782        ) {
1783            break;
1784        }
1785        for class_node in child
1786            .descendants()
1787            .filter(|node| node.kind() == SyntaxKind::ClassSelector)
1788        {
1789            last = class_selector_name_from_cst_node(class_node);
1790        }
1791    }
1792    last
1793}
1794
1795fn normalized_condition_header(header: &str) -> String {
1796    header.split_whitespace().collect::<Vec<_>>().join(" ")
1797}
1798
1799fn previous_non_whitespace_byte_index(bytes: &[u8], before: usize) -> Option<usize> {
1800    let mut index = before.checked_sub(1)?;
1801    loop {
1802        if !bytes[index].is_ascii_whitespace() {
1803            return Some(index);
1804        }
1805        index = index.checked_sub(1)?;
1806    }
1807}
1808
1809fn parser_byte_span_for_offsets(start: usize, end: usize) -> ParserByteSpanV0 {
1810    ParserByteSpanV0 { start, end }
1811}
1812
1813fn parser_range_for_byte_span(source: &str, span: ParserByteSpanV0) -> ParserRangeV0 {
1814    ParserRangeV0 {
1815        start: parser_position_for_byte_offset(source, span.start),
1816        end: parser_position_for_byte_offset(source, span.end),
1817    }
1818}
1819
1820fn parser_position_for_byte_offset(source: &str, byte_offset: usize) -> ParserPositionV0 {
1821    // LSP positions count UTF-16 code units per line, not raw bytes. Walk chars
1822    // and accumulate `len_utf16()` so non-ASCII source produces correct columns,
1823    // matching the canonical helpers in omena-query/src/style.rs and
1824    // omena-lsp-server/src/protocol.rs (previously this used a raw byte offset,
1825    // which diverged on multi-byte characters).
1826    let clamped_offset = byte_offset.min(source.len());
1827    let mut line = 0usize;
1828    let mut character = 0usize;
1829
1830    for (index, ch) in source.char_indices() {
1831        if index >= clamped_offset {
1832            break;
1833        }
1834        if ch == '\n' {
1835            line += 1;
1836            character = 0;
1837        } else {
1838            character += ch.len_utf16();
1839        }
1840    }
1841
1842    ParserPositionV0 { line, character }
1843}
1844
1845fn dialect_for_style_path(style_path: &str) -> Option<StyleDialect> {
1846    if style_path.ends_with(".sass") {
1847        Some(StyleDialect::Sass)
1848    } else if style_path.ends_with(".scss") {
1849        Some(StyleDialect::Scss)
1850    } else if style_path.ends_with(".less") {
1851        Some(StyleDialect::Less)
1852    } else if style_path.ends_with(".css") {
1853        Some(StyleDialect::Css)
1854    } else {
1855        None
1856    }
1857}
1858
1859fn omena_parser_dialect_for_style_path(style_path: &str) -> StyleDialect {
1860    dialect_for_style_path(style_path).unwrap_or(StyleDialect::Css)
1861}
1862
1863fn omena_parser_dialect_label(dialect: StyleDialect) -> &'static str {
1864    match dialect {
1865        StyleDialect::Css => "css",
1866        StyleDialect::Scss => "scss",
1867        StyleDialect::Sass => "sass",
1868        StyleDialect::Less => "less",
1869    }
1870}
1871
1872#[cfg(test)]
1873mod tests;