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    });
122
123    OmenaQueryRegisteredCustomPropertyComputedValueV0 {
124        schema_version: "0",
125        product: "omena-query.registered-custom-property-computed-value",
126        style_uri: style_uri.to_string(),
127        selector: selector.to_string(),
128        property: property.to_string(),
129        registration_applied,
130        registration_projection_complete,
131        matched_value_count,
132        unmatched_value_count,
133        unknown_value_count,
134        computed_value,
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use omena_cascade::{ComputedCascadeIndeterminateReasonV0, ComputedCascadeValueStatusV0};
142
143    #[test]
144    fn registered_properties_use_typed_syntax_inheritance_and_initial_values() {
145        let source = r#"
146@property --gap {
147  syntax: '<length>';
148  inherits: false;
149  initial-value: 8px;
150}
151.valid { --gap: 12px; }
152.invalid { --gap: red; }
153"#;
154
155        let valid = summarize_omena_query_registered_custom_property_computed_value_v0(
156            "tokens.css",
157            source,
158            ".valid",
159            "--gap",
160            Some(CascadeValue::Literal("16px".to_string())),
161        );
162        assert!(valid.registration_applied);
163        assert!(valid.registration_projection_complete);
164        assert_eq!(valid.matched_value_count, 1);
165        assert_eq!(valid.unmatched_value_count, 0);
166        assert_eq!(
167            valid.computed_value.status,
168            ComputedCascadeValueStatusV0::Resolved
169        );
170        assert_eq!(
171            valid.computed_value.value,
172            CascadeValue::Literal("12px".to_string())
173        );
174
175        let invalid = summarize_omena_query_registered_custom_property_computed_value_v0(
176            "tokens.css",
177            source,
178            ".invalid",
179            "--gap",
180            Some(CascadeValue::Literal("16px".to_string())),
181        );
182        assert_eq!(invalid.matched_value_count, 0);
183        assert_eq!(invalid.unmatched_value_count, 1);
184        assert_eq!(
185            invalid.computed_value.status,
186            ComputedCascadeValueStatusV0::InvalidAtComputedValueTime
187        );
188        assert_eq!(
189            invalid.computed_value.value,
190            CascadeValue::Literal("8px".to_string())
191        );
192        assert!(invalid.computed_value.invalid_at_computed_value_time);
193
194        let absent = summarize_omena_query_registered_custom_property_computed_value_v0(
195            "tokens.css",
196            source,
197            ".absent",
198            "--gap",
199            Some(CascadeValue::Literal("16px".to_string())),
200        );
201        assert_eq!(
202            absent.computed_value.status,
203            ComputedCascadeValueStatusV0::Initial
204        );
205        assert_eq!(
206            absent.computed_value.value,
207            CascadeValue::Literal("8px".to_string())
208        );
209    }
210
211    #[test]
212    fn indeterminate_registered_syntax_has_a_typed_reason() {
213        let source = r#"
214@property --gap {
215  syntax: '<length>';
216  inherits: false;
217  initial-value: 8px;
218}
219.deferred { --gap: var(--runtime-value); }
220"#;
221
222        let result = summarize_omena_query_registered_custom_property_computed_value_v0(
223            "tokens.css",
224            source,
225            ".deferred",
226            "--gap",
227            None,
228        );
229
230        assert_eq!(result.unknown_value_count, 1);
231        assert_eq!(
232            result.computed_value.status,
233            ComputedCascadeValueStatusV0::Indeterminate
234        );
235        assert_eq!(result.computed_value.value, CascadeValue::Indeterminate);
236        assert!(!result.computed_value.invalid_at_computed_value_time);
237        assert_eq!(
238            result.computed_value.indeterminate_reason,
239            Some(ComputedCascadeIndeterminateReasonV0::RegisteredPropertySyntaxIndeterminate)
240        );
241    }
242
243    #[test]
244    fn unregistered_custom_properties_preserve_the_existing_computed_path() {
245        let report = summarize_omena_query_registered_custom_property_computed_value_v0(
246            "tokens.css",
247            ".target { --legacy-gap: 12px; }",
248            ".target",
249            "--legacy-gap",
250            Some(CascadeValue::Literal("16px".to_string())),
251        );
252
253        assert!(!report.registration_applied);
254        assert!(report.registration_projection_complete);
255        assert_eq!(report.matched_value_count, 0);
256        assert_eq!(report.unmatched_value_count, 0);
257        assert_eq!(report.unknown_value_count, 0);
258        assert_eq!(
259            report.computed_value.status,
260            ComputedCascadeValueStatusV0::Resolved
261        );
262        assert_eq!(
263            report.computed_value.value,
264            CascadeValue::Literal("12px".to_string())
265        );
266        assert_eq!(
267            report.computed_value.derivation_steps,
268            vec![
269                "cascadeWinnerSelected",
270                "computedValueResolutionStarted",
271                "computedValueResolved",
272            ]
273        );
274    }
275}