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 serde::Serialize;
14
15use super::cascade_checker::{
16    collect_query_checker_cascade_declarations,
17    collect_query_checker_custom_property_registrations,
18    query_runtime_cascade_declaration_from_input,
19};
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
22#[serde(rename_all = "camelCase")]
23pub struct OmenaQueryRegisteredCustomPropertyComputedValueV0 {
24    pub schema_version: &'static str,
25    pub product: &'static str,
26    pub style_uri: String,
27    pub selector: String,
28    pub property: String,
29    pub registration_applied: bool,
30    pub registration_projection_complete: bool,
31    pub matched_value_count: usize,
32    pub unmatched_value_count: usize,
33    pub unknown_value_count: usize,
34    pub computed_value: CascadeComputedValueResultV0,
35}
36
37pub fn summarize_omena_query_registered_custom_property_computed_value_v0(
38    style_uri: &str,
39    source: &str,
40    selector: &str,
41    property: &str,
42    parent_computed_value: Option<CascadeValue>,
43) -> OmenaQueryRegisteredCustomPropertyComputedValueV0 {
44    let registration_inputs =
45        collect_query_checker_custom_property_registrations(style_uri, source);
46    let active_registrations =
47        active_omena_checker_custom_property_registrations_v0(registration_inputs.as_slice());
48    let active_registration = active_registrations.get(property);
49    let mut verdicts = BTreeMap::new();
50    let mut matched_value_count = 0usize;
51    let mut unmatched_value_count = 0usize;
52    let mut unknown_value_count = 0usize;
53    let declarations = collect_query_checker_cascade_declarations(source)
54        .into_iter()
55        .filter(|declaration| {
56            declaration.input.property == property
57                && declaration.input.selector.as_str() == selector
58                && declaration.input.condition_context.is_empty()
59        })
60        .map(|declaration| {
61            let mut cascade_declaration =
62                query_runtime_cascade_declaration_from_input(&declaration.input);
63            cascade_declaration.value =
64                parse_static_css_cascade_value(declaration.input.value.as_str())
65                    .unwrap_or(CascadeValue::GuaranteedInvalid);
66            if let Some(registration) = active_registration {
67                let verdict = match validate_registered_property_value_v0(
68                    registration.syntax.as_str(),
69                    declaration.input.value.as_str(),
70                )
71                .class
72                {
73                    CssValueValidationClassV0::Valid => {
74                        matched_value_count += 1;
75                        CascadeRegisteredValueVerdictV0::Matched
76                    }
77                    CssValueValidationClassV0::Invalid => {
78                        unmatched_value_count += 1;
79                        CascadeRegisteredValueVerdictV0::Unmatched
80                    }
81                    CssValueValidationClassV0::NotValidatable => {
82                        unknown_value_count += 1;
83                        CascadeRegisteredValueVerdictV0::Unknown
84                    }
85                };
86                verdicts.insert(cascade_declaration.id.clone(), verdict);
87            }
88            cascade_declaration
89        })
90        .collect::<Vec<_>>();
91
92    let (registered_custom_property, registration_projection_complete) = match active_registration {
93        Some(registration) => {
94            let initial_value = registration
95                .initial_value
96                .as_deref()
97                .map(parse_static_css_cascade_value)
98                .unwrap_or(Some(CascadeValue::GuaranteedInvalid));
99            match initial_value {
100                Some(initial_value) => (
101                    Some(CascadeRegisteredCustomPropertyV0 {
102                        name: registration.name.clone(),
103                        inherits: registration.inherits,
104                        initial_value,
105                        declaration_value_verdicts: verdicts,
106                    }),
107                    true,
108                ),
109                None => (None, false),
110            }
111        }
112        None => (None, true),
113    };
114    let registration_applied = registered_custom_property.is_some();
115    let computed_value = compute_cascade_computed_value(CascadeComputedValueInputV0 {
116        property: property.to_string(),
117        declarations,
118        custom_property_env: CustomPropertyEnv::new(),
119        parent_computed_value,
120        registered_custom_property,
121        standard_property_value_verdicts: BTreeMap::new(),
122    });
123
124    OmenaQueryRegisteredCustomPropertyComputedValueV0 {
125        schema_version: "0",
126        product: "omena-query.registered-custom-property-computed-value",
127        style_uri: style_uri.to_string(),
128        selector: selector.to_string(),
129        property: property.to_string(),
130        registration_applied,
131        registration_projection_complete,
132        matched_value_count,
133        unmatched_value_count,
134        unknown_value_count,
135        computed_value,
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use omena_cascade::{ComputedCascadeIndeterminateReasonV0, ComputedCascadeValueStatusV0};
143
144    #[test]
145    fn registered_properties_use_typed_syntax_inheritance_and_initial_values() {
146        let source = r#"
147@property --gap {
148  syntax: '<length>';
149  inherits: false;
150  initial-value: 8px;
151}
152.valid { --gap: 12px; }
153.invalid { --gap: red; }
154"#;
155
156        let valid = summarize_omena_query_registered_custom_property_computed_value_v0(
157            "tokens.css",
158            source,
159            ".valid",
160            "--gap",
161            Some(CascadeValue::Literal("16px".to_string())),
162        );
163        assert!(valid.registration_applied);
164        assert!(valid.registration_projection_complete);
165        assert_eq!(valid.matched_value_count, 1);
166        assert_eq!(valid.unmatched_value_count, 0);
167        assert_eq!(
168            valid.computed_value.status,
169            ComputedCascadeValueStatusV0::Resolved
170        );
171        assert_eq!(
172            valid.computed_value.value,
173            CascadeValue::Literal("12px".to_string())
174        );
175
176        let invalid = summarize_omena_query_registered_custom_property_computed_value_v0(
177            "tokens.css",
178            source,
179            ".invalid",
180            "--gap",
181            Some(CascadeValue::Literal("16px".to_string())),
182        );
183        assert_eq!(invalid.matched_value_count, 0);
184        assert_eq!(invalid.unmatched_value_count, 1);
185        assert_eq!(
186            invalid.computed_value.status,
187            ComputedCascadeValueStatusV0::InvalidAtComputedValueTime
188        );
189        assert_eq!(
190            invalid.computed_value.value,
191            CascadeValue::Literal("8px".to_string())
192        );
193        assert!(invalid.computed_value.invalid_at_computed_value_time);
194
195        let absent = summarize_omena_query_registered_custom_property_computed_value_v0(
196            "tokens.css",
197            source,
198            ".absent",
199            "--gap",
200            Some(CascadeValue::Literal("16px".to_string())),
201        );
202        assert_eq!(
203            absent.computed_value.status,
204            ComputedCascadeValueStatusV0::Initial
205        );
206        assert_eq!(
207            absent.computed_value.value,
208            CascadeValue::Literal("8px".to_string())
209        );
210    }
211
212    #[test]
213    fn indeterminate_registered_syntax_has_a_typed_reason() {
214        let source = r#"
215@property --gap {
216  syntax: '<length>';
217  inherits: false;
218  initial-value: 8px;
219}
220.deferred { --gap: var(--runtime-value); }
221"#;
222
223        let result = summarize_omena_query_registered_custom_property_computed_value_v0(
224            "tokens.css",
225            source,
226            ".deferred",
227            "--gap",
228            None,
229        );
230
231        assert_eq!(result.unknown_value_count, 1);
232        assert_eq!(
233            result.computed_value.status,
234            ComputedCascadeValueStatusV0::Indeterminate
235        );
236        assert_eq!(result.computed_value.value, CascadeValue::Indeterminate);
237        assert!(!result.computed_value.invalid_at_computed_value_time);
238        assert_eq!(
239            result.computed_value.indeterminate_reason,
240            Some(ComputedCascadeIndeterminateReasonV0::RegisteredPropertySyntaxIndeterminate)
241        );
242    }
243
244    #[test]
245    fn unregistered_custom_properties_preserve_the_existing_computed_path() {
246        let report = summarize_omena_query_registered_custom_property_computed_value_v0(
247            "tokens.css",
248            ".target { --legacy-gap: 12px; }",
249            ".target",
250            "--legacy-gap",
251            Some(CascadeValue::Literal("16px".to_string())),
252        );
253
254        assert!(!report.registration_applied);
255        assert!(report.registration_projection_complete);
256        assert_eq!(report.matched_value_count, 0);
257        assert_eq!(report.unmatched_value_count, 0);
258        assert_eq!(report.unknown_value_count, 0);
259        assert_eq!(
260            report.computed_value.status,
261            ComputedCascadeValueStatusV0::Resolved
262        );
263        assert_eq!(
264            report.computed_value.value,
265            CascadeValue::Literal("12px".to_string())
266        );
267        assert_eq!(
268            report.computed_value.derivation_steps,
269            vec![
270                "cascadeWinnerSelected",
271                "computedValueResolutionStarted",
272                "computedValueResolved",
273            ]
274        );
275    }
276}