1use omena_cascade::{
8 CascadeKey, CascadeLevel, LayerRank, ModuleRank, SelectorMatchVerdict, Specificity,
9 select_cascade_winner, selector_context_witness, selector_context_witness_for_declaration,
10};
11use omena_syntax::css_keyword;
12use serde::Serialize;
13use std::collections::{BTreeMap, BTreeSet};
14
15use crate::{
16 ParserBoundarySyntaxFactsV0, ParserByteSpanV0, ParserIndexCustomPropertyDeclFactV0,
17 ParserIndexCustomPropertyRefFactV0, ParserRangeV0, StyleContextIndexV0, StyleSemanticFactsV0,
18};
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
21#[serde(rename_all = "camelCase")]
22pub struct DesignTokenSemanticSummaryV0 {
23 pub schema_version: &'static str,
24 pub product: &'static str,
25 pub status: &'static str,
26 pub resolution_scope: &'static str,
27 pub declaration_count: usize,
28 pub reference_count: usize,
29 pub resolved_reference_count: usize,
30 pub unresolved_reference_count: usize,
31 pub selectors_with_references_count: usize,
32 pub context_signal: DesignTokenContextSignalV0,
33 pub resolution_signal: DesignTokenResolutionSignalV0,
34 pub cascade_ranking_signal: DesignTokenCascadeRankingSignalV0,
35 pub declaration_candidates: Vec<DesignTokenDeclarationCandidateV0>,
36 pub capabilities: DesignTokenSemanticCapabilitiesV0,
37 pub blocking_gaps: Vec<&'static str>,
38 pub next_priorities: Vec<&'static str>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
42#[serde(rename_all = "camelCase")]
43pub struct DesignTokenContextSignalV0 {
44 pub declaration_context_selector_count: usize,
45 pub declaration_wrapper_context_count: usize,
46 pub media_context_selector_count: usize,
47 pub supports_context_selector_count: usize,
48 pub layer_context_selector_count: usize,
49 pub wrapper_context_count: usize,
50}
51
52#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
53#[serde(rename_all = "camelCase")]
54pub struct DesignTokenResolutionSignalV0 {
55 pub declaration_fact_count: usize,
56 pub reference_fact_count: usize,
57 pub source_ordered_declaration_count: usize,
58 pub source_ordered_reference_count: usize,
59 pub occurrence_resolved_reference_count: usize,
60 pub occurrence_unresolved_reference_count: usize,
61 pub workspace_declaration_fact_count: usize,
62 pub cross_file_declaration_fact_count: usize,
63 pub workspace_occurrence_resolved_reference_count: usize,
64 pub workspace_occurrence_unresolved_reference_count: usize,
65 pub context_matched_reference_count: usize,
66 pub context_unmatched_reference_count: usize,
67 pub root_declaration_count: usize,
68 pub selector_scoped_declaration_count: usize,
69 pub wrapper_scoped_declaration_count: usize,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
73#[serde(rename_all = "camelCase")]
74pub struct DesignTokenCascadeRankingSignalV0 {
75 pub ranked_reference_count: usize,
76 pub unranked_reference_count: usize,
77 pub source_order_winner_declaration_count: usize,
78 pub source_order_shadowed_declaration_count: usize,
79 pub repeated_name_declaration_count: usize,
80 pub theme_context_winner_reference_count: usize,
81 pub cross_file_candidate_declaration_count: usize,
82 pub cross_file_winner_declaration_count: usize,
83 pub cross_file_shadowed_declaration_count: usize,
84 pub ranked_references: Vec<DesignTokenRankedReferenceV0>,
85}
86
87#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
88#[serde(rename_all = "camelCase")]
89pub struct DesignTokenRankedReferenceV0 {
90 pub reference_name: String,
91 pub reference_source_order: usize,
92 pub winner_declaration_source_order: usize,
93 #[serde(skip_serializing_if = "Option::is_none")]
94 pub winner_declaration_file_path: Option<String>,
95 #[serde(skip_serializing_if = "Option::is_none")]
96 pub winner_declaration_range: Option<ParserRangeV0>,
97 #[serde(skip_serializing_if = "Option::is_none")]
98 pub winner_import_graph_distance: Option<usize>,
99 #[serde(skip_serializing_if = "Option::is_none")]
100 pub winner_import_graph_order: Option<usize>,
101 pub winner_declaration_layer_rank: i32,
102 pub winner_scope_proximity_status: &'static str,
103 #[serde(skip_serializing_if = "Option::is_none")]
104 pub winner_declaration_layer_name: Option<String>,
105 pub shadowed_declaration_source_orders: Vec<usize>,
106 pub candidate_declaration_count: usize,
107 pub winner_context_kind: &'static str,
108 pub cross_file_candidate_declaration_count: usize,
109 pub cross_file_shadowed_declaration_count: usize,
110}
111
112#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
113#[serde(rename_all = "camelCase")]
114pub struct DesignTokenSemanticCapabilitiesV0 {
115 pub same_file_resolution_ready: bool,
116 pub wrapper_context_signal_ready: bool,
117 pub source_order_signal_ready: bool,
118 pub source_order_cascade_ranking_ready: bool,
119 pub workspace_cascade_candidate_signal_ready: bool,
120 pub occurrence_resolution_signal_ready: bool,
121 pub selector_context_resolution_ready: bool,
122 pub theme_override_context_signal_ready: bool,
123 pub cross_file_import_graph_ready: bool,
124 pub cross_package_cascade_ranking_ready: bool,
125 pub theme_override_context_ready: bool,
126}
127
128#[derive(Debug, Clone, PartialEq, Eq)]
129pub struct DesignTokenWorkspaceDeclarationFactV0 {
130 pub file_path: String,
131 pub name: String,
132 pub value: String,
133 pub source_order: usize,
134 pub import_graph_distance: Option<usize>,
135 pub import_graph_order: Option<usize>,
136 pub byte_span: ParserByteSpanV0,
137 pub range: ParserRangeV0,
138 pub selector_contexts: Vec<String>,
139 pub condition_context: Vec<String>,
140 pub layer_names: Vec<String>,
141 pub under_media: bool,
142 pub under_supports: bool,
143 pub under_layer: bool,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
147#[serde(rename_all = "camelCase")]
148pub struct DesignTokenDeclarationCandidateV0 {
149 pub name: String,
150 pub value: String,
151 pub source_order: usize,
152 pub file_path: String,
153 pub range: ParserRangeV0,
154 pub selector_contexts: Vec<String>,
155 #[serde(default, skip_serializing_if = "Vec::is_empty")]
156 pub condition_context: Vec<String>,
157 pub layer_names: Vec<String>,
158 pub under_media: bool,
159 pub under_supports: bool,
160 pub under_layer: bool,
161 pub candidate_scope: &'static str,
162 #[serde(skip_serializing_if = "Option::is_none")]
163 pub import_graph_distance: Option<usize>,
164 #[serde(skip_serializing_if = "Option::is_none")]
165 pub import_graph_order: Option<usize>,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum DesignTokenExternalDeclarationCandidateScopeV0 {
170 Workspace,
171 CrossFileImportGraph,
172}
173
174pub fn summarize_design_token_semantics(
175 parser_facts: &ParserBoundarySyntaxFactsV0,
176 semantic_facts: &StyleSemanticFactsV0,
177) -> DesignTokenSemanticSummaryV0 {
178 summarize_design_token_semantics_with_workspace_declarations(
179 parser_facts,
180 semantic_facts,
181 None,
182 &[],
183 )
184}
185
186pub fn summarize_design_token_semantics_with_workspace_declarations(
187 parser_facts: &ParserBoundarySyntaxFactsV0,
188 semantic_facts: &StyleSemanticFactsV0,
189 target_style_path: Option<&str>,
190 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
191) -> DesignTokenSemanticSummaryV0 {
192 summarize_design_token_semantics_with_scoped_workspace_declarations(
193 parser_facts,
194 semantic_facts,
195 target_style_path,
196 workspace_declarations,
197 DesignTokenExternalDeclarationCandidateScopeV0::Workspace,
198 )
199}
200
201pub fn summarize_design_token_semantics_with_scoped_workspace_declarations(
202 parser_facts: &ParserBoundarySyntaxFactsV0,
203 semantic_facts: &StyleSemanticFactsV0,
204 target_style_path: Option<&str>,
205 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
206 candidate_scope: DesignTokenExternalDeclarationCandidateScopeV0,
207) -> DesignTokenSemanticSummaryV0 {
208 let media_context_selector_count = parser_facts
209 .custom_properties
210 .selectors_with_refs_under_media_names
211 .len();
212 let supports_context_selector_count = parser_facts
213 .custom_properties
214 .selectors_with_refs_under_supports_names
215 .len();
216 let layer_context_selector_count = parser_facts
217 .custom_properties
218 .selectors_with_refs_under_layer_names
219 .len();
220 let declaration_wrapper_context_count =
221 parser_facts.custom_properties.decl_names_under_media.len()
222 + parser_facts
223 .custom_properties
224 .decl_names_under_supports
225 .len()
226 + parser_facts.custom_properties.decl_names_under_layer.len();
227 let wrapper_context_count = media_context_selector_count
228 + supports_context_selector_count
229 + layer_context_selector_count;
230 let declaration_context_selector_count =
231 parser_facts.custom_properties.decl_context_selectors.len();
232 let reference_count = semantic_facts.custom_properties.ref_names.len();
233 let declaration_count = semantic_facts.custom_properties.decl_names.len();
234 let resolution_signal = summarize_design_token_resolution_signal(
235 parser_facts,
236 target_style_path,
237 workspace_declarations,
238 );
239 let cascade_ranking_signal = summarize_design_token_cascade_ranking_signal(
240 parser_facts,
241 semantic_facts,
242 target_style_path,
243 workspace_declarations,
244 );
245
246 let external_candidate_scope_ready = candidate_scope.cross_file_import_graph_ready();
247 let status = if reference_count == 0 && declaration_count == 0 {
248 "empty"
249 } else if cascade_ranking_signal.has_workspace_signal() && external_candidate_scope_ready {
250 "cross-file-import-cascade-ranking-seed"
251 } else if cascade_ranking_signal.has_workspace_signal() {
252 "workspace-cascade-ranking-seed"
253 } else if cascade_ranking_signal.has_shadowing_signal() {
254 "same-file-cascade-ranking-seed"
255 } else if resolution_signal.occurrence_resolution_ready() {
256 "context-aware-resolution-seed"
257 } else if wrapper_context_count > 0 {
258 "context-aware-seed"
259 } else {
260 "same-file-seed"
261 };
262
263 let mut blocking_gaps = Vec::new();
264 if reference_count > 0 || declaration_count > 0 {
265 if !external_candidate_scope_ready {
266 blocking_gaps.push("crossFileImportGraph");
267 }
268 blocking_gaps.push("crossPackageCascadeRanking");
269 if !cascade_ranking_signal.theme_override_context_ready() {
270 blocking_gaps.push("themeOverrideContext");
271 }
272 }
273 if !semantic_facts
274 .custom_properties
275 .unresolved_ref_names
276 .is_empty()
277 {
278 blocking_gaps.push("unresolvedDesignTokenRefs");
279 }
280
281 let next_priorities = if reference_count == 0 && declaration_count == 0 {
282 vec!["designTokenSeed"]
283 } else {
284 let mut priorities = Vec::new();
285 if !external_candidate_scope_ready {
286 priorities.push("crossFileImportGraph");
287 }
288 priorities.push("crossPackageCascadeRanking");
289 if !cascade_ranking_signal.theme_override_context_ready() {
290 priorities.push("themeOverrideContext");
291 }
292 priorities
293 };
294 let resolution_scope = if cascade_ranking_signal.has_workspace_signal() {
295 candidate_scope.resolution_scope()
296 } else {
297 "same-file"
298 };
299 let declaration_candidates = summarize_design_token_declaration_candidates(
300 parser_facts,
301 target_style_path,
302 workspace_declarations,
303 candidate_scope,
304 );
305
306 DesignTokenSemanticSummaryV0 {
307 schema_version: "0",
308 product: "omena-semantic.design-token-semantics",
309 status,
310 resolution_scope,
311 declaration_count,
312 reference_count,
313 resolved_reference_count: semantic_facts.custom_properties.resolved_ref_names.len(),
314 unresolved_reference_count: semantic_facts.custom_properties.unresolved_ref_names.len(),
315 selectors_with_references_count: semantic_facts
316 .custom_properties
317 .selectors_with_refs_names
318 .len(),
319 context_signal: DesignTokenContextSignalV0 {
320 declaration_context_selector_count,
321 declaration_wrapper_context_count,
322 media_context_selector_count,
323 supports_context_selector_count,
324 layer_context_selector_count,
325 wrapper_context_count,
326 },
327 resolution_signal: resolution_signal.clone(),
328 cascade_ranking_signal: cascade_ranking_signal.clone(),
329 declaration_candidates,
330 capabilities: DesignTokenSemanticCapabilitiesV0 {
331 same_file_resolution_ready: declaration_count > 0 || reference_count > 0,
332 wrapper_context_signal_ready: wrapper_context_count > 0,
333 source_order_signal_ready: resolution_signal.source_order_signal_ready(),
334 source_order_cascade_ranking_ready: cascade_ranking_signal
335 .source_order_cascade_ranking_ready(),
336 workspace_cascade_candidate_signal_ready: cascade_ranking_signal.has_workspace_signal(),
337 occurrence_resolution_signal_ready: resolution_signal.occurrence_resolution_ready(),
338 selector_context_resolution_ready: resolution_signal
339 .selector_context_resolution_ready(),
340 theme_override_context_signal_ready: declaration_context_selector_count > 0
341 || declaration_wrapper_context_count > 0,
342 cross_file_import_graph_ready: external_candidate_scope_ready,
343 cross_package_cascade_ranking_ready: false,
344 theme_override_context_ready: cascade_ranking_signal.theme_override_context_ready(),
345 },
346 blocking_gaps,
347 next_priorities,
348 }
349}
350
351fn summarize_design_token_declaration_candidates(
352 parser_facts: &ParserBoundarySyntaxFactsV0,
353 target_style_path: Option<&str>,
354 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
355 candidate_scope: DesignTokenExternalDeclarationCandidateScopeV0,
356) -> Vec<DesignTokenDeclarationCandidateV0> {
357 let mut candidates = Vec::new();
358 if let Some(file_path) = target_style_path {
359 candidates.extend(
360 parser_facts
361 .custom_properties
362 .decl_facts
363 .iter()
364 .map(|declaration| DesignTokenDeclarationCandidateV0 {
365 name: declaration.name.clone(),
366 value: declaration.value.clone(),
367 source_order: declaration.source_order,
368 file_path: file_path.to_string(),
369 range: declaration.range,
370 selector_contexts: declaration.selector_contexts.clone(),
371 condition_context: declaration.condition_context.clone(),
372 layer_names: declaration.layer_names.clone(),
373 under_media: declaration.under_media,
374 under_supports: declaration.under_supports,
375 under_layer: declaration.under_layer,
376 candidate_scope: "same-file",
377 import_graph_distance: None,
378 import_graph_order: None,
379 }),
380 );
381 }
382 candidates.extend(workspace_declarations.iter().map(|declaration| {
383 DesignTokenDeclarationCandidateV0 {
384 name: declaration.name.clone(),
385 value: declaration.value.clone(),
386 source_order: declaration.source_order,
387 file_path: declaration.file_path.clone(),
388 range: declaration.range,
389 selector_contexts: declaration.selector_contexts.clone(),
390 condition_context: declaration.condition_context.clone(),
391 layer_names: declaration.layer_names.clone(),
392 under_media: declaration.under_media,
393 under_supports: declaration.under_supports,
394 under_layer: declaration.under_layer,
395 candidate_scope: candidate_scope.resolution_scope(),
396 import_graph_distance: declaration.import_graph_distance,
397 import_graph_order: declaration.import_graph_order,
398 }
399 }));
400 candidates.sort_by(|left, right| {
401 left.file_path
402 .cmp(&right.file_path)
403 .then_with(|| left.source_order.cmp(&right.source_order))
404 .then_with(|| left.name.cmp(&right.name))
405 });
406 candidates.dedup_by(|left, right| {
407 left.file_path == right.file_path
408 && left.source_order == right.source_order
409 && left.name == right.name
410 && left.range == right.range
411 });
412 candidates
413}
414
415pub fn collect_design_token_workspace_declarations(
416 style_path: &str,
417 parser_facts: &ParserBoundarySyntaxFactsV0,
418) -> Vec<DesignTokenWorkspaceDeclarationFactV0> {
419 parser_facts
420 .custom_properties
421 .decl_facts
422 .iter()
423 .map(|declaration| DesignTokenWorkspaceDeclarationFactV0 {
424 file_path: style_path.to_string(),
425 name: declaration.name.clone(),
426 value: declaration.value.clone(),
427 source_order: declaration.source_order,
428 import_graph_distance: None,
429 import_graph_order: None,
430 byte_span: declaration.byte_span,
431 range: declaration.range,
432 selector_contexts: declaration.selector_contexts.clone(),
433 condition_context: declaration.condition_context.clone(),
434 layer_names: declaration.layer_names.clone(),
435 under_media: declaration.under_media,
436 under_supports: declaration.under_supports,
437 under_layer: declaration.under_layer,
438 })
439 .collect()
440}
441
442fn summarize_design_token_cascade_ranking_signal(
443 parser_facts: &ParserBoundarySyntaxFactsV0,
444 semantic_facts: &StyleSemanticFactsV0,
445 target_style_path: Option<&str>,
446 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
447) -> DesignTokenCascadeRankingSignalV0 {
448 let custom_properties = &parser_facts.custom_properties;
449 let cascade_context =
450 DesignTokenCascadeContext::from_style_context_index(&semantic_facts.context_index);
451 let mut declaration_name_counts = BTreeMap::<&str, usize>::new();
452 let mut winner_declarations = BTreeSet::<(String, usize)>::new();
453 let mut shadowed_declarations = BTreeSet::<(String, usize)>::new();
454 let mut ranked_reference_count = 0;
455 let mut unranked_reference_count = 0;
456 let mut cross_file_candidate_declaration_count = 0;
457 let mut cross_file_winner_declaration_count = 0;
458 let mut cross_file_shadowed_declaration_count = 0;
459 let mut theme_context_winner_reference_count = 0;
460 let mut ranked_references = Vec::new();
461
462 for declaration in &custom_properties.decl_facts {
463 *declaration_name_counts
464 .entry(declaration.name.as_str())
465 .or_insert(0) += 1;
466 }
467
468 for reference in &custom_properties.ref_facts {
469 let local_candidates = custom_properties
470 .decl_facts
471 .iter()
472 .filter(|declaration| custom_property_context_matches(declaration, reference))
473 .collect::<Vec<_>>();
474 let workspace_candidates = workspace_declarations
475 .iter()
476 .filter(|declaration| {
477 target_style_path.is_none_or(|target| declaration.file_path != target)
478 && custom_property_workspace_context_matches(declaration, reference)
479 })
480 .collect::<Vec<_>>();
481
482 let local_winner = select_cascade_winner(
483 local_candidates
484 .iter()
485 .copied()
486 .map(DesignTokenCandidateDeclaration::Local),
487 |candidate| candidate.cascade_key(reference, None, &cascade_context),
488 )
489 .map(|(winner, _)| winner);
490 let workspace_file_ranks = summarize_workspace_candidate_file_ranks(&workspace_candidates);
491 let workspace_winner = select_cascade_winner(
492 workspace_candidates
493 .iter()
494 .copied()
495 .map(DesignTokenCandidateDeclaration::Workspace),
496 |candidate| {
497 candidate.cascade_key(reference, Some(&workspace_file_ranks), &cascade_context)
498 },
499 )
500 .map(|(winner, _)| winner);
501 let winner = local_winner.or(workspace_winner);
502
503 let Some(winner) = winner else {
504 unranked_reference_count += 1;
505 continue;
506 };
507
508 ranked_reference_count += 1;
509 let candidate_declaration_count = local_candidates.len() + workspace_candidates.len();
510 let reference_cross_file_candidate_declaration_count = workspace_candidates.len();
511 cross_file_candidate_declaration_count += reference_cross_file_candidate_declaration_count;
512 let mut shadowed_declaration_source_orders = Vec::new();
513 for candidate in local_candidates {
514 if winner.is_local_source_order(candidate.source_order) {
515 winner_declarations.insert(custom_property_declaration_key(candidate));
516 } else {
517 shadowed_declaration_source_orders.push(candidate.source_order);
518 shadowed_declarations.insert(custom_property_declaration_key(candidate));
519 }
520 }
521 let reference_cross_file_shadowed_declaration_count = workspace_candidates
522 .iter()
523 .filter(|candidate| !winner.is_workspace(candidate))
524 .count();
525 cross_file_shadowed_declaration_count += reference_cross_file_shadowed_declaration_count;
526 if winner.is_workspace_winner() {
527 cross_file_winner_declaration_count += 1;
528 }
529 if winner.is_theme_context_winner(reference) {
530 theme_context_winner_reference_count += 1;
531 }
532 shadowed_declaration_source_orders.sort_unstable();
533 ranked_references.push(DesignTokenRankedReferenceV0 {
534 reference_name: reference.name.clone(),
535 reference_source_order: reference.source_order,
536 winner_declaration_source_order: winner.source_order(),
537 winner_declaration_file_path: winner.file_path().map(ToString::to_string),
538 winner_declaration_range: winner.range(),
539 winner_import_graph_distance: winner.import_graph_distance(),
540 winner_import_graph_order: winner.import_graph_order(),
541 winner_declaration_layer_rank: winner.layer_rank(&cascade_context).0,
542 winner_scope_proximity_status: "legacySelectorContextFallback",
543 winner_declaration_layer_name: winner.layer_name(&cascade_context),
544 shadowed_declaration_source_orders,
545 candidate_declaration_count,
546 winner_context_kind: winner.context_kind(reference),
547 cross_file_candidate_declaration_count:
548 reference_cross_file_candidate_declaration_count,
549 cross_file_shadowed_declaration_count: reference_cross_file_shadowed_declaration_count,
550 });
551 }
552
553 DesignTokenCascadeRankingSignalV0 {
554 ranked_reference_count,
555 unranked_reference_count,
556 source_order_winner_declaration_count: winner_declarations.len(),
557 source_order_shadowed_declaration_count: shadowed_declarations.len(),
558 repeated_name_declaration_count: custom_properties
559 .decl_facts
560 .iter()
561 .filter(|declaration| {
562 declaration_name_counts
563 .get(declaration.name.as_str())
564 .is_some_and(|count| *count > 1)
565 })
566 .count(),
567 theme_context_winner_reference_count,
568 cross_file_candidate_declaration_count,
569 cross_file_winner_declaration_count,
570 cross_file_shadowed_declaration_count,
571 ranked_references,
572 }
573}
574
575fn summarize_design_token_resolution_signal(
576 parser_facts: &ParserBoundarySyntaxFactsV0,
577 target_style_path: Option<&str>,
578 workspace_declarations: &[DesignTokenWorkspaceDeclarationFactV0],
579) -> DesignTokenResolutionSignalV0 {
580 let custom_properties = &parser_facts.custom_properties;
581 let mut occurrence_resolved_reference_count = 0;
582 let mut occurrence_unresolved_reference_count = 0;
583 let mut workspace_occurrence_resolved_reference_count = 0;
584 let mut workspace_occurrence_unresolved_reference_count = 0;
585 let cross_file_declaration_fact_count = workspace_declarations
586 .iter()
587 .filter(|declaration| {
588 target_style_path.is_none_or(|target| declaration.file_path != target)
589 })
590 .count();
591
592 for reference in &custom_properties.ref_facts {
593 let has_same_file_match = custom_properties
594 .decl_facts
595 .iter()
596 .any(|declaration| custom_property_context_matches(declaration, reference));
597 let has_workspace_match = has_same_file_match
598 || workspace_declarations.iter().any(|declaration| {
599 target_style_path.is_none_or(|target| declaration.file_path != target)
600 && custom_property_workspace_context_matches(declaration, reference)
601 });
602
603 if has_same_file_match {
604 occurrence_resolved_reference_count += 1;
605 } else {
606 occurrence_unresolved_reference_count += 1;
607 }
608 if has_workspace_match {
609 workspace_occurrence_resolved_reference_count += 1;
610 } else {
611 workspace_occurrence_unresolved_reference_count += 1;
612 }
613 }
614
615 DesignTokenResolutionSignalV0 {
616 declaration_fact_count: custom_properties.decl_facts.len(),
617 reference_fact_count: custom_properties.ref_facts.len(),
618 source_ordered_declaration_count: custom_properties.decl_facts.len(),
619 source_ordered_reference_count: custom_properties.ref_facts.len(),
620 occurrence_resolved_reference_count,
621 occurrence_unresolved_reference_count,
622 workspace_declaration_fact_count: custom_properties.decl_facts.len()
623 + cross_file_declaration_fact_count,
624 cross_file_declaration_fact_count,
625 workspace_occurrence_resolved_reference_count,
626 workspace_occurrence_unresolved_reference_count,
627 context_matched_reference_count: occurrence_resolved_reference_count,
628 context_unmatched_reference_count: occurrence_unresolved_reference_count,
629 root_declaration_count: custom_properties
630 .decl_facts
631 .iter()
632 .filter(|declaration| {
633 declaration
634 .selector_contexts
635 .iter()
636 .any(|selector| css_keyword(selector).equals(":root"))
637 })
638 .count(),
639 selector_scoped_declaration_count: custom_properties
640 .decl_facts
641 .iter()
642 .filter(|declaration| {
643 declaration
644 .selector_contexts
645 .iter()
646 .any(|selector| !css_keyword(selector).equals(":root"))
647 })
648 .count(),
649 wrapper_scoped_declaration_count: custom_properties
650 .decl_facts
651 .iter()
652 .filter(|declaration| {
653 declaration.under_media || declaration.under_supports || declaration.under_layer
654 })
655 .count(),
656 }
657}
658
659impl DesignTokenResolutionSignalV0 {
660 fn occurrence_resolution_ready(&self) -> bool {
661 self.declaration_fact_count > 0 || self.reference_fact_count > 0
662 }
663
664 fn source_order_signal_ready(&self) -> bool {
665 self.source_ordered_declaration_count > 0 || self.source_ordered_reference_count > 0
666 }
667
668 fn selector_context_resolution_ready(&self) -> bool {
669 self.occurrence_resolution_ready()
670 && (self.root_declaration_count > 0 || self.selector_scoped_declaration_count > 0)
671 }
672}
673
674impl DesignTokenCascadeRankingSignalV0 {
675 fn source_order_cascade_ranking_ready(&self) -> bool {
676 self.ranked_reference_count > 0
677 }
678
679 fn has_shadowing_signal(&self) -> bool {
680 self.source_order_shadowed_declaration_count > 0
681 }
682
683 fn has_workspace_signal(&self) -> bool {
684 self.cross_file_candidate_declaration_count > 0
685 }
686
687 fn theme_override_context_ready(&self) -> bool {
688 self.theme_context_winner_reference_count > 0
689 }
690}
691
692impl DesignTokenExternalDeclarationCandidateScopeV0 {
693 fn cross_file_import_graph_ready(self) -> bool {
694 matches!(
695 self,
696 DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph
697 )
698 }
699
700 fn resolution_scope(self) -> &'static str {
701 match self {
702 DesignTokenExternalDeclarationCandidateScopeV0::Workspace => "workspace-candidate",
703 DesignTokenExternalDeclarationCandidateScopeV0::CrossFileImportGraph => {
704 "cross-file-import-candidate"
705 }
706 }
707 }
708}
709
710#[derive(Clone, Copy)]
711enum DesignTokenCandidateDeclaration<'a> {
712 Local(&'a ParserIndexCustomPropertyDeclFactV0),
713 Workspace(&'a DesignTokenWorkspaceDeclarationFactV0),
714}
715
716#[derive(Debug, Clone, PartialEq, Eq)]
717struct DesignTokenCascadeContext {
718 layer_name_ranks: BTreeMap<String, i32>,
719 layer_name_depths: BTreeMap<String, usize>,
720 layer_ranks_by_selector: BTreeMap<String, i32>,
721 layer_names_by_selector: BTreeMap<String, String>,
722 unlayered_rank: i32,
723}
724
725impl DesignTokenCascadeContext {
726 fn from_style_context_index(index: &StyleContextIndexV0) -> Self {
727 let mut layer_name_ranks = BTreeMap::<String, i32>::new();
728 let mut layer_name_depths = BTreeMap::<String, usize>::new();
729 let mut local_name_counts = BTreeMap::<&str, usize>::new();
730 for layer in &index.layer_index.order_nodes {
731 *local_name_counts
732 .entry(layer.local_name.as_str())
733 .or_default() += 1;
734 layer_name_ranks.insert(
735 layer.canonical_name.clone(),
736 layer.cascade_rank.min(i32::MAX as usize) as i32,
737 );
738 layer_name_depths.insert(layer.canonical_name.clone(), layer.nesting_depth);
739 }
740 for layer in &index.layer_index.order_nodes {
741 if local_name_counts.get(layer.local_name.as_str()) == Some(&1) {
742 layer_name_ranks.insert(
743 layer.local_name.clone(),
744 layer.cascade_rank.min(i32::MAX as usize) as i32,
745 );
746 layer_name_depths.insert(layer.local_name.clone(), layer.nesting_depth);
747 }
748 }
749
750 let mut block_layer_ranks = BTreeMap::<String, (usize, i32, String)>::new();
751 for binding in &index.layer_index.block_bindings {
752 block_layer_ranks.insert(
753 binding.context_id.clone(),
754 (
755 binding.nesting_depth,
756 binding.cascade_rank.min(i32::MAX as usize) as i32,
757 binding.canonical_name.clone(),
758 ),
759 );
760 }
761
762 let mut selector_layers = BTreeMap::<String, (usize, i32, String)>::new();
763 for membership in &index.layer_index.selector_memberships {
764 let Some(candidate) = block_layer_ranks.get(&membership.context_id) else {
765 continue;
766 };
767 let entry = selector_layers
768 .entry(membership.selector_name.clone())
769 .or_insert_with(|| candidate.clone());
770 if candidate.0 > entry.0 || (candidate.0 == entry.0 && candidate.1 > entry.1) {
771 *entry = candidate.clone();
772 }
773 }
774 let layer_ranks_by_selector = selector_layers
775 .iter()
776 .map(|(selector, (_, rank, _))| (selector.clone(), *rank))
777 .collect();
778 let layer_names_by_selector = selector_layers
779 .into_iter()
780 .map(|(selector, (_, _, name))| (selector, name))
781 .collect();
782
783 let unlayered_rank = index
784 .layer_index
785 .order_nodes
786 .len()
787 .saturating_add(index.layer_index.anonymous_layer_block_count)
788 .saturating_add(1)
789 .min(i32::MAX as usize) as i32;
790
791 Self {
792 layer_name_ranks,
793 layer_name_depths,
794 layer_ranks_by_selector,
795 layer_names_by_selector,
796 unlayered_rank,
797 }
798 }
799
800 fn layer_rank_for(
801 &self,
802 layer_names: &[String],
803 selector_contexts: &[String],
804 under_layer: bool,
805 ) -> LayerRank {
806 if !under_layer {
807 return LayerRank(self.unlayered_rank);
808 }
809 let canonical_path = layer_names.join(".");
810 if let Some(rank) = self.layer_name_ranks.get(canonical_path.as_str()) {
811 return LayerRank(*rank);
812 }
813 if let Some((rank, _)) = layer_names
814 .iter()
815 .filter_map(|name| {
816 Some((
817 self.layer_name_ranks.get(name).copied()?,
818 self.layer_name_depths.get(name).copied().unwrap_or(0),
819 ))
820 })
821 .max_by_key(|(_, depth)| *depth)
822 {
823 return LayerRank(rank);
824 }
825 selector_contexts
826 .iter()
827 .filter_map(|selector| {
828 self.layer_ranks_by_selector
829 .get(normalized_selector(selector))
830 })
831 .copied()
832 .max()
833 .map(LayerRank)
834 .unwrap_or(LayerRank(0))
835 }
836
837 fn layer_name_for(
838 &self,
839 layer_names: &[String],
840 selector_contexts: &[String],
841 under_layer: bool,
842 ) -> Option<String> {
843 if !under_layer {
844 return None;
845 }
846 let canonical_path = layer_names.join(".");
847 if self.layer_name_ranks.contains_key(canonical_path.as_str()) {
848 return Some(canonical_path);
849 }
850 if let Some(name) = layer_names
851 .iter()
852 .filter(|name| self.layer_name_ranks.contains_key(*name))
853 .max_by_key(|name| self.layer_name_depths.get(*name).copied().unwrap_or(0))
854 {
855 return Some(name.clone());
856 }
857 selector_contexts.iter().find_map(|selector| {
858 self.layer_names_by_selector
859 .get(normalized_selector(selector))
860 .cloned()
861 })
862 }
863}
864
865impl DesignTokenCandidateDeclaration<'_> {
866 fn cascade_key(
867 &self,
868 reference: &ParserIndexCustomPropertyRefFactV0,
869 workspace_file_ranks: Option<&BTreeMap<&str, usize>>,
870 cascade_context: &DesignTokenCascadeContext,
871 ) -> CascadeKey {
872 let scope_proximity = cascade_scope_proximity_fallback_for_selector_context_rank(
873 self.context_rank(reference),
874 );
875 match self {
876 DesignTokenCandidateDeclaration::Local(declaration) => CascadeKey::new(
877 CascadeLevel::AuthorNormal,
878 cascade_context.layer_rank_for(
879 &declaration.layer_names,
880 &declaration.selector_contexts,
881 declaration.under_layer,
882 ),
883 scope_proximity,
884 Specificity::ZERO,
885 ModuleRank::ZERO,
886 cascade_u32_rank(declaration.source_order),
887 ),
888 DesignTokenCandidateDeclaration::Workspace(declaration) => {
889 let file_rank = workspace_file_ranks
890 .and_then(|ranks| ranks.get(declaration.file_path.as_str()).copied())
891 .unwrap_or(usize::MAX);
892 CascadeKey::new(
893 CascadeLevel::AuthorNormal,
894 cascade_context.layer_rank_for(
895 &declaration.layer_names,
896 &declaration.selector_contexts,
897 declaration.under_layer,
898 ),
899 scope_proximity,
900 Specificity::ZERO,
901 ModuleRank::new(
902 cascade_inverse_rank(
903 declaration.import_graph_distance.unwrap_or(usize::MAX),
904 ),
905 cascade_inverse_rank(declaration.import_graph_order.unwrap_or(usize::MAX)),
906 cascade_inverse_rank(file_rank),
907 ),
908 cascade_u32_rank(declaration.source_order),
909 )
910 }
911 }
912 }
913
914 fn source_order(&self) -> usize {
915 match self {
916 DesignTokenCandidateDeclaration::Local(declaration) => declaration.source_order,
917 DesignTokenCandidateDeclaration::Workspace(declaration) => declaration.source_order,
918 }
919 }
920
921 fn file_path(&self) -> Option<&str> {
922 match self {
923 DesignTokenCandidateDeclaration::Local(_) => None,
924 DesignTokenCandidateDeclaration::Workspace(declaration) => {
925 Some(declaration.file_path.as_str())
926 }
927 }
928 }
929
930 fn range(&self) -> Option<ParserRangeV0> {
931 match self {
932 DesignTokenCandidateDeclaration::Local(_) => None,
933 DesignTokenCandidateDeclaration::Workspace(declaration) => Some(declaration.range),
934 }
935 }
936
937 fn import_graph_distance(&self) -> Option<usize> {
938 match self {
939 DesignTokenCandidateDeclaration::Local(_) => None,
940 DesignTokenCandidateDeclaration::Workspace(declaration) => {
941 declaration.import_graph_distance
942 }
943 }
944 }
945
946 fn import_graph_order(&self) -> Option<usize> {
947 match self {
948 DesignTokenCandidateDeclaration::Local(_) => None,
949 DesignTokenCandidateDeclaration::Workspace(declaration) => {
950 declaration.import_graph_order
951 }
952 }
953 }
954
955 fn is_local_source_order(&self, source_order: usize) -> bool {
956 matches!(
957 self,
958 DesignTokenCandidateDeclaration::Local(declaration)
959 if declaration.source_order == source_order
960 )
961 }
962
963 fn is_workspace(&self, declaration: &DesignTokenWorkspaceDeclarationFactV0) -> bool {
964 matches!(
965 self,
966 DesignTokenCandidateDeclaration::Workspace(winner)
967 if winner.file_path == declaration.file_path
968 && winner.source_order == declaration.source_order
969 && winner.name == declaration.name
970 )
971 }
972
973 fn is_workspace_winner(&self) -> bool {
974 matches!(self, DesignTokenCandidateDeclaration::Workspace(_))
975 }
976
977 fn layer_rank(&self, cascade_context: &DesignTokenCascadeContext) -> LayerRank {
978 match self {
979 DesignTokenCandidateDeclaration::Local(declaration) => cascade_context.layer_rank_for(
980 &declaration.layer_names,
981 &declaration.selector_contexts,
982 declaration.under_layer,
983 ),
984 DesignTokenCandidateDeclaration::Workspace(declaration) => cascade_context
985 .layer_rank_for(
986 &declaration.layer_names,
987 &declaration.selector_contexts,
988 declaration.under_layer,
989 ),
990 }
991 }
992
993 fn layer_name(&self, cascade_context: &DesignTokenCascadeContext) -> Option<String> {
994 match self {
995 DesignTokenCandidateDeclaration::Local(declaration) => cascade_context.layer_name_for(
996 &declaration.layer_names,
997 &declaration.selector_contexts,
998 declaration.under_layer,
999 ),
1000 DesignTokenCandidateDeclaration::Workspace(declaration) => cascade_context
1001 .layer_name_for(
1002 &declaration.layer_names,
1003 &declaration.selector_contexts,
1004 declaration.under_layer,
1005 ),
1006 }
1007 }
1008
1009 fn is_theme_context_winner(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> bool {
1010 self.context_rank(reference) >= 2
1011 }
1012
1013 fn context_rank(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> usize {
1014 match self {
1015 DesignTokenCandidateDeclaration::Local(declaration) => {
1016 custom_property_declaration_context_rank(&declaration.selector_contexts, reference)
1017 }
1018 DesignTokenCandidateDeclaration::Workspace(declaration) => {
1019 custom_property_declaration_context_rank(&declaration.selector_contexts, reference)
1020 }
1021 }
1022 }
1023
1024 fn context_kind(&self, reference: &ParserIndexCustomPropertyRefFactV0) -> &'static str {
1025 match self.context_rank(reference) {
1026 2.. => "selector",
1027 1 => "root",
1028 _ => "global",
1029 }
1030 }
1031}
1032
1033fn custom_property_declaration_key(
1034 declaration: &ParserIndexCustomPropertyDeclFactV0,
1035) -> (String, usize) {
1036 (declaration.name.clone(), declaration.source_order)
1037}
1038
1039fn custom_property_context_matches(
1040 declaration: &ParserIndexCustomPropertyDeclFactV0,
1041 reference: &ParserIndexCustomPropertyRefFactV0,
1042) -> bool {
1043 if declaration.name != reference.name {
1044 return false;
1045 }
1046 if declaration.under_media && !reference.under_media {
1047 return false;
1048 }
1049 if declaration.under_supports && !reference.under_supports {
1050 return false;
1051 }
1052 if !condition_context_applies(&declaration.condition_context, &reference.condition_context) {
1053 return false;
1054 }
1055 if declaration.selector_contexts.is_empty() {
1056 return true;
1057 }
1058 declaration
1059 .selector_contexts
1060 .iter()
1061 .any(|selector| custom_property_selector_context_matches(selector, reference))
1062}
1063
1064fn custom_property_workspace_context_matches(
1065 declaration: &DesignTokenWorkspaceDeclarationFactV0,
1066 reference: &ParserIndexCustomPropertyRefFactV0,
1067) -> bool {
1068 if declaration.name != reference.name {
1069 return false;
1070 }
1071 if declaration.under_media && !reference.under_media {
1072 return false;
1073 }
1074 if declaration.under_supports && !reference.under_supports {
1075 return false;
1076 }
1077 if !condition_context_applies(&declaration.condition_context, &reference.condition_context) {
1078 return false;
1079 }
1080 if declaration.selector_contexts.is_empty() {
1081 return true;
1082 }
1083 declaration
1084 .selector_contexts
1085 .iter()
1086 .any(|selector| custom_property_selector_context_matches(selector, reference))
1087}
1088
1089fn condition_context_applies(declaration_context: &[String], reference_context: &[String]) -> bool {
1090 declaration_context
1091 .iter()
1092 .all(|condition| reference_context.iter().any(|value| value == condition))
1093}
1094
1095fn custom_property_selector_context_matches(
1096 declaration_selector: &str,
1097 reference: &ParserIndexCustomPropertyRefFactV0,
1098) -> bool {
1099 !matches!(
1100 selector_context_witness_for_declaration(
1101 declaration_selector,
1102 &reference.selector_contexts
1103 )
1104 .verdict,
1105 SelectorMatchVerdict::No
1106 )
1107}
1108
1109fn custom_property_declaration_context_rank(
1110 declaration_selectors: &[String],
1111 reference: &ParserIndexCustomPropertyRefFactV0,
1112) -> usize {
1113 selector_context_witness(declaration_selectors, &reference.selector_contexts).rank
1114}
1115
1116fn summarize_workspace_candidate_file_ranks<'a>(
1117 workspace_candidates: &[&'a DesignTokenWorkspaceDeclarationFactV0],
1118) -> BTreeMap<&'a str, usize> {
1119 workspace_candidates
1120 .iter()
1121 .map(|candidate| candidate.file_path.as_str())
1122 .collect::<BTreeSet<_>>()
1123 .into_iter()
1124 .enumerate()
1125 .map(|(rank, file_path)| (file_path, rank))
1126 .collect()
1127}
1128
1129fn cascade_scope_proximity_fallback_for_selector_context_rank(context_rank: usize) -> u32 {
1130 match context_rank {
1131 2.. => 0,
1132 1 => 1,
1133 _ => 2,
1134 }
1135}
1136
1137fn cascade_u32_rank(rank: usize) -> u32 {
1138 rank.min(u32::MAX as usize) as u32
1139}
1140
1141fn cascade_inverse_rank(rank: usize) -> u32 {
1142 u32::MAX - cascade_u32_rank(rank)
1143}
1144
1145fn normalized_selector(selector: &str) -> &str {
1146 selector.trim().trim_start_matches('.')
1147}