1use 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 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;