Skip to main content

omena_query/style/
registered_property_values.rs

1//! Registered custom-property projection into the cascade computed-value model.
2
3use 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}