Skip to main content

omena_transform_target/
lib.rs

1//! Target feature matrix planning for Omena CSS transforms.
2//!
3//! This crate owns the target-sensitive lowering decision boundary. It
4//! intentionally models the feature matrix directly before adding heavier browserslist /
5//! caniuse-lite ingestion, so target-driven transforms can be tested without
6//! coupling the core transform graph to external data formats.
7
8use omena_transform_cst::TransformPassKind;
9use omena_transform_passes::{TransformPassPlanV0, plan_transform_passes};
10use serde::Serialize;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "camelCase")]
14pub struct TargetFeatureSupportV0 {
15    pub vendor_prefix_required: bool,
16    pub supports_light_dark: bool,
17    pub supports_color_mix: bool,
18    pub supports_oklch_oklab: bool,
19    pub supports_color_function: bool,
20    pub supports_logical_properties: bool,
21    pub supports_css_nesting: bool,
22    pub supports_css_scope: bool,
23    pub supports_cascade_layers: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
27#[serde(rename_all = "camelCase")]
28pub struct TargetTransformOptionsV0 {
29    pub allow_logical_to_physical: bool,
30    pub allow_scope_flatten: bool,
31    pub allow_layer_flatten: bool,
32    pub enable_supports_static_eval: bool,
33    pub enable_media_static_eval: bool,
34}
35
36#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
37#[serde(rename_all = "camelCase")]
38pub struct TransformTargetBoundarySummaryV0 {
39    pub schema_version: &'static str,
40    pub product: &'static str,
41    pub managed_pass_ids: Vec<&'static str>,
42    pub opt_in_pass_ids: Vec<&'static str>,
43    pub target_data_source: &'static str,
44    pub planner_surface: &'static str,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
48#[serde(rename_all = "camelCase")]
49pub struct TransformTargetPlanV0 {
50    pub schema_version: &'static str,
51    pub product: &'static str,
52    pub target_label: String,
53    pub required_pass_ids: Vec<&'static str>,
54    pub blocked_pass_ids: Vec<&'static str>,
55    pub planned_pass_ids: Vec<&'static str>,
56    pub pass_plan: TransformPassPlanV0,
57}
58
59pub fn summarize_omena_transform_target_boundary() -> TransformTargetBoundarySummaryV0 {
60    TransformTargetBoundarySummaryV0 {
61        schema_version: "0",
62        product: "omena-transform-target.boundary",
63        managed_pass_ids: target_managed_passes()
64            .iter()
65            .map(|pass| pass.id())
66            .collect(),
67        opt_in_pass_ids: vec![
68            TransformPassKind::LogicalToPhysical.id(),
69            TransformPassKind::ScopeFlatten.id(),
70            TransformPassKind::LayerFlatten.id(),
71        ],
72        target_data_source: "explicitFeatureMatrixV0",
73        planner_surface: "omena-transform-passes.plan",
74    }
75}
76
77pub fn plan_target_transforms(
78    target_label: impl Into<String>,
79    support: TargetFeatureSupportV0,
80    options: TargetTransformOptionsV0,
81) -> TransformTargetPlanV0 {
82    let target_label = target_label.into();
83    let mut required_passes = Vec::new();
84    let mut blocked_passes = Vec::new();
85
86    if support.vendor_prefix_required {
87        required_passes.push(TransformPassKind::VendorPrefixing);
88    }
89    if !support.supports_light_dark {
90        required_passes.push(TransformPassKind::LightDarkLowering);
91    }
92    if !support.supports_color_mix {
93        required_passes.push(TransformPassKind::ColorMixLowering);
94    }
95    if !support.supports_oklch_oklab {
96        required_passes.push(TransformPassKind::OklchOklabLowering);
97    }
98    if !support.supports_color_function {
99        required_passes.push(TransformPassKind::ColorFunctionLowering);
100    }
101    if !support.supports_logical_properties {
102        push_required_or_blocked(
103            TransformPassKind::LogicalToPhysical,
104            options.allow_logical_to_physical,
105            &mut required_passes,
106            &mut blocked_passes,
107        );
108    }
109    if !support.supports_css_nesting {
110        required_passes.push(TransformPassKind::NestingUnwrap);
111    }
112    if !support.supports_css_scope {
113        push_required_or_blocked(
114            TransformPassKind::ScopeFlatten,
115            options.allow_scope_flatten,
116            &mut required_passes,
117            &mut blocked_passes,
118        );
119    }
120    if !support.supports_cascade_layers {
121        push_required_or_blocked(
122            TransformPassKind::LayerFlatten,
123            options.allow_layer_flatten,
124            &mut required_passes,
125            &mut blocked_passes,
126        );
127    }
128    if options.enable_supports_static_eval {
129        required_passes.push(TransformPassKind::SupportsStaticEval);
130    }
131    if options.enable_media_static_eval {
132        required_passes.push(TransformPassKind::MediaStaticEval);
133    }
134
135    required_passes.sort_by_key(|pass| pass.ordinal());
136    required_passes.dedup();
137    blocked_passes.sort_by_key(|pass| pass.ordinal());
138    blocked_passes.dedup();
139
140    let pass_plan = plan_transform_passes(&required_passes);
141    let planned_pass_ids = pass_plan.ordered_pass_ids.clone();
142
143    TransformTargetPlanV0 {
144        schema_version: "0",
145        product: "omena-transform-target.plan",
146        target_label,
147        required_pass_ids: required_passes.iter().map(|pass| pass.id()).collect(),
148        blocked_pass_ids: blocked_passes.iter().map(|pass| pass.id()).collect(),
149        planned_pass_ids,
150        pass_plan,
151    }
152}
153
154pub const fn modern_feature_support() -> TargetFeatureSupportV0 {
155    TargetFeatureSupportV0 {
156        vendor_prefix_required: false,
157        supports_light_dark: true,
158        supports_color_mix: true,
159        supports_oklch_oklab: true,
160        supports_color_function: true,
161        supports_logical_properties: true,
162        supports_css_nesting: true,
163        supports_css_scope: true,
164        supports_cascade_layers: true,
165    }
166}
167
168pub const fn conservative_target_options() -> TargetTransformOptionsV0 {
169    TargetTransformOptionsV0 {
170        allow_logical_to_physical: false,
171        allow_scope_flatten: false,
172        allow_layer_flatten: false,
173        enable_supports_static_eval: false,
174        enable_media_static_eval: false,
175    }
176}
177
178fn push_required_or_blocked(
179    pass: TransformPassKind,
180    allowed: bool,
181    required_passes: &mut Vec<TransformPassKind>,
182    blocked_passes: &mut Vec<TransformPassKind>,
183) {
184    if allowed {
185        required_passes.push(pass);
186    } else {
187        blocked_passes.push(pass);
188    }
189}
190
191fn target_managed_passes() -> [TransformPassKind; 11] {
192    [
193        TransformPassKind::VendorPrefixing,
194        TransformPassKind::LightDarkLowering,
195        TransformPassKind::ColorMixLowering,
196        TransformPassKind::OklchOklabLowering,
197        TransformPassKind::ColorFunctionLowering,
198        TransformPassKind::LogicalToPhysical,
199        TransformPassKind::NestingUnwrap,
200        TransformPassKind::ScopeFlatten,
201        TransformPassKind::LayerFlatten,
202        TransformPassKind::SupportsStaticEval,
203        TransformPassKind::MediaStaticEval,
204    ]
205}
206
207#[cfg(test)]
208mod tests {
209    use super::{
210        TargetFeatureSupportV0, TargetTransformOptionsV0, conservative_target_options,
211        plan_target_transforms, summarize_omena_transform_target_boundary,
212    };
213
214    #[test]
215    fn exposes_target_lowering_boundary() {
216        let boundary = summarize_omena_transform_target_boundary();
217
218        assert_eq!(boundary.product, "omena-transform-target.boundary");
219        assert_eq!(boundary.managed_pass_ids.len(), 11);
220        assert!(boundary.managed_pass_ids.contains(&"vendor-prefixing"));
221        assert!(boundary.managed_pass_ids.contains(&"media-static-eval"));
222        assert!(boundary.opt_in_pass_ids.contains(&"scope-flatten"));
223    }
224
225    #[test]
226    fn plans_target_lowering_with_vendor_prefix_after_lowering_edges() {
227        let support = TargetFeatureSupportV0 {
228            vendor_prefix_required: true,
229            supports_light_dark: false,
230            supports_color_mix: false,
231            supports_oklch_oklab: true,
232            supports_color_function: true,
233            supports_logical_properties: true,
234            supports_css_nesting: false,
235            supports_css_scope: false,
236            supports_cascade_layers: false,
237        };
238        let options = TargetTransformOptionsV0 {
239            allow_logical_to_physical: false,
240            allow_scope_flatten: true,
241            allow_layer_flatten: true,
242            enable_supports_static_eval: true,
243            enable_media_static_eval: true,
244        };
245
246        let plan = plan_target_transforms("legacy-webview", support, options);
247
248        assert_eq!(plan.pass_plan.violated_dag_edge_count, 0);
249        assert!(plan.required_pass_ids.contains(&"light-dark-lowering"));
250        assert!(plan.required_pass_ids.contains(&"color-mix-lowering"));
251        assert!(plan.required_pass_ids.contains(&"vendor-prefixing"));
252        assert!(plan.required_pass_ids.contains(&"scope-flatten"));
253        assert!(plan.required_pass_ids.contains(&"layer-flatten"));
254        let vendor_index = plan
255            .planned_pass_ids
256            .iter()
257            .position(|id| *id == "vendor-prefixing");
258        let light_dark_index = plan
259            .planned_pass_ids
260            .iter()
261            .position(|id| *id == "light-dark-lowering");
262        assert!(light_dark_index < vendor_index);
263    }
264
265    #[test]
266    fn blocks_opt_in_flattening_when_not_explicitly_enabled() {
267        let support = TargetFeatureSupportV0 {
268            supports_css_scope: false,
269            supports_cascade_layers: false,
270            ..super::modern_feature_support()
271        };
272
273        let plan = plan_target_transforms(
274            "modern-without-scope",
275            support,
276            conservative_target_options(),
277        );
278
279        assert!(plan.blocked_pass_ids.contains(&"scope-flatten"));
280        assert!(plan.blocked_pass_ids.contains(&"layer-flatten"));
281        assert!(!plan.required_pass_ids.contains(&"scope-flatten"));
282        assert!(!plan.required_pass_ids.contains(&"layer-flatten"));
283    }
284}