Skip to main content

omena_query/style/transform/
context.rs

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}