1use super::css_modules::{
2 derive_class_name_rewrites_for_transform_context,
3 derive_css_module_composes_resolutions_for_transform_context,
4 derive_css_module_value_resolutions_for_transform_context,
5};
6use super::design_tokens::derive_design_token_routes_for_transform_context;
7use super::imports::derive_import_inlines_for_transform_context;
8use super::static_stylesheet::{
9 derive_static_scss_module_use_evaluations_for_transform_context,
10 derive_static_stylesheet_module_evaluation_for_transform_context,
11};
12use super::*;
13use crate::types::{
14 OmenaQueryEngineInputModuleAttributionV0, normalize_omena_query_style_path,
15 resolve_omena_query_style_path_against_known,
16};
17use omena_syntax::ident::{AuthoredPropertyTextV0, CanonicalCustomPropertyNameV0, ClassNameV0};
18use std::collections::{BTreeMap, BTreeSet};
19
20#[derive(Clone, Copy)]
21pub(super) struct TransformResolutionContext<'a> {
22 pub(super) package_manifests: &'a [OmenaQueryStylePackageManifestV0],
23 pub(super) bundler_path_mappings: &'a [OmenaResolverBundlerPathAliasMappingV0],
24 pub(super) tsconfig_path_mappings: &'a [OmenaResolverTsconfigPathMappingV0],
25 pub(super) disk_style_path_identities: &'a [OmenaResolverStyleModuleDiskCandidateIdentityV0],
26 pub(super) resolver_identity_index:
27 Option<&'a OmenaResolverStyleModuleConfirmationIdentityIndexV0>,
28}
29
30impl<'a> TransformResolutionContext<'a> {
31 pub(super) fn from_resolution_inputs(
32 resolution_inputs: &'a OmenaQueryStyleResolutionInputsV0,
33 ) -> Self {
34 Self {
35 package_manifests: resolution_inputs.package_manifests.as_slice(),
36 bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
37 tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
38 disk_style_path_identities: resolution_inputs.disk_style_path_identities.as_slice(),
39 resolver_identity_index: None,
40 }
41 }
42
43 fn with_resolver_identity_index<'b>(
44 self,
45 resolver_identity_index: &'b OmenaResolverStyleModuleConfirmationIdentityIndexV0,
46 ) -> TransformResolutionContext<'b>
47 where
48 'a: 'b,
49 {
50 TransformResolutionContext {
51 package_manifests: self.package_manifests,
52 bundler_path_mappings: self.bundler_path_mappings,
53 tsconfig_path_mappings: self.tsconfig_path_mappings,
54 disk_style_path_identities: self.disk_style_path_identities,
55 resolver_identity_index: Some(resolver_identity_index),
56 }
57 }
58
59 pub(super) fn resolve_style_module_source(
60 self,
61 from_style_path: &str,
62 source: &str,
63 available_style_paths: &BTreeSet<&str>,
64 ) -> Option<String> {
65 self.resolve_style_module(from_style_path, source, available_style_paths)
66 .resolved_style_path
67 }
68
69 pub(super) fn resolve_style_module(
70 self,
71 from_style_path: &str,
72 source: &str,
73 available_style_paths: &BTreeSet<&str>,
74 ) -> OmenaResolverStyleModuleResolutionV0 {
75 let load_path_roots = super::super::collect_load_path_roots(available_style_paths);
76 let load_path_root_refs = load_path_roots
77 .iter()
78 .map(String::as_str)
79 .collect::<Vec<_>>();
80 let resolver_package_manifests = self
81 .package_manifests
82 .iter()
83 .map(|manifest| OmenaResolverStylePackageManifestV0 {
84 package_json_path: manifest.package_json_path.clone(),
85 package_json_source: manifest.package_json_source.clone(),
86 })
87 .collect::<Vec<_>>();
88 summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
89 from_style_path,
90 source,
91 available_style_paths,
92 self.disk_style_path_identities,
93 &resolver_package_manifests,
94 self.bundler_path_mappings,
95 self.tsconfig_path_mappings,
96 &load_path_root_refs,
97 OmenaResolverStyleModuleConfirmationOptionsV0 {
98 allow_disk_confirmation: true,
99 identity_index: self.resolver_identity_index,
100 ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
101 },
102 )
103 }
104}
105
106pub(super) fn merge_transform_context(
107 mut merged: TransformExecutionContextV0,
108 context: &TransformExecutionContextV0,
109) -> TransformExecutionContextV0 {
110 merged.drop_dark_mode_media_queries =
111 merged.drop_dark_mode_media_queries || context.drop_dark_mode_media_queries;
112 if context.vendor_prefix_policy.is_some() {
113 merged.vendor_prefix_policy = context.vendor_prefix_policy;
114 }
115 if context.supports_target_capability.is_some() {
116 merged.supports_target_capability = context.supports_target_capability;
117 }
118 merge_context_list(
119 &mut merged.reachable_class_names,
120 &context.reachable_class_names,
121 );
122 merge_context_list(
123 &mut merged.reachable_keyframe_names,
124 &context.reachable_keyframe_names,
125 );
126 merge_context_list(
127 &mut merged.reachable_value_names,
128 &context.reachable_value_names,
129 );
130 merged
131 .reachable_custom_property_names
132 .extend(context.reachable_custom_property_names.iter().cloned());
133 merged.reachable_custom_property_names =
134 dedupe_custom_property_names(merged.reachable_custom_property_names.drain(..));
135
136 if context.scss_module_evaluation.is_some() {
137 merged.scss_module_evaluation = context.scss_module_evaluation.clone();
138 }
139 if context.less_module_evaluation.is_some() {
140 merged.less_module_evaluation = context.less_module_evaluation.clone();
141 }
142 if !context.import_inlines.is_empty() {
143 merge_context_records_by_key(
144 &mut merged.import_inlines,
145 &context.import_inlines,
146 |inline| inline.import_source.as_str(),
147 );
148 }
149 if !context.class_name_rewrites.is_empty() {
150 merge_class_context_records_by_key(
151 &mut merged.class_name_rewrites,
152 &context.class_name_rewrites,
153 |rewrite| rewrite.original_name.as_str(),
154 );
155 }
156 if !context.css_module_composes_resolutions.is_empty() {
157 merge_class_context_records_by_key(
158 &mut merged.css_module_composes_resolutions,
159 &context.css_module_composes_resolutions,
160 |resolution| resolution.local_class_name.as_str(),
161 );
162 }
163 if !context.css_module_value_resolutions.is_empty() {
164 merge_context_records_by_key(
165 &mut merged.css_module_value_resolutions,
166 &context.css_module_value_resolutions,
167 |resolution| resolution.local_name.as_str(),
168 );
169 }
170 if !context.design_token_routes.is_empty() {
171 merge_design_token_routes_by_key(
172 &mut merged.design_token_routes,
173 &context.design_token_routes,
174 );
175 }
176 if context.cascade_environment.is_some() {
177 merged.cascade_environment = context.cascade_environment.clone();
178 }
179
180 expand_reachable_class_names_through_composes(&mut merged);
181 merged
182}
183
184fn expand_reachable_class_names_through_composes(context: &mut TransformExecutionContextV0) {
185 let mut changed = true;
186 while changed {
187 changed = false;
188 for resolution in &context.css_module_composes_resolutions {
189 if !class_name_is_reachable(
190 &resolution.local_class_name,
191 &context.reachable_class_names,
192 ) {
193 continue;
194 }
195 for exported_class_name in &resolution.exported_class_names {
196 if !class_name_is_reachable(exported_class_name, &context.reachable_class_names) {
197 context
198 .reachable_class_names
199 .push(exported_class_name.clone());
200 changed = true;
201 }
202 }
203 }
204 }
205 context.reachable_class_names.sort();
206 context.reachable_class_names.dedup();
207}
208
209fn class_name_is_reachable(class_name: &str, reachable_class_names: &[String]) -> bool {
210 let Some(normalized_class_name) = normalize_reachable_class_name(class_name) else {
211 return false;
212 };
213 reachable_class_names
214 .iter()
215 .filter_map(|name| normalize_reachable_class_name(name))
216 .any(|name| css_identifier_names_match(name, normalized_class_name))
217}
218
219fn normalize_reachable_class_name(name: &str) -> Option<&str> {
220 let name = name.trim().strip_prefix('.').unwrap_or(name.trim());
221 (!name.is_empty()).then_some(name)
222}
223
224pub(super) fn css_identifier_names_match(left: &str, right: &str) -> bool {
225 ClassNameV0::new(left).same_as(&ClassNameV0::new(right))
226}
227
228pub(super) fn merge_target_options_transform_context(
229 context: &TransformExecutionContextV0,
230 target_options: OmenaQueryTargetTransformOptionsV0,
231) -> TransformExecutionContextV0 {
232 let mut merged = context.clone();
233 if target_options.drop_dark_mode_media_queries {
234 merged.drop_dark_mode_media_queries = true;
235 }
236 merged
237}
238
239pub(super) fn find_target_style_source<'a>(
240 target_style_path: &str,
241 style_sources: &'a [OmenaQueryStyleSourceInputV0],
242) -> Option<&'a str> {
243 style_sources
244 .iter()
245 .find(|source| source.style_path == target_style_path)
246 .map(|source| source.style_source.as_str())
247}
248
249pub(super) fn summarize_omena_query_transform_context_from_sources_with_resolution_context<'a>(
250 target_style_path: &str,
251 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
252 resolution_context: TransformResolutionContext<'_>,
253) -> OmenaQueryTransformContextFromSourcesSummaryV0 {
254 derive_omena_query_transform_context_from_sources_with_resolution_context(
255 target_style_path,
256 styles,
257 resolution_context,
258 )
259 .summary
260}
261
262pub(super) struct OmenaQueryTransformContextFromSourcesDerivationV0 {
263 pub(super) summary: OmenaQueryTransformContextFromSourcesSummaryV0,
264 pub(super) style_fact_entries: Vec<OmenaQueryStyleFactEntry>,
265}
266
267pub(super) fn derive_omena_query_transform_context_from_sources_with_resolution_context<'a>(
268 target_style_path: &str,
269 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
270 resolution_context: TransformResolutionContext<'_>,
271) -> OmenaQueryTransformContextFromSourcesDerivationV0 {
272 let style_sources = styles.into_iter().collect::<Vec<_>>();
273 let style_count = style_sources.len();
274 let style_fact_entries = collect_omena_query_style_fact_entries(style_sources.as_slice());
275 let source_by_path = style_sources
276 .iter()
277 .map(|(style_path, style_source)| ((*style_path).to_string(), (*style_source).to_string()))
278 .collect::<BTreeMap<_, _>>();
279 let available_style_paths = style_fact_entries
280 .iter()
281 .map(|entry| entry.style_path.as_str())
282 .collect::<BTreeSet<_>>();
283 let resolver_identity_index = build_omena_resolver_style_module_confirmation_identity_index(
284 &available_style_paths,
285 resolution_context.disk_style_path_identities,
286 );
287 let resolution_context =
288 resolution_context.with_resolver_identity_index(&resolver_identity_index);
289 let known_style_paths = available_style_paths
290 .iter()
291 .map(|path| normalize_omena_query_style_path(path))
292 .collect::<Vec<_>>();
293 let canonical_target_style_path =
294 resolve_omena_query_style_path_against_known(target_style_path, &known_style_paths)
295 .unwrap_or_else(|| normalize_omena_query_style_path(target_style_path));
296 let target_entry = style_fact_entries.iter().find(|entry| {
297 normalize_omena_query_style_path(entry.style_path.as_str()) == canonical_target_style_path
298 });
299
300 let mut context = TransformExecutionContextV0::default();
301
302 if let Some(entry) = target_entry {
303 context.import_inlines = derive_import_inlines_for_transform_context(
304 entry,
305 &style_fact_entries,
306 &available_style_paths,
307 &source_by_path,
308 resolution_context,
309 );
310 let scss_module_uses = derive_static_scss_module_use_evaluations_for_transform_context(
311 entry,
312 &available_style_paths,
313 &source_by_path,
314 resolution_context,
315 );
316 match omena_parser_dialect_for_style_path(entry.style_path.as_str()) {
317 OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass => {
318 let dialect = omena_parser_dialect_for_style_path(entry.style_path.as_str());
319 context.scss_module_evaluation =
320 derive_static_stylesheet_module_evaluation_for_transform_context(
321 entry.style_source.as_str(),
322 dialect,
323 &context.import_inlines,
324 &scss_module_uses,
325 );
326 }
327 OmenaParserStyleDialect::Less => {
328 context.less_module_evaluation =
329 derive_static_stylesheet_module_evaluation_for_transform_context(
330 entry.style_source.as_str(),
331 OmenaParserStyleDialect::Less,
332 &context.import_inlines,
333 &[],
334 );
335 }
336 OmenaParserStyleDialect::Css => {}
337 }
338 context.class_name_rewrites = derive_class_name_rewrites_for_transform_context(entry);
339 context.css_module_composes_resolutions =
340 derive_css_module_composes_resolutions_for_transform_context(
341 entry,
342 &style_fact_entries,
343 &available_style_paths,
344 resolution_context,
345 );
346 context.css_module_value_resolutions =
347 derive_css_module_value_resolutions_for_transform_context(
348 entry,
349 &style_fact_entries,
350 &available_style_paths,
351 &source_by_path,
352 resolution_context,
353 );
354 context.design_token_routes = derive_design_token_routes_for_transform_context(
355 entry,
356 &style_fact_entries,
357 resolution_context,
358 );
359 }
360
361 let summary = OmenaQueryTransformContextFromSourcesSummaryV0 {
362 schema_version: "0",
363 product: "omena-query.transform-context",
364 target_style_path: target_style_path.to_string(),
365 style_count,
366 import_inline_count: context.import_inlines.len(),
367 class_name_rewrite_count: context.class_name_rewrites.len(),
368 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
369 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
370 design_token_route_count: context.design_token_routes.len(),
371 reachable_class_name_count: context.reachable_class_names.len(),
372 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
373 reachable_value_name_count: context.reachable_value_names.len(),
374 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
375 context,
376 ready_surfaces: vec![
377 "transformContextProducer",
378 "stylesheetModuleEvaluationProducer",
379 "cssModuleClassRewriteProducer",
380 "cssModuleComposesResolutionProducer",
381 "cssModuleValueResolutionProducer",
382 "designTokenRouteProducer",
383 "transitiveImportInlineProducer",
384 ],
385 };
386 OmenaQueryTransformContextFromSourcesDerivationV0 {
387 summary,
388 style_fact_entries,
389 }
390}
391
392pub(super) struct OmenaQueryEngineInputTransformContextDerivationV0 {
393 pub(super) module_reachability: OmenaQueryEngineInputModuleReachabilityV0,
394 pub(super) reachability_precision: Option<FactPrecision>,
395 pub(super) closed_set_enumeration_candidate: bool,
396}
397
398pub fn summarize_omena_query_transform_context_from_engine_input(
399 input: &EngineInputV2,
400 target_style_path: &str,
401 closed_world_requested: bool,
402) -> OmenaQueryTransformContextFromEngineInputSummaryV0 {
403 derive_omena_query_transform_context_from_engine_input(
404 input,
405 target_style_path,
406 closed_world_requested,
407 )
408 .module_reachability
409 .into_summary()
410}
411
412pub fn derive_omena_query_module_reachability_from_engine_input(
413 input: &EngineInputV2,
414 target_style_path: &str,
415 closed_world_requested: bool,
416) -> OmenaQueryEngineInputModuleReachabilityV0 {
417 derive_omena_query_transform_context_from_engine_input(
418 input,
419 target_style_path,
420 closed_world_requested,
421 )
422 .module_reachability
423}
424
425pub(super) fn derive_omena_query_transform_context_from_engine_input(
426 input: &EngineInputV2,
427 target_style_path: &str,
428 closed_world_requested: bool,
429) -> OmenaQueryEngineInputTransformContextDerivationV0 {
430 let mut known_style_paths = input
431 .styles
432 .iter()
433 .map(|style| normalize_omena_query_style_path(style.file_path.as_str()))
434 .collect::<Vec<_>>();
435 known_style_paths.sort();
436 known_style_paths.dedup();
437 let canonical_target_style_path = resolve_omena_query_style_path_against_known(
438 target_style_path,
439 known_style_paths.as_slice(),
440 )
441 .unwrap_or_else(|| normalize_omena_query_style_path(target_style_path));
442 let (projection_summary, projection_precisions) =
443 omena_query_core::summarize_omena_query_expression_domain_selector_projection_with_precision_and_style_path_resolver(
444 input,
445 resolve_omena_query_style_path_against_known,
446 );
447 let precision_by_projection = projection_precisions
448 .iter()
449 .map(|entry| {
450 (
451 (entry.graph_id.as_str(), entry.node_id.as_str()),
452 entry.precision,
453 )
454 })
455 .collect::<BTreeMap<_, _>>();
456 let mut reachable_class_names = BTreeSet::new();
457 let mut reachability_sources = Vec::new();
458 let mut reachability_precision_ceiling: Option<FactPrecision> = None;
459 let mut closed_set_enumeration_candidate = true;
460 let mut selected_projection_count = 0_usize;
461 let mut targeted_class_names_by_style_path = BTreeMap::<String, BTreeSet<String>>::new();
462 let mut targeted_projection_count_by_style_path = BTreeMap::<String, usize>::new();
463 let mut unattributed_class_names = BTreeSet::new();
464 let mut unattributed_projection_count = 0_usize;
465 let mut projected_class_names = BTreeSet::new();
466
467 for projection in &projection_summary.projections {
468 projected_class_names.extend(projection.selector_names.iter().cloned());
469 if projection.target_style_paths.is_empty() {
470 unattributed_projection_count += 1;
471 unattributed_class_names.extend(projection.selector_names.iter().cloned());
472 } else {
473 let mut has_unresolved_target = false;
474 for target_style_path in projection
475 .target_style_paths
476 .iter()
477 .collect::<BTreeSet<_>>()
478 {
479 let Some(target_style_path) = resolve_omena_query_style_path_against_known(
480 target_style_path,
481 known_style_paths.as_slice(),
482 ) else {
483 let target_style_path = normalize_omena_query_style_path(target_style_path);
484 *targeted_projection_count_by_style_path
485 .entry(target_style_path.clone())
486 .or_default() += 1;
487 targeted_class_names_by_style_path
488 .entry(target_style_path)
489 .or_default()
490 .extend(projection.selector_names.iter().cloned());
491 has_unresolved_target = true;
492 continue;
493 };
494 *targeted_projection_count_by_style_path
495 .entry(target_style_path.clone())
496 .or_default() += 1;
497 targeted_class_names_by_style_path
498 .entry(target_style_path)
499 .or_default()
500 .extend(projection.selector_names.iter().cloned());
501 }
502 if has_unresolved_target {
503 unattributed_projection_count += 1;
504 unattributed_class_names.extend(projection.selector_names.iter().cloned());
505 }
506 }
507 let projection_targets_current_style = projection.target_style_paths.is_empty()
508 || projection.target_style_paths.iter().any(|path| {
509 resolve_omena_query_style_path_against_known(path, known_style_paths.as_slice())
510 .is_none_or(|path| path == canonical_target_style_path)
511 });
512 if projection_targets_current_style {
513 selected_projection_count += 1;
514 closed_set_enumeration_candidate &=
515 matches!(projection.value_kind, "bottom" | "exact" | "finiteSet");
516 reachable_class_names.extend(projection.selector_names.iter().cloned());
517 let projection_precision = precision_by_projection
518 .get(&(projection.graph_id.as_str(), projection.node_id.as_str()))
519 .copied()
520 .unwrap_or(FactPrecision::Unknown);
521 reachability_precision_ceiling = Some(
522 reachability_precision_ceiling.map_or(projection_precision, |current| {
523 current.bounded_by(projection_precision)
524 }),
525 );
526 reachability_sources.push(OmenaQuerySemanticReachabilitySourceV0 {
527 graph_id: projection.graph_id.clone(),
528 file_path: projection.file_path.clone(),
529 node_id: projection.node_id.clone(),
530 target_style_paths: projection.target_style_paths.clone(),
531 value_kind: projection.value_kind,
532 reduced_product: projection.reduced_product.clone(),
533 selector_names: projection.selector_names.clone(),
534 certainty: projection.certainty,
535 });
536 }
537 }
538
539 let semantic_context = TransformExecutionContextV0 {
540 reachable_class_names: reachable_class_names.into_iter().collect(),
541 ..TransformExecutionContextV0::default()
542 };
543 let style_sources = input
544 .styles
545 .iter()
546 .filter_map(|style| {
547 style
548 .source
549 .as_deref()
550 .map(|source| (style.file_path.as_str(), source))
551 })
552 .collect::<Vec<_>>();
553 let source_context_summary = (!style_sources.is_empty()).then(|| {
554 super::summarize_omena_query_transform_context_from_sources(
555 target_style_path,
556 style_sources,
557 &[],
558 )
559 });
560 let context = if let Some(source_context_summary) = &source_context_summary {
561 merge_transform_context(source_context_summary.context.clone(), &semantic_context)
562 } else {
563 semantic_context
564 };
565
566 let mut ready_surfaces = vec![
567 "expressionDomainSelectorProjection",
568 "semanticReachabilityTransformContext",
569 ];
570 if source_context_summary.is_some() {
571 ready_surfaces.push("engineInputStyleSourceTransformContext");
572 }
573
574 let summary = OmenaQueryTransformContextFromEngineInputSummaryV0 {
575 schema_version: "0",
576 product: "omena-query.transform-context-from-engine-input",
577 input_version: input.version.clone(),
578 target_style_path: target_style_path.to_string(),
579 closed_world_requested,
580 style_source_count: source_context_summary
581 .as_ref()
582 .map_or(0, |summary| summary.style_count),
583 projection_count: projection_summary.projection_count,
584 selected_projection_count: reachability_sources.len(),
585 import_inline_count: context.import_inlines.len(),
586 class_name_rewrite_count: context.class_name_rewrites.len(),
587 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
588 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
589 design_token_route_count: context.design_token_routes.len(),
590 reachable_class_name_count: context.reachable_class_names.len(),
591 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
592 reachable_value_name_count: context.reachable_value_names.len(),
593 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
594 reachability_sources,
595 context,
596 ready_surfaces,
597 };
598 let targeted_class_names_by_style_path = targeted_class_names_by_style_path
599 .into_iter()
600 .map(|(path, names)| (path, names.into_iter().collect()))
601 .collect();
602 let declared_class_names_by_style_path = input
603 .styles
604 .iter()
605 .map(|style| {
606 let mut class_names = style
607 .document
608 .selectors
609 .iter()
610 .filter_map(|selector| {
611 let name = selector
612 .canonical_name
613 .as_deref()
614 .unwrap_or(selector.name.as_str())
615 .trim();
616 let name = name.strip_prefix('.').unwrap_or(name);
617 (!name.is_empty()).then(|| name.to_string())
618 })
619 .collect::<Vec<_>>();
620 class_names.sort();
621 class_names.dedup();
622 (
623 normalize_omena_query_style_path(style.file_path.as_str()),
624 class_names,
625 )
626 })
627 .collect::<BTreeMap<_, _>>();
628 let module_reachability = OmenaQueryEngineInputModuleReachabilityV0::new(
629 summary,
630 known_style_paths,
631 OmenaQueryEngineInputModuleAttributionV0::new(
632 declared_class_names_by_style_path,
633 targeted_class_names_by_style_path,
634 targeted_projection_count_by_style_path,
635 ),
636 unattributed_class_names.into_iter().collect(),
637 unattributed_projection_count,
638 projected_class_names.into_iter().collect(),
639 );
640
641 OmenaQueryEngineInputTransformContextDerivationV0 {
642 reachability_precision: reachability_precision_ceiling,
643 closed_set_enumeration_candidate: selected_projection_count > 0
644 && closed_set_enumeration_candidate,
645 module_reachability,
646 }
647}
648
649fn merge_context_list(target: &mut Vec<String>, additional: &[String]) {
650 for item in additional {
651 if !target.contains(item) {
652 target.push(item.clone());
653 }
654 }
655 target.sort();
656}
657
658pub(super) fn dedupe_custom_property_names(
659 names: impl IntoIterator<Item = AuthoredPropertyTextV0>,
660) -> Vec<AuthoredPropertyTextV0> {
661 let mut by_identity = BTreeMap::<CanonicalCustomPropertyNameV0, AuthoredPropertyTextV0>::new();
662 for authored in names {
663 by_identity
664 .entry(authored.to_custom_key())
665 .or_insert(authored);
666 }
667 by_identity.into_values().collect()
668}
669
670fn merge_context_records_by_key<T, F>(target: &mut Vec<T>, overrides: &[T], key: F)
671where
672 T: Clone,
673 F: Fn(&T) -> &str,
674{
675 for item in overrides {
676 let item_key = key(item);
677 if let Some(existing) = target.iter_mut().find(|existing| key(existing) == item_key) {
678 *existing = item.clone();
679 } else {
680 target.push(item.clone());
681 }
682 }
683 target.sort_by(|left, right| key(left).cmp(key(right)));
684}
685
686fn merge_design_token_routes_by_key(
687 target: &mut Vec<omena_query_transform_runner::TransformDesignTokenRouteV0>,
688 overrides: &[omena_query_transform_runner::TransformDesignTokenRouteV0],
689) {
690 for item in overrides {
691 let item_key = item.token_name.to_custom_key();
692 if let Some(existing) = target
693 .iter_mut()
694 .find(|existing| existing.token_name.to_custom_key() == item_key)
695 {
696 *existing = item.clone();
697 } else {
698 target.push(item.clone());
699 }
700 }
701 target.sort_by_key(|route| route.token_name.to_custom_key());
702}
703
704fn merge_class_context_records_by_key<T, F>(target: &mut Vec<T>, overrides: &[T], key: F)
705where
706 T: Clone,
707 F: Fn(&T) -> &str,
708{
709 let mut merged = Vec::with_capacity(target.len() + overrides.len());
714 append_class_context_records_first_witness(&mut merged, overrides, &key);
715 append_class_context_records_first_witness(&mut merged, target, &key);
716 *target = merged;
717}
718
719fn append_class_context_records_first_witness<T, F>(target: &mut Vec<T>, candidates: &[T], key: &F)
720where
721 T: Clone,
722 F: Fn(&T) -> &str,
723{
724 let occupied_canonical_keys = target
725 .iter()
726 .map(|existing| ClassNameV0::new(key(existing)).canonical_key())
727 .collect::<BTreeSet<_>>();
728 let mut admitted_raw_keys = target
729 .iter()
730 .map(|existing| key(existing).to_string())
731 .collect::<BTreeSet<_>>();
732 for item in candidates {
733 let item_key = ClassNameV0::new(key(item));
734 if !occupied_canonical_keys.contains(&item_key.canonical_key())
735 && admitted_raw_keys.insert(key(item).to_string())
736 {
737 target.push(item.clone());
738 }
739 }
740}
741
742pub(super) fn merge_module_css_module_contexts_first_witness(
743 left: &[TransformModuleCssModuleContextV0],
744 right: &[TransformModuleCssModuleContextV0],
745) -> Vec<TransformModuleCssModuleContextV0> {
746 let mut merged = Vec::<TransformModuleCssModuleContextV0>::new();
747 for context in left.iter().chain(right) {
748 let target = if let Some(index) = merged
749 .iter()
750 .position(|candidate| candidate.module_instance == context.module_instance)
751 {
752 &mut merged[index]
753 } else {
754 let index = merged.len();
755 merged.push(TransformModuleCssModuleContextV0::new(
756 context.module_instance.clone(),
757 ));
758 &mut merged[index]
759 };
760 append_class_context_records_first_witness(
761 &mut target.class_name_rewrites,
762 context.class_name_rewrites.as_slice(),
763 &|rewrite: &TransformClassNameRewriteV0| rewrite.original_name.as_str(),
764 );
765 append_class_context_records_first_witness(
766 &mut target.composes_resolutions,
767 context.composes_resolutions.as_slice(),
768 &|resolution: &TransformCssModuleComposesResolutionV0| {
769 resolution.local_class_name.as_str()
770 },
771 );
772 }
773 merged
774}
775
776#[cfg(test)]
777mod tests {
778 use super::*;
779 use omena_query_transform_runner::{
780 TransformClassNameRewriteV0, TransformCssModuleComposesResolutionV0,
781 TransformCssModuleValueResolutionV0, TransformDesignTokenRouteV0, TransformImportInlineV0,
782 };
783
784 fn class_rewrite(original_name: &str, rewritten_name: &str) -> TransformClassNameRewriteV0 {
785 TransformClassNameRewriteV0 {
786 original_name: original_name.to_string(),
787 rewritten_name: rewritten_name.to_string(),
788 }
789 }
790
791 #[test]
792 fn explicit_class_rewrite_precedence_ignores_escape_spelling_order() {
793 for (derived_name, explicit_name) in [(r#"\E9 tat"#, "état"), ("état", r#"\E9 tat"#)] {
794 let derived = TransformExecutionContextV0 {
795 class_name_rewrites: vec![class_rewrite(derived_name, "_derived")],
796 css_module_composes_resolutions: vec![TransformCssModuleComposesResolutionV0 {
797 local_class_name: derived_name.to_string(),
798 exported_class_names: vec!["derived".to_string()],
799 }],
800 ..TransformExecutionContextV0::default()
801 };
802 let explicit = TransformExecutionContextV0 {
803 class_name_rewrites: vec![class_rewrite(explicit_name, "_explicit")],
804 css_module_composes_resolutions: vec![TransformCssModuleComposesResolutionV0 {
805 local_class_name: explicit_name.to_string(),
806 exported_class_names: vec!["explicit".to_string()],
807 }],
808 ..TransformExecutionContextV0::default()
809 };
810
811 let merged = merge_transform_context(derived, &explicit);
812
813 assert_eq!(
814 merged.class_name_rewrites,
815 vec![class_rewrite(explicit_name, "_explicit")]
816 );
817 assert_eq!(
818 merged.css_module_composes_resolutions,
819 vec![TransformCssModuleComposesResolutionV0 {
820 local_class_name: explicit_name.to_string(),
821 exported_class_names: vec!["explicit".to_string()],
822 }]
823 );
824 println!(
825 "canonical-merge derived={derived_name:?} explicit={explicit_name:?} winner={:?}",
826 merged.class_name_rewrites
827 );
828 }
829 }
830
831 #[test]
832 fn class_and_property_merge_authorities_stay_scoped_to_their_fields() {
833 let derived = TransformExecutionContextV0 {
834 import_inlines: vec![TransformImportInlineV0 {
835 import_source: r#"\E9 tat"#.to_string(),
836 replacement_css: "derived".to_string(),
837 }],
838 css_module_value_resolutions: vec![TransformCssModuleValueResolutionV0 {
839 local_name: r#"\E9 tat"#.to_string(),
840 resolved_value: "derived".to_string(),
841 }],
842 design_token_routes: vec![TransformDesignTokenRouteV0 {
843 token_name: AuthoredPropertyTextV0::new(r#"\E9 tat"#),
844 routed_value: "derived".to_string(),
845 }],
846 ..TransformExecutionContextV0::default()
847 };
848 let explicit = TransformExecutionContextV0 {
849 import_inlines: vec![TransformImportInlineV0 {
850 import_source: "état".to_string(),
851 replacement_css: "explicit".to_string(),
852 }],
853 css_module_value_resolutions: vec![TransformCssModuleValueResolutionV0 {
854 local_name: "état".to_string(),
855 resolved_value: "explicit".to_string(),
856 }],
857 design_token_routes: vec![TransformDesignTokenRouteV0 {
858 token_name: AuthoredPropertyTextV0::new("état"),
859 routed_value: "explicit".to_string(),
860 }],
861 ..TransformExecutionContextV0::default()
862 };
863
864 let merged = merge_transform_context(derived, &explicit);
865
866 assert_eq!(merged.import_inlines.len(), 2);
867 assert_eq!(merged.css_module_value_resolutions.len(), 2);
868 assert_eq!(merged.design_token_routes.len(), 1);
869 assert_eq!(merged.design_token_routes[0].routed_value, "explicit");
870 }
871
872 fn module_context(
873 module: &str,
874 original_name: &str,
875 rewritten_name: &str,
876 ) -> TransformModuleCssModuleContextV0 {
877 TransformModuleCssModuleContextV0::new(omena_parser::ModuleInstanceKeyV0::unconfigured(
878 omena_parser::ModuleIdV0::new(module),
879 ))
880 .with_class_name_rewrites(vec![class_rewrite(original_name, rewritten_name)])
881 }
882
883 fn merge_module_css_module_contexts_last_witness(
884 left: &[TransformModuleCssModuleContextV0],
885 right: &[TransformModuleCssModuleContextV0],
886 ) -> Vec<TransformModuleCssModuleContextV0> {
887 merge_module_css_module_contexts_first_witness(right, left)
888 }
889
890 #[test]
891 fn module_context_explicit_precedence_ignores_escape_spelling_order() {
892 for (derived_name, explicit_name) in [(r#"\E9 tat"#, "état"), ("état", r#"\E9 tat"#)] {
893 let explicit = vec![module_context(
894 "src/app.module.css",
895 explicit_name,
896 "_explicit",
897 )];
898 let derived = vec![module_context(
899 "src/app.module.css",
900 derived_name,
901 "_derived",
902 )];
903
904 let merged = merge_module_css_module_contexts_first_witness(&explicit, &derived);
905
906 assert_eq!(
910 merged,
911 vec![module_context(
912 "src/app.module.css",
913 explicit_name,
914 "_explicit",
915 )]
916 );
917 println!(
918 "module-canonical-merge derived={derived_name:?} explicit={explicit_name:?} winner={:?}",
919 merged[0].class_name_rewrites
920 );
921 }
922 }
923
924 #[test]
925 fn module_context_first_witness_merge_obeys_left_regular_band_laws() {
926 let u = vec![
927 module_context("src/a.module.css", "shared", "_u"),
928 module_context("src/b.module.css", "own", "_b"),
929 ];
930 let v = vec![
931 module_context("src/a.module.css", "shared", "_v"),
932 module_context("src/c.module.css", "own", "_c"),
933 ];
934 let w = vec![module_context("src/a.module.css", "third", "_w")];
935
936 let uv = merge_module_css_module_contexts_first_witness(&u, &v);
937 assert_eq!(
938 merge_module_css_module_contexts_first_witness(&uv, &w),
939 merge_module_css_module_contexts_first_witness(
940 &u,
941 &merge_module_css_module_contexts_first_witness(&v, &w),
942 ),
943 "associativity"
944 );
945 assert_eq!(
946 merge_module_css_module_contexts_first_witness(&u, &u),
947 u,
948 "idempotence"
949 );
950 assert_eq!(
951 merge_module_css_module_contexts_first_witness(&uv, &u),
952 uv,
953 "left-regular-band absorption"
954 );
955
956 let last_wins_uv = merge_module_css_module_contexts_last_witness(&u, &v);
957 let last_wins_uv_then_u = merge_module_css_module_contexts_last_witness(&last_wins_uv, &u);
958 assert_ne!(
959 last_wins_uv_then_u, last_wins_uv,
960 "a last-wins variant must fail the absorption control"
961 );
962 }
963}