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