1use std::collections::BTreeMap;
4
5use omena_cascade::{
6 CascadeComputedValueInputV0, CascadeComputedValueResultV0, CascadeRegisteredCustomPropertyV0,
7 CascadeRegisteredValueVerdictV0, CascadeValue, CustomPropertyEnv,
8 compute_cascade_computed_value,
9};
10use omena_query_checker_orchestrator::active_omena_checker_custom_property_registrations_v0;
11use omena_query_core::{CssValueValidationClassV0, validate_registered_property_value_v0};
12use omena_query_transform_runner::parse_static_css_cascade_value;
13use omena_syntax::ident::{AuthoredPropertyTextV0, PropertyNameV0};
14use serde::Serialize;
15
16use super::cascade_checker::{
17 collect_query_checker_cascade_declarations,
18 collect_query_checker_custom_property_registrations,
19 query_runtime_cascade_declaration_from_input,
20};
21
22#[derive(Debug, Clone, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct OmenaQueryRegisteredCustomPropertyComputedValueV0 {
25 pub schema_version: &'static str,
26 pub product: &'static str,
27 pub style_uri: String,
28 pub selector: String,
29 pub property: AuthoredPropertyTextV0,
30 pub registration_applied: bool,
31 pub registration_projection_complete: bool,
32 pub matched_value_count: usize,
33 pub unmatched_value_count: usize,
34 pub unknown_value_count: usize,
35 pub computed_value: CascadeComputedValueResultV0,
36}
37
38impl PartialEq for OmenaQueryRegisteredCustomPropertyComputedValueV0 {
39 fn eq(&self, other: &Self) -> bool {
40 self.schema_version == other.schema_version
41 && self.product == other.product
42 && self.style_uri == other.style_uri
43 && self.selector == other.selector
44 && self
45 .property
46 .to_property_name()
47 .same_as(&other.property.to_property_name())
48 && self.registration_applied == other.registration_applied
49 && self.registration_projection_complete == other.registration_projection_complete
50 && self.matched_value_count == other.matched_value_count
51 && self.unmatched_value_count == other.unmatched_value_count
52 && self.unknown_value_count == other.unknown_value_count
53 && self.computed_value == other.computed_value
54 }
55}
56
57impl Eq for OmenaQueryRegisteredCustomPropertyComputedValueV0 {}
58
59pub fn summarize_omena_query_registered_custom_property_computed_value_v0(
60 style_uri: &str,
61 source: &str,
62 selector: &str,
63 property: &str,
64 parent_computed_value: Option<CascadeValue>,
65) -> OmenaQueryRegisteredCustomPropertyComputedValueV0 {
66 let registration_inputs =
67 collect_query_checker_custom_property_registrations(style_uri, source);
68 let active_registrations =
69 active_omena_checker_custom_property_registrations_v0(registration_inputs.as_slice());
70 let property_key = PropertyNameV0::canonical_custom_key(property);
71 let property_name = PropertyNameV0::from_authored(property);
72 let active_registration = active_registrations.get(&property_key);
73 let mut verdicts = BTreeMap::new();
74 let mut matched_value_count = 0usize;
75 let mut unmatched_value_count = 0usize;
76 let mut unknown_value_count = 0usize;
77 let declarations = collect_query_checker_cascade_declarations(source)
78 .into_iter()
79 .filter(|declaration| {
80 declaration
81 .input
82 .property
83 .to_property_name()
84 .same_as(&property_name)
85 && declaration.input.selector.as_str() == selector
86 && declaration.input.condition_context.is_empty()
87 })
88 .map(|declaration| {
89 let mut cascade_declaration =
90 query_runtime_cascade_declaration_from_input(&declaration.input);
91 cascade_declaration.value =
92 parse_static_css_cascade_value(declaration.input.value.as_str())
93 .unwrap_or(CascadeValue::GuaranteedInvalid);
94 if let Some(registration) = active_registration {
95 let verdict = match validate_registered_property_value_v0(
96 registration.syntax.as_str(),
97 declaration.input.value.as_str(),
98 )
99 .class
100 {
101 CssValueValidationClassV0::Valid => {
102 matched_value_count += 1;
103 CascadeRegisteredValueVerdictV0::Matched
104 }
105 CssValueValidationClassV0::Invalid => {
106 unmatched_value_count += 1;
107 CascadeRegisteredValueVerdictV0::Unmatched
108 }
109 CssValueValidationClassV0::NotValidatable => {
110 unknown_value_count += 1;
111 CascadeRegisteredValueVerdictV0::Unknown
112 }
113 };
114 verdicts.insert(cascade_declaration.id.clone(), verdict);
115 }
116 cascade_declaration
117 })
118 .collect::<Vec<_>>();
119
120 let (registered_custom_property, registration_projection_complete) = match active_registration {
121 Some(registration) => {
122 let initial_value = registration
123 .initial_value
124 .as_deref()
125 .map(parse_static_css_cascade_value)
126 .unwrap_or(Some(CascadeValue::GuaranteedInvalid));
127 match initial_value {
128 Some(initial_value) => (
129 Some(CascadeRegisteredCustomPropertyV0 {
130 name: registration.name.clone(),
131 inherits: registration.inherits,
132 initial_value,
133 declaration_value_verdicts: verdicts,
134 }),
135 true,
136 ),
137 None => (None, false),
138 }
139 }
140 None => (None, true),
141 };
142 let registration_applied = registered_custom_property.is_some();
143 let computed_value = compute_cascade_computed_value(CascadeComputedValueInputV0 {
144 property: AuthoredPropertyTextV0::new(property),
145 declarations,
146 custom_property_env: CustomPropertyEnv::new(),
147 parent_computed_value,
148 registered_custom_property,
149 standard_property_value_verdicts: BTreeMap::new(),
150 });
151
152 OmenaQueryRegisteredCustomPropertyComputedValueV0 {
153 schema_version: "0",
154 product: "omena-query.registered-custom-property-computed-value",
155 style_uri: style_uri.to_string(),
156 selector: selector.to_string(),
157 property: AuthoredPropertyTextV0::new(property),
158 registration_applied,
159 registration_projection_complete,
160 matched_value_count,
161 unmatched_value_count,
162 unknown_value_count,
163 computed_value,
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use omena_cascade::{ComputedCascadeIndeterminateReasonV0, ComputedCascadeValueStatusV0};
171
172 #[test]
173 fn registered_computed_value_identity_uses_sealed_property_keys() {
174 let plain = summarize_omena_query_registered_custom_property_computed_value_v0(
175 "tokens.css",
176 "",
177 ":root",
178 "--foo",
179 None,
180 );
181 let mut escaped = plain.clone();
182 escaped.property = AuthoredPropertyTextV0::new(r"--f\6f o");
183 escaped.computed_value.property = AuthoredPropertyTextV0::new(r"--f\6f o");
184 assert_eq!(plain, escaped);
185
186 let mut different_case = plain.clone();
187 different_case.property = AuthoredPropertyTextV0::new("--FOO");
188 different_case.computed_value.property = AuthoredPropertyTextV0::new("--FOO");
189 assert_ne!(plain, different_case);
190 }
191
192 #[test]
193 fn registered_properties_use_typed_syntax_inheritance_and_initial_values() {
194 let source = r#"
195@property --gap {
196 syntax: '<length>';
197 inherits: false;
198 initial-value: 8px;
199}
200.valid { --gap: 12px; }
201.invalid { --gap: red; }
202"#;
203
204 let valid = summarize_omena_query_registered_custom_property_computed_value_v0(
205 "tokens.css",
206 source,
207 ".valid",
208 "--gap",
209 Some(CascadeValue::Literal("16px".to_string())),
210 );
211 assert!(valid.registration_applied);
212 assert!(valid.registration_projection_complete);
213 assert_eq!(valid.matched_value_count, 1);
214 assert_eq!(valid.unmatched_value_count, 0);
215 assert_eq!(
216 valid.computed_value.status,
217 ComputedCascadeValueStatusV0::Resolved
218 );
219 assert_eq!(
220 valid.computed_value.value,
221 CascadeValue::Literal("12px".to_string())
222 );
223
224 let invalid = summarize_omena_query_registered_custom_property_computed_value_v0(
225 "tokens.css",
226 source,
227 ".invalid",
228 "--gap",
229 Some(CascadeValue::Literal("16px".to_string())),
230 );
231 assert_eq!(invalid.matched_value_count, 0);
232 assert_eq!(invalid.unmatched_value_count, 1);
233 assert_eq!(
234 invalid.computed_value.status,
235 ComputedCascadeValueStatusV0::InvalidAtComputedValueTime
236 );
237 assert_eq!(
238 invalid.computed_value.value,
239 CascadeValue::Literal("8px".to_string())
240 );
241 assert!(invalid.computed_value.invalid_at_computed_value_time);
242
243 let absent = summarize_omena_query_registered_custom_property_computed_value_v0(
244 "tokens.css",
245 source,
246 ".absent",
247 "--gap",
248 Some(CascadeValue::Literal("16px".to_string())),
249 );
250 assert_eq!(
251 absent.computed_value.status,
252 ComputedCascadeValueStatusV0::Initial
253 );
254 assert_eq!(
255 absent.computed_value.value,
256 CascadeValue::Literal("8px".to_string())
257 );
258 }
259
260 #[test]
261 fn indeterminate_registered_syntax_has_a_typed_reason() {
262 let source = r#"
263@property --gap {
264 syntax: '<length>';
265 inherits: false;
266 initial-value: 8px;
267}
268.deferred { --gap: var(--runtime-value); }
269"#;
270
271 let result = summarize_omena_query_registered_custom_property_computed_value_v0(
272 "tokens.css",
273 source,
274 ".deferred",
275 "--gap",
276 None,
277 );
278
279 assert_eq!(result.unknown_value_count, 1);
280 assert_eq!(
281 result.computed_value.status,
282 ComputedCascadeValueStatusV0::Indeterminate
283 );
284 assert_eq!(result.computed_value.value, CascadeValue::Indeterminate);
285 assert!(!result.computed_value.invalid_at_computed_value_time);
286 assert_eq!(
287 result.computed_value.indeterminate_reason,
288 Some(ComputedCascadeIndeterminateReasonV0::RegisteredPropertySyntaxIndeterminate)
289 );
290 }
291
292 #[test]
293 fn property_registration_joins_escape_identity_but_not_custom_case() {
294 let escaped = summarize_omena_query_registered_custom_property_computed_value_v0(
295 "tokens.css",
296 r#"
297@property --f\6f o {
298 syntax: '<length>';
299 inherits: false;
300 initial-value: 8px;
301}
302.target { --foo: 12px; }
303"#,
304 ".target",
305 "--foo",
306 None,
307 );
308 assert!(escaped.registration_applied);
309 assert_eq!(escaped.matched_value_count, 1);
310
311 let distinct_case = summarize_omena_query_registered_custom_property_computed_value_v0(
312 "tokens.css",
313 r#"
314@property --FOO {
315 syntax: '<length>';
316 inherits: false;
317 initial-value: 8px;
318}
319.target { --foo: 12px; }
320"#,
321 ".target",
322 "--foo",
323 None,
324 );
325 assert!(!distinct_case.registration_applied);
326 }
327
328 #[test]
329 fn unregistered_custom_properties_preserve_the_existing_computed_path() {
330 let report = summarize_omena_query_registered_custom_property_computed_value_v0(
331 "tokens.css",
332 ".target { --legacy-gap: 12px; }",
333 ".target",
334 "--legacy-gap",
335 Some(CascadeValue::Literal("16px".to_string())),
336 );
337
338 assert!(!report.registration_applied);
339 assert!(report.registration_projection_complete);
340 assert_eq!(report.matched_value_count, 0);
341 assert_eq!(report.unmatched_value_count, 0);
342 assert_eq!(report.unknown_value_count, 0);
343 assert_eq!(
344 report.computed_value.status,
345 ComputedCascadeValueStatusV0::Resolved
346 );
347 assert_eq!(
348 report.computed_value.value,
349 CascadeValue::Literal("12px".to_string())
350 );
351 assert_eq!(
352 report.computed_value.derivation_steps,
353 vec![
354 "cascadeWinnerSelected",
355 "computedValueResolutionStarted",
356 "computedValueResolved",
357 ]
358 );
359 }
360}