1use 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}