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 std::borrow::Cow;
14use std::collections::{BTreeMap, BTreeSet};
15
16#[derive(Clone, Copy)]
17pub(super) struct TransformResolutionContext<'a> {
18 pub(super) package_manifests: &'a [OmenaQueryStylePackageManifestV0],
19 pub(super) bundler_path_mappings: &'a [OmenaResolverBundlerPathAliasMappingV0],
20 pub(super) tsconfig_path_mappings: &'a [OmenaResolverTsconfigPathMappingV0],
21 pub(super) disk_style_path_identities: &'a [OmenaResolverStyleModuleDiskCandidateIdentityV0],
22}
23
24impl<'a> TransformResolutionContext<'a> {
25 pub(super) fn from_resolution_inputs(
26 resolution_inputs: &'a OmenaQueryStyleResolutionInputsV0,
27 ) -> Self {
28 Self {
29 package_manifests: resolution_inputs.package_manifests.as_slice(),
30 bundler_path_mappings: resolution_inputs.bundler_path_mappings.as_slice(),
31 tsconfig_path_mappings: resolution_inputs.tsconfig_path_mappings.as_slice(),
32 disk_style_path_identities: resolution_inputs.disk_style_path_identities.as_slice(),
33 }
34 }
35
36 pub(super) fn resolve_style_module_source(
37 self,
38 from_style_path: &str,
39 source: &str,
40 available_style_paths: &BTreeSet<&str>,
41 ) -> Option<String> {
42 let load_path_roots = super::super::collect_load_path_roots(available_style_paths);
43 let load_path_root_refs = load_path_roots
44 .iter()
45 .map(String::as_str)
46 .collect::<Vec<_>>();
47 let resolver_package_manifests = self
48 .package_manifests
49 .iter()
50 .map(|manifest| OmenaResolverStylePackageManifestV0 {
51 package_json_path: manifest.package_json_path.clone(),
52 package_json_source: manifest.package_json_source.clone(),
53 })
54 .collect::<Vec<_>>();
55 summarize_omena_resolver_style_module_resolution_with_confirmation_inputs(
56 from_style_path,
57 source,
58 available_style_paths,
59 self.disk_style_path_identities,
60 &resolver_package_manifests,
61 self.bundler_path_mappings,
62 self.tsconfig_path_mappings,
63 &load_path_root_refs,
64 OmenaResolverStyleModuleConfirmationOptionsV0 {
65 allow_disk_confirmation: true,
66 ..OmenaResolverStyleModuleConfirmationOptionsV0::default()
67 },
68 )
69 .resolved_style_path
70 }
71}
72
73pub(super) fn merge_transform_context(
74 mut merged: TransformExecutionContextV0,
75 context: &TransformExecutionContextV0,
76) -> TransformExecutionContextV0 {
77 merged.drop_dark_mode_media_queries =
78 merged.drop_dark_mode_media_queries || context.drop_dark_mode_media_queries;
79 if context.vendor_prefix_policy.is_some() {
80 merged.vendor_prefix_policy = context.vendor_prefix_policy;
81 }
82 if context.supports_target_capability.is_some() {
83 merged.supports_target_capability = context.supports_target_capability;
84 }
85 merge_context_list(
86 &mut merged.reachable_class_names,
87 &context.reachable_class_names,
88 );
89 merge_context_list(
90 &mut merged.reachable_keyframe_names,
91 &context.reachable_keyframe_names,
92 );
93 merge_context_list(
94 &mut merged.reachable_value_names,
95 &context.reachable_value_names,
96 );
97 merge_context_list(
98 &mut merged.reachable_custom_property_names,
99 &context.reachable_custom_property_names,
100 );
101
102 if context.scss_module_evaluation.is_some() {
103 merged.scss_module_evaluation = context.scss_module_evaluation.clone();
104 }
105 if context.less_module_evaluation.is_some() {
106 merged.less_module_evaluation = context.less_module_evaluation.clone();
107 }
108 if !context.import_inlines.is_empty() {
109 merge_context_records_by_key(
110 &mut merged.import_inlines,
111 &context.import_inlines,
112 |inline| inline.import_source.as_str(),
113 );
114 }
115 if !context.class_name_rewrites.is_empty() {
116 merge_context_records_by_key(
117 &mut merged.class_name_rewrites,
118 &context.class_name_rewrites,
119 |rewrite| rewrite.original_name.as_str(),
120 );
121 }
122 if !context.css_module_composes_resolutions.is_empty() {
123 merge_context_records_by_key(
124 &mut merged.css_module_composes_resolutions,
125 &context.css_module_composes_resolutions,
126 |resolution| resolution.local_class_name.as_str(),
127 );
128 }
129 if !context.css_module_value_resolutions.is_empty() {
130 merge_context_records_by_key(
131 &mut merged.css_module_value_resolutions,
132 &context.css_module_value_resolutions,
133 |resolution| resolution.local_name.as_str(),
134 );
135 }
136 if !context.design_token_routes.is_empty() {
137 merge_context_records_by_key(
138 &mut merged.design_token_routes,
139 &context.design_token_routes,
140 |route| route.token_name.as_str(),
141 );
142 }
143 if context.cascade_environment.is_some() {
144 merged.cascade_environment = context.cascade_environment.clone();
145 }
146
147 expand_reachable_class_names_through_composes(&mut merged);
148 merged
149}
150
151fn expand_reachable_class_names_through_composes(context: &mut TransformExecutionContextV0) {
152 let mut changed = true;
153 while changed {
154 changed = false;
155 for resolution in &context.css_module_composes_resolutions {
156 if !class_name_is_reachable(
157 &resolution.local_class_name,
158 &context.reachable_class_names,
159 ) {
160 continue;
161 }
162 for exported_class_name in &resolution.exported_class_names {
163 if !class_name_is_reachable(exported_class_name, &context.reachable_class_names) {
164 context
165 .reachable_class_names
166 .push(exported_class_name.clone());
167 changed = true;
168 }
169 }
170 }
171 }
172 context.reachable_class_names.sort();
173 context.reachable_class_names.dedup();
174}
175
176fn class_name_is_reachable(class_name: &str, reachable_class_names: &[String]) -> bool {
177 let Some(normalized_class_name) = normalize_reachable_class_name(class_name) else {
178 return false;
179 };
180 reachable_class_names
181 .iter()
182 .filter_map(|name| normalize_reachable_class_name(name))
183 .any(|name| css_identifier_names_match(name, normalized_class_name))
184}
185
186fn normalize_reachable_class_name(name: &str) -> Option<&str> {
187 let name = name.trim().strip_prefix('.').unwrap_or(name.trim());
188 (!name.is_empty()).then_some(name)
189}
190
191pub(super) fn css_identifier_names_match(left: &str, right: &str) -> bool {
192 left == right || decode_css_identifier_escapes(left) == decode_css_identifier_escapes(right)
193}
194
195pub(super) fn decode_css_identifier_escapes(text: &str) -> Cow<'_, str> {
196 if !text.contains('\\') {
197 return Cow::Borrowed(text);
198 }
199
200 let mut output = String::with_capacity(text.len());
201 let mut index = 0usize;
202 while index < text.len() {
203 let Some(ch) = text[index..].chars().next() else {
204 break;
205 };
206 if ch != '\\' {
207 output.push(ch);
208 index += ch.len_utf8();
209 continue;
210 }
211
212 index += ch.len_utf8();
213 let Some(next) = text[index..].chars().next() else {
214 output.push('\\');
215 break;
216 };
217 if next.is_ascii_hexdigit() {
218 let hex_start = index;
219 let mut hex_end = index;
220 let mut digit_count = 0usize;
221 while hex_end < text.len() && digit_count < 6 {
222 let Some(candidate) = text[hex_end..].chars().next() else {
223 break;
224 };
225 if !candidate.is_ascii_hexdigit() {
226 break;
227 }
228 hex_end += candidate.len_utf8();
229 digit_count += 1;
230 }
231 if let Some(decoded) = u32::from_str_radix(&text[hex_start..hex_end], 16)
232 .ok()
233 .and_then(char::from_u32)
234 {
235 output.push(decoded);
236 }
237 index = hex_end;
238 if let Some(terminator) = text[index..].chars().next()
239 && terminator.is_ascii_whitespace()
240 {
241 index += terminator.len_utf8();
242 }
243 continue;
244 }
245
246 output.push(next);
247 index += next.len_utf8();
248 }
249
250 Cow::Owned(output)
251}
252
253pub(super) fn merge_target_options_transform_context(
254 context: &TransformExecutionContextV0,
255 target_options: OmenaQueryTargetTransformOptionsV0,
256) -> TransformExecutionContextV0 {
257 let mut merged = context.clone();
258 if target_options.drop_dark_mode_media_queries {
259 merged.drop_dark_mode_media_queries = true;
260 }
261 merged
262}
263
264pub(super) fn find_target_style_source<'a>(
265 target_style_path: &str,
266 style_sources: &'a [OmenaQueryStyleSourceInputV0],
267) -> Option<&'a str> {
268 style_sources
269 .iter()
270 .find(|source| source.style_path == target_style_path)
271 .map(|source| source.style_source.as_str())
272}
273
274pub(super) fn summarize_omena_query_transform_context_from_sources_with_resolution_context<'a>(
275 target_style_path: &str,
276 styles: impl IntoIterator<Item = (&'a str, &'a str)>,
277 resolution_context: TransformResolutionContext<'_>,
278) -> OmenaQueryTransformContextFromSourcesSummaryV0 {
279 let style_sources = styles.into_iter().collect::<Vec<_>>();
280 let style_count = style_sources.len();
281 let style_fact_entries = collect_omena_query_style_fact_entries(style_sources.as_slice());
282 let source_by_path = style_sources
283 .iter()
284 .map(|(style_path, style_source)| ((*style_path).to_string(), (*style_source).to_string()))
285 .collect::<BTreeMap<_, _>>();
286 let available_style_paths = style_fact_entries
287 .iter()
288 .map(|entry| entry.style_path.as_str())
289 .collect::<BTreeSet<_>>();
290 let target_entry = style_fact_entries
291 .iter()
292 .find(|entry| entry.style_path == target_style_path);
293
294 let mut context = TransformExecutionContextV0::default();
295
296 if let Some(entry) = target_entry {
297 context.import_inlines = derive_import_inlines_for_transform_context(
298 entry,
299 &style_fact_entries,
300 &available_style_paths,
301 &source_by_path,
302 resolution_context,
303 );
304 let scss_module_uses = derive_static_scss_module_use_evaluations_for_transform_context(
305 entry,
306 &available_style_paths,
307 &source_by_path,
308 resolution_context,
309 );
310 match omena_parser_dialect_for_style_path(entry.style_path.as_str()) {
311 OmenaParserStyleDialect::Scss | OmenaParserStyleDialect::Sass => {
312 let dialect = omena_parser_dialect_for_style_path(entry.style_path.as_str());
313 context.scss_module_evaluation =
314 derive_static_stylesheet_module_evaluation_for_transform_context(
315 entry.style_source.as_str(),
316 dialect,
317 &context.import_inlines,
318 &scss_module_uses,
319 );
320 }
321 OmenaParserStyleDialect::Less => {
322 context.less_module_evaluation =
323 derive_static_stylesheet_module_evaluation_for_transform_context(
324 entry.style_source.as_str(),
325 OmenaParserStyleDialect::Less,
326 &context.import_inlines,
327 &[],
328 );
329 }
330 OmenaParserStyleDialect::Css => {}
331 }
332 context.class_name_rewrites = derive_class_name_rewrites_for_transform_context(entry);
333 context.css_module_composes_resolutions =
334 derive_css_module_composes_resolutions_for_transform_context(
335 entry,
336 &style_fact_entries,
337 &available_style_paths,
338 resolution_context,
339 );
340 context.css_module_value_resolutions =
341 derive_css_module_value_resolutions_for_transform_context(
342 entry,
343 &style_fact_entries,
344 &available_style_paths,
345 &source_by_path,
346 resolution_context,
347 );
348 context.design_token_routes = derive_design_token_routes_for_transform_context(
349 entry,
350 &style_fact_entries,
351 resolution_context,
352 );
353 }
354
355 OmenaQueryTransformContextFromSourcesSummaryV0 {
356 schema_version: "0",
357 product: "omena-query.transform-context",
358 target_style_path: target_style_path.to_string(),
359 style_count,
360 import_inline_count: context.import_inlines.len(),
361 class_name_rewrite_count: context.class_name_rewrites.len(),
362 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
363 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
364 design_token_route_count: context.design_token_routes.len(),
365 reachable_class_name_count: context.reachable_class_names.len(),
366 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
367 reachable_value_name_count: context.reachable_value_names.len(),
368 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
369 context,
370 ready_surfaces: vec![
371 "transformContextProducer",
372 "stylesheetModuleEvaluationProducer",
373 "cssModuleClassRewriteProducer",
374 "cssModuleComposesResolutionProducer",
375 "cssModuleValueResolutionProducer",
376 "designTokenRouteProducer",
377 "transitiveImportInlineProducer",
378 ],
379 }
380}
381
382pub(super) struct OmenaQueryEngineInputTransformContextDerivationV0 {
383 pub(super) summary: OmenaQueryTransformContextFromEngineInputSummaryV0,
384 pub(super) reachability_precision: Option<FactPrecision>,
385 pub(super) closed_set_enumeration_candidate: bool,
386}
387
388pub fn summarize_omena_query_transform_context_from_engine_input(
389 input: &EngineInputV2,
390 target_style_path: &str,
391 closed_world_requested: bool,
392) -> OmenaQueryTransformContextFromEngineInputSummaryV0 {
393 derive_omena_query_transform_context_from_engine_input(
394 input,
395 target_style_path,
396 closed_world_requested,
397 )
398 .summary
399}
400
401pub(super) fn derive_omena_query_transform_context_from_engine_input(
402 input: &EngineInputV2,
403 target_style_path: &str,
404 closed_world_requested: bool,
405) -> OmenaQueryEngineInputTransformContextDerivationV0 {
406 let (projection_summary, projection_precisions) =
407 summarize_omena_query_expression_domain_selector_projection_with_precision(input);
408 let precision_by_projection = projection_precisions
409 .iter()
410 .map(|entry| {
411 (
412 (entry.graph_id.as_str(), entry.node_id.as_str()),
413 entry.precision,
414 )
415 })
416 .collect::<BTreeMap<_, _>>();
417 let mut reachable_class_names = BTreeSet::new();
418 let mut reachability_sources = Vec::new();
419 let mut reachability_precision_ceiling: Option<FactPrecision> = None;
420 let mut closed_set_enumeration_candidate = true;
421 let mut selected_projection_count = 0_usize;
422
423 for projection in &projection_summary.projections {
424 if projection.target_style_paths.is_empty()
425 || projection
426 .target_style_paths
427 .iter()
428 .any(|path| path == target_style_path)
429 {
430 selected_projection_count += 1;
431 closed_set_enumeration_candidate &=
432 matches!(projection.value_kind, "bottom" | "exact" | "finiteSet");
433 reachable_class_names.extend(projection.selector_names.iter().cloned());
434 let projection_precision = precision_by_projection
435 .get(&(projection.graph_id.as_str(), projection.node_id.as_str()))
436 .copied()
437 .unwrap_or(FactPrecision::Unknown);
438 reachability_precision_ceiling = Some(
439 reachability_precision_ceiling.map_or(projection_precision, |current| {
440 current.bounded_by(projection_precision)
441 }),
442 );
443 reachability_sources.push(OmenaQuerySemanticReachabilitySourceV0 {
444 graph_id: projection.graph_id.clone(),
445 file_path: projection.file_path.clone(),
446 node_id: projection.node_id.clone(),
447 target_style_paths: projection.target_style_paths.clone(),
448 value_kind: projection.value_kind,
449 reduced_product: projection.reduced_product.clone(),
450 selector_names: projection.selector_names.clone(),
451 certainty: projection.certainty,
452 });
453 }
454 }
455
456 let semantic_context = TransformExecutionContextV0 {
457 reachable_class_names: reachable_class_names.into_iter().collect(),
458 ..TransformExecutionContextV0::default()
459 };
460 let style_sources = input
461 .styles
462 .iter()
463 .filter_map(|style| {
464 style
465 .source
466 .as_deref()
467 .map(|source| (style.file_path.as_str(), source))
468 })
469 .collect::<Vec<_>>();
470 let source_context_summary = (!style_sources.is_empty()).then(|| {
471 super::summarize_omena_query_transform_context_from_sources(
472 target_style_path,
473 style_sources,
474 &[],
475 )
476 });
477 let context = if let Some(source_context_summary) = &source_context_summary {
478 merge_transform_context(source_context_summary.context.clone(), &semantic_context)
479 } else {
480 semantic_context
481 };
482
483 let mut ready_surfaces = vec![
484 "expressionDomainSelectorProjection",
485 "semanticReachabilityTransformContext",
486 ];
487 if source_context_summary.is_some() {
488 ready_surfaces.push("engineInputStyleSourceTransformContext");
489 }
490
491 OmenaQueryEngineInputTransformContextDerivationV0 {
492 reachability_precision: reachability_precision_ceiling,
493 closed_set_enumeration_candidate: selected_projection_count > 0
494 && closed_set_enumeration_candidate,
495 summary: OmenaQueryTransformContextFromEngineInputSummaryV0 {
496 schema_version: "0",
497 product: "omena-query.transform-context-from-engine-input",
498 input_version: input.version.clone(),
499 target_style_path: target_style_path.to_string(),
500 closed_world_requested,
501 style_source_count: source_context_summary
502 .as_ref()
503 .map_or(0, |summary| summary.style_count),
504 projection_count: projection_summary.projection_count,
505 selected_projection_count: reachability_sources.len(),
506 import_inline_count: context.import_inlines.len(),
507 class_name_rewrite_count: context.class_name_rewrites.len(),
508 css_module_composes_resolution_count: context.css_module_composes_resolutions.len(),
509 css_module_value_resolution_count: context.css_module_value_resolutions.len(),
510 design_token_route_count: context.design_token_routes.len(),
511 reachable_class_name_count: context.reachable_class_names.len(),
512 reachable_keyframe_name_count: context.reachable_keyframe_names.len(),
513 reachable_value_name_count: context.reachable_value_names.len(),
514 reachable_custom_property_name_count: context.reachable_custom_property_names.len(),
515 reachability_sources,
516 context,
517 ready_surfaces,
518 },
519 }
520}
521
522fn merge_context_list(target: &mut Vec<String>, additional: &[String]) {
523 for item in additional {
524 if !target.contains(item) {
525 target.push(item.clone());
526 }
527 }
528 target.sort();
529}
530
531fn merge_context_records_by_key<T, F>(target: &mut Vec<T>, overrides: &[T], key: F)
532where
533 T: Clone,
534 F: Fn(&T) -> &str,
535{
536 for item in overrides {
537 let item_key = key(item);
538 if let Some(existing) = target.iter_mut().find(|existing| key(existing) == item_key) {
539 *existing = item.clone();
540 } else {
541 target.push(item.clone());
542 }
543 }
544 target.sort_by(|left, right| key(left).cmp(key(right)));
545}