1use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
2use std::sync::{Arc, OnceLock, RwLock};
3
4use omena_cascade::{CascadeStandardValueValidatorV0, CascadeStandardValueVerdictV0};
5use omena_evidence_graph::{
6 EvidenceNodeKeyV0, EvidenceNodeSeedV0, ExternalToolRunWitnessV0, FamilyStampV0, GuaranteeKindV0,
7};
8use omena_spec_audit::{
9 SpecGrammarBoundaryClassificationV0, SpecGrammarRegistryV0, spec_grammar_registry,
10};
11use omena_syntax::ident::{
12 CanonicalStandardPropertyNameV0, PropertyNameV0, is_custom_property_name,
13};
14use omena_value_lattice::{
15 CssValueComponentKindV0, CssValueComponentV0, DeclarationValueLensV0, ValueNodeV0,
16 css_value_component_stream, declaration_value_lens, parse_numeric_value_with_unit,
17};
18use serde::{Deserialize, Serialize};
19use sha2::{Digest, Sha256};
20
21const CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE: &str =
22 include_str!("../data/closed-world-keyword-closure-certificate.json");
23const CLOSED_WORLD_BUILTIN_TOKEN_PROFILES_SOURCE: &str =
24 include_str!("../data/closed-world-builtin-token-profiles.json");
25
26use crate::{
27 AbstractCssTypedScalarValueV0, AbstractCssTypedValueV0, AbstractCssValueV0,
28 DeclaredNumericTypeV0, DeclaredValueKindV0, abstract_css_typed_scalar_from_text,
29 classify_registered_property_declared_value_v0,
30};
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
33#[serde(rename_all = "camelCase")]
34pub struct CssValueGrammarBudgetV0 {
35 pub max_match_steps: usize,
36 pub max_reference_depth: usize,
37 pub max_states: usize,
38}
39
40impl Default for CssValueGrammarBudgetV0 {
41 fn default() -> Self {
42 Self {
43 max_match_steps: 50_000,
44 max_reference_depth: 64,
45 max_states: 4_096,
46 }
47 }
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
51#[serde(rename_all = "camelCase")]
52pub enum CssValueGrammarBudgetKindV0 {
53 MatchSteps,
54 ReferenceDepth,
55 CandidateStates,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct CssValueGrammarLocusV0 {
61 pub start: usize,
62 pub end: usize,
63}
64
65#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
66#[serde(
67 tag = "kind",
68 rename_all = "camelCase",
69 rename_all_fields = "camelCase"
70)]
71pub enum CssValueGrammarVerdictV0 {
72 Matched {
73 grammar: String,
74 consumed_components: usize,
75 },
76 Unmatched {
77 grammar: String,
78 locus: CssValueGrammarLocusV0,
79 },
80 NotMatchedWithinBudget {
81 grammar: String,
82 locus: CssValueGrammarLocusV0,
83 budget: CssValueGrammarBudgetKindV0,
84 limit: usize,
85 reference: Option<String>,
86 },
87 GrammarDefect {
88 grammar: String,
89 offset: usize,
90 code: String,
91 detail: String,
92 },
93}
94
95#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
96#[serde(rename_all = "camelCase")]
97pub enum CssValueValidationClassV0 {
98 Valid,
99 Invalid,
100 NotValidatable,
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
104#[serde(rename_all = "camelCase")]
105pub enum CssValueValidationReasonV0 {
106 GrammarMatched,
107 GrammarUnmatched,
108 GrammarDefect,
109 MatchBudgetExhausted,
110 DeferredSubstitution,
111 VendorExtension,
112 ForwardTierGrammar,
113 UnvalidatedStandardFunction,
114 MatcherCoverageIncomplete,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
118#[serde(rename_all = "camelCase")]
119pub struct CssValueValidationV0 {
120 pub class: CssValueValidationClassV0,
121 pub reason: CssValueValidationReasonV0,
122 pub verdict: CssValueGrammarVerdictV0,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
126#[serde(rename_all = "camelCase")]
127pub struct CssValueValidationConsumerPolicyV0 {
128 pub consumer: &'static str,
129 pub matched: &'static str,
130 pub unmatched: &'static str,
131 pub forward_tier_unmatched: &'static str,
132 pub grammar_defect: &'static str,
133 pub budget_exhausted: &'static str,
134}
135
136pub const CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0: [CssValueValidationConsumerPolicyV0; 5] = [
137 CssValueValidationConsumerPolicyV0 {
138 consumer: "checker.registeredPropertyTypeMismatch",
139 matched: "accept",
140 unmatched: "diagnostic",
141 forward_tier_unmatched: "not-applicable",
142 grammar_defect: "silent",
143 budget_exhausted: "silent",
144 },
145 CssValueValidationConsumerPolicyV0 {
146 consumer: "checker.invalidPropertyValue",
147 matched: "accept",
148 unmatched: "diagnostic",
149 forward_tier_unmatched: "not-validatable",
150 grammar_defect: "silent",
151 budget_exhausted: "silent",
152 },
153 CssValueValidationConsumerPolicyV0 {
154 consumer: "cascade.postSubstitutionStandardProperty",
155 matched: "accept",
156 unmatched: "reject",
157 forward_tier_unmatched: "not-validatable",
158 grammar_defect: "unknown",
159 budget_exhausted: "unknown",
160 },
161 CssValueValidationConsumerPolicyV0 {
162 consumer: "scss.nativeCssFunctionParameter",
163 matched: "accept",
164 unmatched: "reject",
165 forward_tier_unmatched: "not-applicable",
166 grammar_defect: "unknown",
167 budget_exhausted: "unknown",
168 },
169 CssValueValidationConsumerPolicyV0 {
170 consumer: "scss.nativeCssFunctionReturn",
171 matched: "accept",
172 unmatched: "reject",
173 forward_tier_unmatched: "not-applicable",
174 grammar_defect: "unknown",
175 budget_exhausted: "unknown",
176 },
177];
178
179pub fn css_value_grammar_external_tool_evidence_v0(
182 tool_name: &str,
183 tool_version: &str,
184 input_digest: &str,
185 exit_status: i32,
186) -> EvidenceNodeSeedV0 {
187 let witness = ExternalToolRunWitnessV0 {
188 tool_name: tool_name.to_string(),
189 tool_version: tool_version.to_string(),
190 input_digest: input_digest.to_string(),
191 exit_status,
192 };
193 EvidenceNodeSeedV0::with_family(
194 EvidenceNodeKeyV0::new(
195 "omena-abstract-value.value-grammar-differential",
196 input_digest,
197 ),
198 vec![
199 format!("externalTool:{tool_name}"),
200 format!("toolVersion:{tool_version}"),
201 format!("exitStatus:{exit_status}"),
202 ],
203 GuaranteeKindV0::for_label_less_family(),
204 FamilyStampV0::external_tool(&witness),
205 )
206}
207
208#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
209#[serde(rename_all = "camelCase")]
210pub struct CssValueGrammarRegistryAuditV0 {
211 pub total_entry_count: usize,
212 pub parsed_entry_count: usize,
213 pub missing_syntax_count: usize,
214 pub grammar_defect_count: usize,
215 pub categories: Vec<CssValueGrammarCategoryAuditV0>,
216 pub defects: Vec<CssValueGrammarDefectV0>,
217}
218
219#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
220#[serde(rename_all = "camelCase")]
221pub struct CssValueGrammarCategoryAuditV0 {
222 pub category: String,
223 pub entry_count: usize,
224 pub parsed_entry_count: usize,
225 pub missing_syntax_count: usize,
226 pub grammar_defect_count: usize,
227}
228
229#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
230#[serde(rename_all = "camelCase")]
231pub struct CssValueGrammarDefectV0 {
232 pub category: String,
233 pub name: String,
234 pub offset: usize,
235 pub code: String,
236 pub detail: String,
237}
238
239#[derive(Debug, Clone, PartialEq)]
240pub struct CssValueGrammarTypedMatchV0<'a> {
241 pub verdict: CssValueGrammarVerdictV0,
242 pub abstract_value: AbstractCssValueV0,
243 pub projection: Option<CssValueTypedProjectionV0<'a>>,
244}
245
246#[derive(Debug, Clone, PartialEq)]
247pub struct CssValueTypedProjectionV0<'a> {
248 pub lattice: DeclarationValueLensV0<'a>,
249 pub scalar_leaves: Vec<AbstractCssTypedScalarValueV0>,
250}
251
252impl CssValueGrammarVerdictV0 {
253 pub const fn is_matched(&self) -> bool {
254 matches!(self, Self::Matched { .. })
255 }
256
257 pub const fn is_definite_mismatch(&self) -> bool {
258 matches!(self, Self::Unmatched { .. })
259 }
260
261 pub const fn is_validatable(&self) -> bool {
262 matches!(self, Self::Matched { .. } | Self::Unmatched { .. })
263 }
264}
265
266pub fn audit_css_value_grammar_registry_v0(
270 registry: &SpecGrammarRegistryV0,
271) -> CssValueGrammarRegistryAuditV0 {
272 let mut categories = Vec::new();
273 let mut defects = Vec::new();
274 let mut parsed_entry_count = 0usize;
275 let mut missing_syntax_count = 0usize;
276 for category in ["atrules", "functions", "properties", "selectors", "types"] {
277 let entries = registry.entries(category);
278 let mut category_parsed = 0usize;
279 let mut category_missing = 0usize;
280 let defect_start = defects.len();
281 for entry in entries {
282 let Some(grammar) = entry.syntax.as_deref() else {
283 category_missing += 1;
284 missing_syntax_count += 1;
285 continue;
286 };
287 match VdsParser::new(strip_matching_quotes(grammar.trim())).parse() {
288 Ok(_) => {
289 category_parsed += 1;
290 parsed_entry_count += 1;
291 }
292 Err(error) => defects.push(CssValueGrammarDefectV0 {
293 category: category.to_string(),
294 name: entry.name.clone(),
295 offset: error.offset,
296 code: error.code.to_string(),
297 detail: error.detail,
298 }),
299 }
300 }
301 categories.push(CssValueGrammarCategoryAuditV0 {
302 category: category.to_string(),
303 entry_count: entries.len(),
304 parsed_entry_count: category_parsed,
305 missing_syntax_count: category_missing,
306 grammar_defect_count: defects.len() - defect_start,
307 });
308 }
309 CssValueGrammarRegistryAuditV0 {
310 total_entry_count: registry.total_entry_count(),
311 parsed_entry_count,
312 missing_syntax_count,
313 grammar_defect_count: defects.len(),
314 categories,
315 defects,
316 }
317}
318
319pub fn match_standard_property_value_v0(property: &str, value: &str) -> CssValueGrammarVerdictV0 {
322 let property = PropertyNameV0::from_authored(property);
323 match_standard_property_value_with_coverage_v0(&property, value).0
324}
325
326fn match_standard_property_value_with_coverage_v0(
327 property: &PropertyNameV0,
328 value: &str,
329) -> (CssValueGrammarVerdictV0, bool) {
330 let canonical_property = property.canonical_name();
331 let registry = spec_grammar_registry();
332 let Some(entry) = registry.entry("properties", canonical_property) else {
333 return (
334 grammar_defect(
335 "",
336 0,
337 "unknownProperty",
338 format!("property {canonical_property:?} is absent from the pinned registry"),
339 ),
340 false,
341 );
342 };
343 let Some(grammar) = entry.syntax.as_deref() else {
344 return (
345 grammar_defect(
346 "",
347 0,
348 "missingPropertyGrammar",
349 format!("property {canonical_property:?} has no syntax in the pinned registry"),
350 ),
351 false,
352 );
353 };
354 let matcher_coverage_complete = standard_property_matcher_coverage_complete(property, registry);
355 if matches!(
356 classify_registered_property_declared_value_v0(value),
357 DeclaredValueKindV0::CssWide
358 ) {
359 return (
360 CssValueGrammarVerdictV0::Matched {
361 grammar: grammar.to_string(),
362 consumed_components: 1,
363 },
364 matcher_coverage_complete,
365 );
366 }
367 let components = match css_value_component_stream(value, 0) {
368 Ok(components) => components,
369 Err(error) => {
370 return (
371 grammar_defect(
372 grammar,
373 error.span.start,
374 "invalidValueTokenStream",
375 error.message,
376 ),
377 false,
378 );
379 }
380 };
381 let normalized = strip_matching_quotes(grammar.trim());
382 let expression = match cached_pinned_vds_expression(normalized) {
383 Ok(expression) => expression,
384 Err(error) => {
385 return (
386 grammar_defect(grammar, error.offset, error.code, error.detail),
387 false,
388 );
389 }
390 };
391 (
392 match_css_value_grammar_components_with_expression_v0(
393 grammar,
394 &components,
395 registry,
396 CssValueGrammarBudgetV0::default(),
397 expression.as_ref(),
398 true,
399 ),
400 matcher_coverage_complete,
401 )
402}
403
404pub fn match_registered_property_value_v0(syntax: &str, value: &str) -> CssValueGrammarVerdictV0 {
406 let grammar = strip_matching_quotes(syntax.trim()).trim();
407 if grammar == "*" {
408 return match css_value_component_stream(value, 0) {
409 Ok(components) => CssValueGrammarVerdictV0::Matched {
410 grammar: syntax.to_string(),
411 consumed_components: components.len(),
412 },
413 Err(error) => grammar_defect(
414 syntax,
415 error.span.start,
416 "invalidValueTokenStream",
417 error.message,
418 ),
419 };
420 }
421 if matches!(
422 classify_registered_property_declared_value_v0(value),
423 DeclaredValueKindV0::CssWide
424 ) {
425 return CssValueGrammarVerdictV0::Matched {
426 grammar: syntax.to_string(),
427 consumed_components: 1,
428 };
429 }
430 match_css_value_grammar_v0(
431 grammar,
432 value,
433 spec_grammar_registry(),
434 CssValueGrammarBudgetV0::default(),
435 )
436}
437
438pub fn validate_standard_property_value_v0(property: &str, value: &str) -> CssValueValidationV0 {
439 let property = PropertyNameV0::from_authored(property);
440 let canonical_property = property.canonical_name();
441 let registry = spec_grammar_registry();
442 let classification = registry
443 .entry("properties", canonical_property)
444 .map(|entry| entry.boundary.classification)
445 .unwrap_or(SpecGrammarBoundaryClassificationV0::InBoundary);
446 let (verdict, matcher_coverage_complete) =
447 match_standard_property_value_with_coverage_v0(&property, value);
448 let matcher_coverage_complete = matcher_coverage_complete
449 && standard_property_value_token_kinds_have_closure_authority(&property, value, registry);
450 let closed_world_token_kind_mismatch =
451 matches!(verdict, CssValueGrammarVerdictV0::Unmatched { .. })
452 && standard_property_closed_world_token_kind_mismatch(&property, value, registry);
453 adjudicate_css_value_validation_with_boundary(
454 value,
455 verdict,
456 classification,
457 matcher_coverage_complete,
458 closed_world_token_kind_mismatch,
459 )
460}
461
462#[derive(Debug, Clone, Copy, Default)]
465pub struct SpecStandardPropertyValueValidatorV0;
466
467impl CascadeStandardValueValidatorV0 for SpecStandardPropertyValueValidatorV0 {
468 fn validate_standard_property_value(
469 &self,
470 property: &PropertyNameV0,
471 value: &str,
472 ) -> CascadeStandardValueVerdictV0 {
473 match validate_standard_property_value_v0(property.canonical_name(), value).class {
474 CssValueValidationClassV0::Valid => CascadeStandardValueVerdictV0::Matched,
475 CssValueValidationClassV0::Invalid => CascadeStandardValueVerdictV0::Unmatched,
476 CssValueValidationClassV0::NotValidatable => CascadeStandardValueVerdictV0::Unknown,
477 }
478 }
479}
480
481pub fn validate_registered_property_value_v0(syntax: &str, value: &str) -> CssValueValidationV0 {
482 adjudicate_css_value_validation(value, match_registered_property_value_v0(syntax, value))
483}
484
485fn adjudicate_css_value_validation(
486 value: &str,
487 verdict: CssValueGrammarVerdictV0,
488) -> CssValueValidationV0 {
489 adjudicate_css_value_validation_with_boundary(
490 value,
491 verdict,
492 SpecGrammarBoundaryClassificationV0::InBoundary,
493 true,
494 false,
495 )
496}
497
498fn adjudicate_css_value_validation_with_boundary(
499 value: &str,
500 verdict: CssValueGrammarVerdictV0,
501 classification: SpecGrammarBoundaryClassificationV0,
502 matcher_coverage_complete: bool,
503 closed_world_token_kind_mismatch: bool,
504) -> CssValueValidationV0 {
505 let components = css_value_component_stream(value, 0).ok();
506 let has_unvalidated_standard_function = matches!(
507 &verdict,
508 CssValueGrammarVerdictV0::Unmatched { grammar, locus }
509 if classification == SpecGrammarBoundaryClassificationV0::InBoundary
510 && components.as_deref().is_some_and(|components| {
511 recognized_standard_functions_explain_unmatched_value(
512 grammar,
513 components,
514 *locus,
515 )
516 })
517 );
518 let (class, reason) = if components
519 .as_deref()
520 .is_some_and(contains_deferred_css_value)
521 {
522 (
523 CssValueValidationClassV0::NotValidatable,
524 CssValueValidationReasonV0::DeferredSubstitution,
525 )
526 } else if components
527 .as_deref()
528 .is_some_and(has_leading_vendor_identifier)
529 {
530 (
531 CssValueValidationClassV0::NotValidatable,
532 CssValueValidationReasonV0::VendorExtension,
533 )
534 } else if has_unvalidated_standard_function {
535 (
536 CssValueValidationClassV0::NotValidatable,
537 CssValueValidationReasonV0::UnvalidatedStandardFunction,
538 )
539 } else {
540 match verdict {
541 CssValueGrammarVerdictV0::Matched { .. } => (
542 CssValueValidationClassV0::Valid,
543 CssValueValidationReasonV0::GrammarMatched,
544 ),
545 CssValueGrammarVerdictV0::Unmatched { .. }
546 if classification == SpecGrammarBoundaryClassificationV0::ForwardTier =>
547 {
548 (
549 CssValueValidationClassV0::NotValidatable,
550 CssValueValidationReasonV0::ForwardTierGrammar,
551 )
552 }
553 CssValueGrammarVerdictV0::Unmatched { .. }
554 if !matcher_coverage_complete && !closed_world_token_kind_mismatch =>
555 {
556 (
557 CssValueValidationClassV0::NotValidatable,
558 CssValueValidationReasonV0::MatcherCoverageIncomplete,
559 )
560 }
561 CssValueGrammarVerdictV0::Unmatched { .. } => (
562 CssValueValidationClassV0::Invalid,
563 CssValueValidationReasonV0::GrammarUnmatched,
564 ),
565 CssValueGrammarVerdictV0::NotMatchedWithinBudget { .. } => (
566 CssValueValidationClassV0::NotValidatable,
567 CssValueValidationReasonV0::MatchBudgetExhausted,
568 ),
569 CssValueGrammarVerdictV0::GrammarDefect { .. } => (
570 CssValueValidationClassV0::NotValidatable,
571 CssValueValidationReasonV0::GrammarDefect,
572 ),
573 }
574 };
575 CssValueValidationV0 {
576 class,
577 reason,
578 verdict,
579 }
580}
581
582fn recognized_standard_functions_explain_unmatched_value(
583 grammar: &str,
584 components: &[CssValueComponentV0],
585 locus: CssValueGrammarLocusV0,
586) -> bool {
587 let has_locus_function = components.iter().any(|component| {
588 component.span.start < locus.end
589 && locus.start < component.span.end
590 && component_is_recognized_standard_function(component)
591 });
592 if !has_locus_function {
593 return false;
594 }
595
596 let normalized = strip_matching_quotes(grammar.trim());
597 let Ok(expression) = cached_pinned_vds_expression(normalized) else {
598 return false;
599 };
600 let mut context = MatchContext {
601 registry: spec_grammar_registry(),
602 budget: CssValueGrammarBudgetV0::default(),
603 match_steps: 0,
604 first_stop: None,
605 grammar_cache: HashMap::new(),
606 cache_registered_grammars: true,
607 allow_unvalidated_standard_function_references: true,
608 };
609 context
610 .match_expression(expression.as_ref(), components, 0, 0)
611 .contains(&components.len())
612}
613
614fn component_is_recognized_standard_function(component: &CssValueComponentV0) -> bool {
615 matches!(
616 &component.kind,
617 CssValueComponentKindV0::Function { name, .. }
618 if recognized_standard_function_names().contains(name)
619 )
620}
621
622fn recognized_standard_function_names() -> &'static BTreeSet<String> {
623 static NAMES: OnceLock<BTreeSet<String>> = OnceLock::new();
624 NAMES.get_or_init(|| {
625 spec_grammar_registry()
626 .entries("functions")
627 .iter()
628 .filter(|entry| {
629 entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
630 })
631 .filter_map(|entry| entry.name.strip_suffix("()"))
632 .filter(|name| !name.starts_with('-') && !is_deferred_css_function_name(name))
633 .map(str::to_string)
634 .collect()
635 })
636}
637
638fn is_deferred_css_function_name(name: &str) -> bool {
639 matches!(name, "var" | "env" | "attr")
640}
641
642fn contains_deferred_css_value(components: &[CssValueComponentV0]) -> bool {
643 components.iter().any(|component| match &component.kind {
644 CssValueComponentKindV0::Function { name, arguments } => {
645 is_deferred_css_function_name(name) || contains_deferred_css_value(arguments)
646 }
647 CssValueComponentKindV0::Parenthesized { values }
648 | CssValueComponentKindV0::Bracketed { values }
649 | CssValueComponentKindV0::Braced { values } => contains_deferred_css_value(values),
650 CssValueComponentKindV0::Ident
651 | CssValueComponentKindV0::Number
652 | CssValueComponentKindV0::Percentage
653 | CssValueComponentKindV0::Dimension
654 | CssValueComponentKindV0::Hash
655 | CssValueComponentKindV0::String
656 | CssValueComponentKindV0::Url
657 | CssValueComponentKindV0::Comma
658 | CssValueComponentKindV0::Slash
659 | CssValueComponentKindV0::Delimiter => false,
660 })
661}
662
663fn has_leading_vendor_identifier(components: &[CssValueComponentV0]) -> bool {
664 components.first().is_some_and(|component| {
665 matches!(component.kind, CssValueComponentKindV0::Ident) && component.text.starts_with('-')
666 })
667}
668
669pub fn match_and_type_standard_property_value_v0<'a>(
672 property: &str,
673 value: &'a str,
674) -> CssValueGrammarTypedMatchV0<'a> {
675 typed_match_result(match_standard_property_value_v0(property, value), value)
676}
677
678pub fn match_and_type_css_value_grammar_v0<'a>(
680 grammar: &str,
681 value: &'a str,
682 registry: &SpecGrammarRegistryV0,
683 budget: CssValueGrammarBudgetV0,
684) -> CssValueGrammarTypedMatchV0<'a> {
685 typed_match_result(
686 match_css_value_grammar_v0(grammar, value, registry, budget),
687 value,
688 )
689}
690
691fn typed_match_result<'a>(
692 verdict: CssValueGrammarVerdictV0,
693 value: &'a str,
694) -> CssValueGrammarTypedMatchV0<'a> {
695 if !verdict.is_matched() {
696 return CssValueGrammarTypedMatchV0 {
697 verdict,
698 abstract_value: AbstractCssValueV0::Raw {
699 value: value.to_string(),
700 },
701 projection: None,
702 };
703 }
704 let components = match css_value_component_stream(value, 0) {
705 Ok(components) => components,
706 Err(error) => {
707 return CssValueGrammarTypedMatchV0 {
708 verdict: grammar_defect(
709 verdict_grammar(&verdict),
710 error.span.start,
711 "typedProjectionTokenStreamDrift",
712 error.message,
713 ),
714 abstract_value: AbstractCssValueV0::Raw {
715 value: value.to_string(),
716 },
717 projection: None,
718 };
719 }
720 };
721 let mut scalar_leaves = Vec::new();
722 collect_typed_scalar_leaves(&components, &mut scalar_leaves);
723 let lattice = declaration_value_lens(value, 0);
724 let typed = typed_value_from_projection(&lattice, &scalar_leaves).map(Box::new);
725 CssValueGrammarTypedMatchV0 {
726 verdict,
727 abstract_value: AbstractCssValueV0::Exact {
728 value: value.to_string(),
729 typed,
730 },
731 projection: Some(CssValueTypedProjectionV0 {
732 lattice,
733 scalar_leaves,
734 }),
735 }
736}
737
738fn verdict_grammar(verdict: &CssValueGrammarVerdictV0) -> &str {
739 match verdict {
740 CssValueGrammarVerdictV0::Matched { grammar, .. }
741 | CssValueGrammarVerdictV0::Unmatched { grammar, .. }
742 | CssValueGrammarVerdictV0::NotMatchedWithinBudget { grammar, .. }
743 | CssValueGrammarVerdictV0::GrammarDefect { grammar, .. } => grammar,
744 }
745}
746
747fn collect_typed_scalar_leaves(
748 components: &[CssValueComponentV0],
749 leaves: &mut Vec<AbstractCssTypedScalarValueV0>,
750) {
751 for component in components {
752 if let Some(value) = abstract_css_typed_scalar_from_text(component.text.as_str()) {
753 leaves.push(value);
754 continue;
755 }
756 match &component.kind {
757 CssValueComponentKindV0::Function { arguments, .. }
758 | CssValueComponentKindV0::Parenthesized { values: arguments }
759 | CssValueComponentKindV0::Bracketed { values: arguments }
760 | CssValueComponentKindV0::Braced { values: arguments } => {
761 collect_typed_scalar_leaves(arguments, leaves);
762 }
763 CssValueComponentKindV0::Ident
764 | CssValueComponentKindV0::Number
765 | CssValueComponentKindV0::Percentage
766 | CssValueComponentKindV0::Dimension
767 | CssValueComponentKindV0::Hash
768 | CssValueComponentKindV0::String
769 | CssValueComponentKindV0::Url
770 | CssValueComponentKindV0::Comma
771 | CssValueComponentKindV0::Slash
772 | CssValueComponentKindV0::Delimiter => {}
773 }
774 }
775}
776
777fn typed_value_from_projection(
778 lattice: &DeclarationValueLensV0<'_>,
779 scalar_leaves: &[AbstractCssTypedScalarValueV0],
780) -> Option<AbstractCssTypedValueV0> {
781 match (lattice.root(), scalar_leaves) {
782 (ValueNodeV0::List { .. } | ValueNodeV0::Function { .. }, [_, ..]) | (_, [_, _, ..]) => {
783 Some(AbstractCssTypedValueV0::Compound {
784 leaves: scalar_leaves.to_vec(),
785 })
786 }
787 (_, [value]) => Some(AbstractCssTypedValueV0::Exact {
788 value: value.clone(),
789 }),
790 (_, []) => None,
791 }
792}
793
794pub fn match_css_value_grammar_v0(
796 grammar: &str,
797 value: &str,
798 registry: &SpecGrammarRegistryV0,
799 budget: CssValueGrammarBudgetV0,
800) -> CssValueGrammarVerdictV0 {
801 let components = match css_value_component_stream(value, 0) {
802 Ok(components) => components,
803 Err(error) => {
804 return grammar_defect(
805 grammar,
806 error.span.start,
807 "invalidValueTokenStream",
808 error.message,
809 );
810 }
811 };
812 match_css_value_grammar_components_v0(grammar, &components, registry, budget)
813}
814
815pub fn match_css_value_grammar_components_v0(
817 grammar: &str,
818 components: &[CssValueComponentV0],
819 registry: &SpecGrammarRegistryV0,
820 budget: CssValueGrammarBudgetV0,
821) -> CssValueGrammarVerdictV0 {
822 let normalized = strip_matching_quotes(grammar.trim());
823 let expression = match VdsParser::new(normalized).parse() {
824 Ok(expression) => expression,
825 Err(error) => {
826 return grammar_defect(grammar, error.offset, error.code, error.detail);
827 }
828 };
829 match_css_value_grammar_components_with_expression_v0(
830 grammar,
831 components,
832 registry,
833 budget,
834 &expression,
835 false,
836 )
837}
838
839fn match_css_value_grammar_components_with_expression_v0(
840 grammar: &str,
841 components: &[CssValueComponentV0],
842 registry: &SpecGrammarRegistryV0,
843 budget: CssValueGrammarBudgetV0,
844 expression: &VdsExpression,
845 cache_registered_grammars: bool,
846) -> CssValueGrammarVerdictV0 {
847 let locus = component_locus(components);
848 let mut context = MatchContext {
849 registry,
850 budget,
851 match_steps: 0,
852 first_stop: None,
853 grammar_cache: HashMap::new(),
854 cache_registered_grammars,
855 allow_unvalidated_standard_function_references: false,
856 };
857 let ends = context.match_expression(expression, components, 0, 0);
858 if ends.contains(&components.len()) {
859 return CssValueGrammarVerdictV0::Matched {
860 grammar: grammar.to_string(),
861 consumed_components: components.len(),
862 };
863 }
864 if let Some(stop) = context.first_stop {
865 return match stop {
866 MatchStop::Budget {
867 kind,
868 limit,
869 reference,
870 } => CssValueGrammarVerdictV0::NotMatchedWithinBudget {
871 grammar: grammar.to_string(),
872 locus,
873 budget: kind,
874 limit,
875 reference,
876 },
877 MatchStop::GrammarDefect {
878 offset,
879 code,
880 detail,
881 } => grammar_defect(grammar, offset, code, detail),
882 };
883 }
884 CssValueGrammarVerdictV0::Unmatched {
885 grammar: grammar.to_string(),
886 locus,
887 }
888}
889
890#[derive(Debug, Clone, PartialEq, Eq)]
891enum VdsExpression {
892 Literal(String),
893 Reference(VdsReference),
894 Function {
895 name: String,
896 arguments: Box<VdsExpression>,
897 },
898 Sequence(Vec<VdsExpression>),
899 AllInAnyOrder(Vec<VdsExpression>),
900 OneOrMoreInAnyOrder(Vec<VdsExpression>),
901 Choice(Vec<VdsExpression>),
902 Repeat {
903 expression: Box<VdsExpression>,
904 min: usize,
905 max: Option<usize>,
906 comma_separated: bool,
907 },
908 Required(Box<VdsExpression>),
909}
910
911fn standard_property_matcher_coverage_complete(
912 property: &PropertyNameV0,
913 registry: &SpecGrammarRegistryV0,
914) -> bool {
915 let Some(grammar) = registry.syntax("properties", property.canonical_name()) else {
916 return false;
917 };
918 let Ok(expression) = cached_pinned_vds_expression(strip_matching_quotes(grammar.trim())) else {
919 return false;
920 };
921 let mut visiting = HashSet::new();
922 let mut memo = HashMap::new();
923 expression_matcher_coverage_complete(expression.as_ref(), registry, &mut visiting, &mut memo)
924}
925
926fn expression_matcher_coverage_complete(
927 expression: &VdsExpression,
928 registry: &SpecGrammarRegistryV0,
929 visiting: &mut HashSet<(ReferenceCategory, String)>,
930 memo: &mut HashMap<(ReferenceCategory, String), bool>,
931) -> bool {
932 match expression {
933 VdsExpression::Literal(_) => true,
934 VdsExpression::Reference(reference) => {
935 if is_builtin_reference_name(reference.name.as_str()) {
936 return builtin_reference_matcher_coverage_complete(reference.name.as_str());
937 }
938 let key = (reference.category, reference.name.clone());
939 if let Some(complete) = memo.get(&key) {
940 return *complete;
941 }
942 if !visiting.insert(key.clone()) {
943 return true;
944 }
945 let category = match reference.category {
946 ReferenceCategory::Type => "types",
947 ReferenceCategory::Property => "properties",
948 ReferenceCategory::Function => "functions",
949 };
950 let complete = registry
951 .syntax(category, reference.name.as_str())
952 .and_then(|source| cached_pinned_vds_expression(source).ok())
953 .is_some_and(|expression| {
954 expression_matcher_coverage_complete(
955 expression.as_ref(),
956 registry,
957 visiting,
958 memo,
959 )
960 });
961 visiting.remove(&key);
962 memo.insert(key, complete);
963 complete
964 }
965 VdsExpression::Function { arguments, .. }
966 | VdsExpression::Repeat {
967 expression: arguments,
968 ..
969 }
970 | VdsExpression::Required(arguments) => {
971 expression_matcher_coverage_complete(arguments, registry, visiting, memo)
972 }
973 VdsExpression::Sequence(expressions)
974 | VdsExpression::AllInAnyOrder(expressions)
975 | VdsExpression::OneOrMoreInAnyOrder(expressions)
976 | VdsExpression::Choice(expressions) => expressions.iter().all(|expression| {
977 expression_matcher_coverage_complete(expression, registry, visiting, memo)
978 }),
979 }
980}
981
982fn builtin_reference_matcher_coverage_complete(name: &str) -> bool {
983 matches!(
990 name,
991 "declaration-value"
992 | "any-value"
993 | "whole-value"
994 | "number-token"
995 | "percentage-token"
996 | "ident"
997 | "ident-token"
998 | "dashed-ident"
999 | "custom-property-name"
1000 | "string"
1001 | "string-token"
1002 | "url"
1003 | "url-token"
1004 | "hex-color"
1005 | "zero"
1006 | "dimension-token"
1007 | "hash-token"
1008 | "function-token"
1009 | "comma-token"
1010 )
1011}
1012
1013#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1014enum ClosedWorldTokenKindV0 {
1015 Ident,
1016 Hash,
1017 Dimension,
1018 Number,
1019 Percentage,
1020 FunctionName,
1021 String,
1022 Url,
1023}
1024
1025#[derive(Debug, Clone, Default, PartialEq, Eq)]
1026struct ClosedWorldTokenDomainV0 {
1027 open: bool,
1028 allowed: BTreeSet<String>,
1029}
1030
1031#[derive(Debug, Clone, Default, PartialEq, Eq)]
1032struct ClosedWorldTokenProfileV0 {
1033 ident: ClosedWorldTokenDomainV0,
1034 hash: ClosedWorldTokenDomainV0,
1035 dimension: ClosedWorldTokenDomainV0,
1036 number: ClosedWorldTokenDomainV0,
1037 percentage: ClosedWorldTokenDomainV0,
1038 function_name: ClosedWorldTokenDomainV0,
1039 string: ClosedWorldTokenDomainV0,
1040 url: ClosedWorldTokenDomainV0,
1041}
1042
1043#[derive(Debug, Deserialize)]
1044#[serde(rename_all = "camelCase")]
1045struct ClosedWorldKeywordClosureCertificateV0 {
1046 schema_version: String,
1047 product: String,
1048 oracle: ClosedWorldKeywordClosureOracleV0,
1049 source: String,
1050 maximum_reference_depth: usize,
1051 property_count: usize,
1052 candidate_pair_count: usize,
1053 accepted_pair_count: usize,
1054 matched_pair_count: usize,
1055 matcher_gap_count: usize,
1056 accepted_pair_digest: String,
1057 certified_properties: BTreeSet<CanonicalStandardPropertyNameV0>,
1058 property_tests: Vec<ClosedWorldKeywordClosurePropertyTestV0>,
1059}
1060
1061#[derive(Debug, Deserialize)]
1062#[serde(rename_all = "camelCase")]
1063struct ClosedWorldKeywordClosurePropertyTestV0 {
1064 property: CanonicalStandardPropertyNameV0,
1065 candidate_pair_count: usize,
1066 tested_pair_count: usize,
1067 matched_pair_count: usize,
1068 matcher_gap_count: usize,
1069 accepted_keywords: Vec<String>,
1070}
1071
1072#[derive(Debug, Default, PartialEq, Eq)]
1073struct ClosedWorldKeywordAuthorityV0 {
1074 certified_properties: BTreeSet<CanonicalStandardPropertyNameV0>,
1075 accepted_keywords_by_property: BTreeMap<CanonicalStandardPropertyNameV0, BTreeSet<String>>,
1076}
1077
1078#[derive(Debug, Deserialize)]
1079struct ClosedWorldKeywordClosureOracleV0 {
1080 name: ClosedWorldKeywordClosureOracleNameV0,
1081 version: String,
1082}
1083
1084#[derive(Debug, Deserialize, PartialEq, Eq)]
1085enum ClosedWorldKeywordClosureOracleNameV0 {
1086 #[serde(rename = "css-tree")]
1087 CssTree,
1088}
1089
1090#[derive(Debug, Deserialize)]
1091#[serde(rename_all = "camelCase")]
1092struct ClosedWorldBuiltinTokenProfilesV0 {
1093 schema_version: String,
1094 product: String,
1095 oracle: ClosedWorldKeywordClosureOracleV0,
1096 profile_count: usize,
1097 profiles: Vec<ClosedWorldBuiltinTokenProfileV0>,
1098}
1099
1100#[derive(Debug, Deserialize)]
1101#[serde(rename_all = "camelCase")]
1102struct ClosedWorldBuiltinTokenProfileV0 {
1103 name: String,
1104 authority: String,
1105 open_token_kinds: BTreeSet<String>,
1106 allowed_values: HashMap<String, Vec<String>>,
1107}
1108
1109impl ClosedWorldTokenProfileV0 {
1110 fn domain(&self, kind: ClosedWorldTokenKindV0) -> &ClosedWorldTokenDomainV0 {
1111 match kind {
1112 ClosedWorldTokenKindV0::Ident => &self.ident,
1113 ClosedWorldTokenKindV0::Hash => &self.hash,
1114 ClosedWorldTokenKindV0::Dimension => &self.dimension,
1115 ClosedWorldTokenKindV0::Number => &self.number,
1116 ClosedWorldTokenKindV0::Percentage => &self.percentage,
1117 ClosedWorldTokenKindV0::FunctionName => &self.function_name,
1118 ClosedWorldTokenKindV0::String => &self.string,
1119 ClosedWorldTokenKindV0::Url => &self.url,
1120 }
1121 }
1122
1123 fn domain_mut(&mut self, kind: ClosedWorldTokenKindV0) -> &mut ClosedWorldTokenDomainV0 {
1124 match kind {
1125 ClosedWorldTokenKindV0::Ident => &mut self.ident,
1126 ClosedWorldTokenKindV0::Hash => &mut self.hash,
1127 ClosedWorldTokenKindV0::Dimension => &mut self.dimension,
1128 ClosedWorldTokenKindV0::Number => &mut self.number,
1129 ClosedWorldTokenKindV0::Percentage => &mut self.percentage,
1130 ClosedWorldTokenKindV0::FunctionName => &mut self.function_name,
1131 ClosedWorldTokenKindV0::String => &mut self.string,
1132 ClosedWorldTokenKindV0::Url => &mut self.url,
1133 }
1134 }
1135
1136 fn allow(&mut self, kind: ClosedWorldTokenKindV0, value: &str) {
1137 self.domain_mut(kind)
1138 .allowed
1139 .insert(value.to_ascii_lowercase());
1140 }
1141
1142 fn mark_open(&mut self, kind: ClosedWorldTokenKindV0) {
1143 self.domain_mut(kind).open = true;
1144 }
1145
1146 fn mark_all_open(&mut self) {
1147 for kind in CLOSED_WORLD_TOKEN_KINDS {
1148 self.mark_open(kind);
1149 }
1150 }
1151
1152 fn merge(&mut self, other: Self) {
1153 for kind in CLOSED_WORLD_TOKEN_KINDS {
1154 let other_domain = other.domain(kind);
1155 let domain = self.domain_mut(kind);
1156 domain.open |= other_domain.open;
1157 domain.allowed.extend(other_domain.allowed.iter().cloned());
1158 }
1159 }
1160}
1161
1162const CLOSED_WORLD_TOKEN_KINDS: [ClosedWorldTokenKindV0; 8] = [
1163 ClosedWorldTokenKindV0::Ident,
1164 ClosedWorldTokenKindV0::Hash,
1165 ClosedWorldTokenKindV0::Dimension,
1166 ClosedWorldTokenKindV0::Number,
1167 ClosedWorldTokenKindV0::Percentage,
1168 ClosedWorldTokenKindV0::FunctionName,
1169 ClosedWorldTokenKindV0::String,
1170 ClosedWorldTokenKindV0::Url,
1171];
1172
1173fn certified_keyword_properties() -> &'static BTreeSet<CanonicalStandardPropertyNameV0> {
1174 static EMPTY: OnceLock<BTreeSet<CanonicalStandardPropertyNameV0>> = OnceLock::new();
1175 closed_world_keyword_authority()
1176 .map(|authority| &authority.certified_properties)
1177 .unwrap_or_else(|| EMPTY.get_or_init(BTreeSet::new))
1178}
1179
1180fn closed_world_keyword_authority() -> Option<&'static ClosedWorldKeywordAuthorityV0> {
1181 static AUTHORITY: OnceLock<Option<ClosedWorldKeywordAuthorityV0>> = OnceLock::new();
1182 AUTHORITY
1183 .get_or_init(|| {
1184 parse_closed_world_keyword_closure_certificate(
1185 CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
1186 )
1187 })
1188 .as_ref()
1189}
1190
1191fn parse_closed_world_keyword_closure_certificate(
1192 source: &str,
1193) -> Option<ClosedWorldKeywordAuthorityV0> {
1194 let certificate =
1195 serde_json::from_str::<ClosedWorldKeywordClosureCertificateV0>(source).ok()?;
1196 if certificate.schema_version != "0"
1197 || certificate.product != "omena-abstract-value.closed-world-keyword-closure-certificate"
1198 || certificate.oracle.name != ClosedWorldKeywordClosureOracleNameV0::CssTree
1199 || certificate.oracle.version != "3.2.1"
1200 || certificate.source != "cssTree.lexer.properties.typeAndPropertyReferenceClosure"
1201 || certificate.maximum_reference_depth != 12
1202 || certificate.property_count != 704
1203 || certificate.candidate_pair_count != 23_178
1204 || certificate.accepted_pair_count != 16_445
1205 || certificate.property_count != certificate.property_tests.len()
1206 {
1207 return None;
1208 }
1209
1210 let mut previous_property: Option<&CanonicalStandardPropertyNameV0> = None;
1211 let mut candidate_pair_count = 0usize;
1212 let mut accepted_pair_count = 0usize;
1213 let mut matched_pair_count = 0usize;
1214 let mut matcher_gap_count = 0usize;
1215 let mut derived_certified_properties = BTreeSet::new();
1216 let mut accepted_keywords_by_property = BTreeMap::new();
1217 let mut digest = Sha256::new();
1218
1219 for property_test in &certificate.property_tests {
1220 if property_test.property.as_str().is_empty()
1221 || previous_property.is_some_and(|previous| previous >= &property_test.property)
1222 || property_test.tested_pair_count != property_test.accepted_keywords.len()
1223 || property_test
1224 .matched_pair_count
1225 .checked_add(property_test.matcher_gap_count)
1226 != Some(property_test.tested_pair_count)
1227 || property_test.candidate_pair_count < property_test.tested_pair_count
1228 || property_test
1229 .accepted_keywords
1230 .windows(2)
1231 .any(|pair| pair[0] >= pair[1])
1232 || property_test
1233 .accepted_keywords
1234 .iter()
1235 .any(|keyword| keyword.is_empty())
1236 {
1237 return None;
1238 }
1239 previous_property = Some(&property_test.property);
1240 candidate_pair_count =
1241 candidate_pair_count.checked_add(property_test.candidate_pair_count)?;
1242 accepted_pair_count = accepted_pair_count.checked_add(property_test.tested_pair_count)?;
1243 matched_pair_count = matched_pair_count.checked_add(property_test.matched_pair_count)?;
1244 matcher_gap_count = matcher_gap_count.checked_add(property_test.matcher_gap_count)?;
1245
1246 for keyword in &property_test.accepted_keywords {
1247 digest.update(property_test.property.as_str().as_bytes());
1248 digest.update([0]);
1249 digest.update(keyword.as_bytes());
1250 digest.update([b'\n']);
1251 }
1252 if accepted_keywords_by_property
1253 .insert(
1254 property_test.property.clone(),
1255 property_test.accepted_keywords.iter().cloned().collect(),
1256 )
1257 .is_some()
1258 {
1259 return None;
1260 }
1261 if property_test.tested_pair_count > 0 && property_test.matcher_gap_count == 0 {
1262 derived_certified_properties.insert(property_test.property.clone());
1263 }
1264 }
1265
1266 let accepted_pair_digest = digest
1267 .finalize()
1268 .iter()
1269 .map(|byte| format!("{byte:02x}"))
1270 .collect::<String>();
1271 if certificate.candidate_pair_count != candidate_pair_count
1272 || certificate.accepted_pair_count != accepted_pair_count
1273 || certificate.matched_pair_count != matched_pair_count
1274 || certificate.matcher_gap_count != matcher_gap_count
1275 || certificate.accepted_pair_digest != accepted_pair_digest
1276 || certificate.certified_properties != derived_certified_properties
1277 {
1278 return None;
1279 }
1280
1281 Some(ClosedWorldKeywordAuthorityV0 {
1282 certified_properties: certificate.certified_properties,
1283 accepted_keywords_by_property,
1284 })
1285}
1286
1287fn standard_property_value_token_kinds_have_closure_authority(
1288 property: &PropertyNameV0,
1289 value: &str,
1290 registry: &SpecGrammarRegistryV0,
1291) -> bool {
1292 let Ok(components) = css_value_component_stream(value, 0) else {
1293 return false;
1294 };
1295 if components
1296 .iter()
1297 .any(|component| matches!(component.kind, CssValueComponentKindV0::Ident))
1298 {
1299 let Some(property_key) = property.as_standard_key() else {
1300 return false;
1301 };
1302 return certified_keyword_properties().contains(property_key);
1303 }
1304 standard_property_grammar_is_machine_imported_without_overrides(property, registry)
1305}
1306
1307fn standard_property_grammar_is_machine_imported_without_overrides(
1308 property: &PropertyNameV0,
1309 registry: &SpecGrammarRegistryV0,
1310) -> bool {
1311 let Some(entry) = registry.entry("properties", property.canonical_name()) else {
1312 return false;
1313 };
1314 let Some(source) = entry.syntax.as_deref() else {
1315 return false;
1316 };
1317 if entry.override_provenance.is_some() {
1318 return false;
1319 }
1320 let Ok(expression) = cached_pinned_vds_expression(strip_matching_quotes(source.trim())) else {
1321 return false;
1322 };
1323 let mut visiting = HashSet::new();
1324 expression_sources_are_machine_imported_without_overrides(
1325 expression.as_ref(),
1326 registry,
1327 &mut visiting,
1328 )
1329}
1330
1331fn expression_sources_are_machine_imported_without_overrides(
1332 expression: &VdsExpression,
1333 registry: &SpecGrammarRegistryV0,
1334 visiting: &mut HashSet<(ReferenceCategory, String)>,
1335) -> bool {
1336 match expression {
1337 VdsExpression::Literal(_) => true,
1338 VdsExpression::Reference(reference) => {
1339 if is_builtin_reference_name(reference.name.as_str()) {
1340 return closed_world_builtin_token_profiles().is_some_and(|manifest| {
1341 manifest
1342 .profiles
1343 .iter()
1344 .any(|profile| profile.name == reference.name)
1345 });
1346 }
1347 let key = (reference.category, reference.name.clone());
1348 if !visiting.insert(key.clone()) {
1349 return true;
1350 }
1351 let category = match reference.category {
1352 ReferenceCategory::Type => "types",
1353 ReferenceCategory::Property => "properties",
1354 ReferenceCategory::Function => "functions",
1355 };
1356 let imported = registry
1357 .entry(category, reference.name.as_str())
1358 .filter(|entry| entry.override_provenance.is_none())
1359 .and_then(|entry| entry.syntax.as_deref())
1360 .and_then(|source| cached_pinned_vds_expression(source).ok())
1361 .is_some_and(|expression| {
1362 expression_sources_are_machine_imported_without_overrides(
1363 expression.as_ref(),
1364 registry,
1365 visiting,
1366 )
1367 });
1368 visiting.remove(&key);
1369 imported
1370 }
1371 VdsExpression::Function { arguments, .. }
1372 | VdsExpression::Repeat {
1373 expression: arguments,
1374 ..
1375 }
1376 | VdsExpression::Required(arguments) => {
1377 expression_sources_are_machine_imported_without_overrides(arguments, registry, visiting)
1378 }
1379 VdsExpression::Sequence(expressions)
1380 | VdsExpression::AllInAnyOrder(expressions)
1381 | VdsExpression::OneOrMoreInAnyOrder(expressions)
1382 | VdsExpression::Choice(expressions) => expressions.iter().all(|expression| {
1383 expression_sources_are_machine_imported_without_overrides(
1384 expression, registry, visiting,
1385 )
1386 }),
1387 }
1388}
1389
1390fn standard_property_closed_world_token_kind_mismatch(
1391 property: &PropertyNameV0,
1392 value: &str,
1393 registry: &SpecGrammarRegistryV0,
1394) -> bool {
1395 let Ok(components) = css_value_component_stream(value, 0) else {
1396 return false;
1397 };
1398 let Some(profile) = cached_standard_property_closed_world_token_profile(property, registry)
1399 else {
1400 return false;
1401 };
1402 if components.iter().any(|component| {
1403 closed_world_component_identity(component).is_some_and(|(kind, value)| {
1404 let domain = profile.domain(kind);
1405 kind == ClosedWorldTokenKindV0::FunctionName
1406 && domain.open
1407 && !domain.allowed.contains(value.as_str())
1408 && !recognized_standard_function_names().contains(value.as_str())
1409 })
1410 }) {
1411 return false;
1412 }
1413 components.iter().any(|component| {
1414 let Some((kind, value)) = closed_world_component_identity(component) else {
1415 return false;
1416 };
1417 if kind == ClosedWorldTokenKindV0::Ident {
1418 let Some(property_key) = property.as_standard_key() else {
1419 return false;
1420 };
1421 let Some(accepted_keywords) = closed_world_keyword_authority()
1422 .and_then(|authority| authority.accepted_keywords_by_property.get(property_key))
1423 else {
1424 return false;
1425 };
1426 let domain = profile.domain(kind);
1427 return !domain.open && !accepted_keywords.contains(value.as_str());
1428 }
1429 let domain = profile.domain(kind);
1430 !domain.open && !domain.allowed.contains(value.as_str())
1431 })
1432}
1433
1434fn cached_standard_property_closed_world_token_profile(
1435 property: &PropertyNameV0,
1436 registry: &SpecGrammarRegistryV0,
1437) -> Option<ClosedWorldTokenProfileV0> {
1438 static CACHE: OnceLock<RwLock<HashMap<String, Option<ClosedWorldTokenProfileV0>>>> =
1439 OnceLock::new();
1440 let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
1441 let key = property.canonical_name().to_string();
1442 if let Some(profile) = cache
1443 .read()
1444 .unwrap_or_else(std::sync::PoisonError::into_inner)
1445 .get(&key)
1446 {
1447 return profile.clone();
1448 }
1449 let profile = registry
1450 .entry("properties", property.canonical_name())
1451 .and_then(|entry| entry.syntax.as_deref().map(|grammar| (entry, grammar)))
1452 .and_then(|(entry, grammar)| {
1453 cached_pinned_vds_expression(strip_matching_quotes(grammar.trim()))
1454 .ok()
1455 .map(|expression| (entry, expression))
1456 })
1457 .map(|(entry, expression)| {
1458 let mut ident_visiting = HashSet::new();
1459 let mut ident_memo = HashMap::new();
1460 let ident_open = expression_has_open_ident_production(
1461 expression.as_ref(),
1462 registry,
1463 &mut ident_visiting,
1464 &mut ident_memo,
1465 );
1466 let mut visiting = HashSet::new();
1467 let mut memo = HashMap::new();
1468 let mut profile =
1469 closed_world_token_profile(expression.as_ref(), registry, &mut visiting, &mut memo);
1470 if entry.override_provenance.is_some() {
1471 profile.mark_all_open();
1472 }
1473 profile.ident.open = ident_open;
1479 profile
1480 });
1481 cache
1482 .write()
1483 .unwrap_or_else(std::sync::PoisonError::into_inner)
1484 .entry(key)
1485 .or_insert_with(|| profile.clone());
1486 profile
1487}
1488
1489fn expression_has_open_ident_production(
1490 expression: &VdsExpression,
1491 registry: &SpecGrammarRegistryV0,
1492 visiting: &mut HashSet<(ReferenceCategory, String)>,
1493 memo: &mut HashMap<(ReferenceCategory, String), bool>,
1494) -> bool {
1495 match expression {
1496 VdsExpression::Literal(_) | VdsExpression::Function { .. } => false,
1497 VdsExpression::Reference(reference) => {
1498 reference_has_open_ident_production(reference, registry, visiting, memo)
1499 }
1500 VdsExpression::Sequence(expressions)
1501 | VdsExpression::AllInAnyOrder(expressions)
1502 | VdsExpression::OneOrMoreInAnyOrder(expressions)
1503 | VdsExpression::Choice(expressions) => expressions.iter().any(|expression| {
1504 expression_has_open_ident_production(expression, registry, visiting, memo)
1505 }),
1506 VdsExpression::Repeat { expression, .. } | VdsExpression::Required(expression) => {
1507 expression_has_open_ident_production(expression, registry, visiting, memo)
1508 }
1509 }
1510}
1511
1512fn reference_has_open_ident_production(
1513 reference: &VdsReference,
1514 registry: &SpecGrammarRegistryV0,
1515 visiting: &mut HashSet<(ReferenceCategory, String)>,
1516 memo: &mut HashMap<(ReferenceCategory, String), bool>,
1517) -> bool {
1518 if reference.category == ReferenceCategory::Function {
1519 return false;
1520 }
1521 if reference.category == ReferenceCategory::Type
1522 && let Some(profile) = closed_world_builtin_profile(reference.name.as_str())
1523 {
1524 return profile.ident.open;
1525 }
1526
1527 let key = (reference.category, reference.name.clone());
1528 if let Some(open) = memo.get(&key) {
1529 return *open;
1530 }
1531 if !visiting.insert(key.clone()) {
1532 return true;
1533 }
1534 let category = match reference.category {
1535 ReferenceCategory::Type => "types",
1536 ReferenceCategory::Property => "properties",
1537 ReferenceCategory::Function => unreachable!("function references return above"),
1538 };
1539 let open = registry
1540 .entry(category, reference.name.as_str())
1541 .and_then(|entry| entry.syntax.as_deref())
1542 .and_then(|source| cached_pinned_vds_expression(source).ok())
1543 .map(|expression| {
1544 expression_has_open_ident_production(expression.as_ref(), registry, visiting, memo)
1545 })
1546 .unwrap_or(true);
1547 visiting.remove(&key);
1548 memo.insert(key, open);
1549 open
1550}
1551
1552fn closed_world_token_profile(
1553 expression: &VdsExpression,
1554 registry: &SpecGrammarRegistryV0,
1555 visiting: &mut HashSet<(ReferenceCategory, String)>,
1556 memo: &mut HashMap<(ReferenceCategory, String), ClosedWorldTokenProfileV0>,
1557) -> ClosedWorldTokenProfileV0 {
1558 match expression {
1559 VdsExpression::Literal(literal) => closed_world_literal_profile(literal),
1560 VdsExpression::Reference(reference) => {
1561 closed_world_reference_profile(reference, registry, visiting, memo)
1562 }
1563 VdsExpression::Function { name, .. } => {
1564 let mut profile = ClosedWorldTokenProfileV0::default();
1565 profile.allow(ClosedWorldTokenKindV0::FunctionName, name);
1566 profile
1567 }
1568 VdsExpression::Sequence(expressions)
1569 | VdsExpression::AllInAnyOrder(expressions)
1570 | VdsExpression::OneOrMoreInAnyOrder(expressions)
1571 | VdsExpression::Choice(expressions) => {
1572 let mut profile = ClosedWorldTokenProfileV0::default();
1573 for expression in expressions {
1574 profile.merge(closed_world_token_profile(
1575 expression, registry, visiting, memo,
1576 ));
1577 }
1578 profile
1579 }
1580 VdsExpression::Repeat { expression, .. } | VdsExpression::Required(expression) => {
1581 closed_world_token_profile(expression, registry, visiting, memo)
1582 }
1583 }
1584}
1585
1586fn closed_world_reference_profile(
1587 reference: &VdsReference,
1588 registry: &SpecGrammarRegistryV0,
1589 visiting: &mut HashSet<(ReferenceCategory, String)>,
1590 memo: &mut HashMap<(ReferenceCategory, String), ClosedWorldTokenProfileV0>,
1591) -> ClosedWorldTokenProfileV0 {
1592 if reference.category == ReferenceCategory::Function {
1593 let mut profile = ClosedWorldTokenProfileV0::default();
1594 profile.allow(
1595 ClosedWorldTokenKindV0::FunctionName,
1596 reference.name.trim_end_matches("()"),
1597 );
1598 return profile;
1599 }
1600 if reference.category == ReferenceCategory::Type
1601 && let Some(profile) = closed_world_builtin_profile(reference.name.as_str())
1602 {
1603 return profile;
1604 }
1605
1606 let key = (reference.category, reference.name.clone());
1607 if let Some(profile) = memo.get(&key) {
1608 return profile.clone();
1609 }
1610 if !visiting.insert(key.clone()) {
1611 let mut profile = ClosedWorldTokenProfileV0::default();
1612 profile.mark_all_open();
1613 return profile;
1614 }
1615 let category = match reference.category {
1616 ReferenceCategory::Type => "types",
1617 ReferenceCategory::Property => "properties",
1618 ReferenceCategory::Function => unreachable!("function references return above"),
1619 };
1620 let mut profile = registry
1621 .entry(category, reference.name.as_str())
1622 .and_then(|entry| entry.syntax.as_deref().map(|source| (entry, source)))
1623 .and_then(|(entry, source)| {
1624 cached_pinned_vds_expression(source)
1625 .ok()
1626 .map(|expression| (entry, expression))
1627 })
1628 .map(|(entry, expression)| {
1629 let mut profile =
1630 closed_world_token_profile(expression.as_ref(), registry, visiting, memo);
1631 if entry.override_provenance.is_some() {
1632 profile.mark_all_open();
1633 }
1634 profile
1635 })
1636 .unwrap_or_else(|| {
1637 let mut unknown = ClosedWorldTokenProfileV0::default();
1638 unknown.mark_all_open();
1639 unknown
1640 });
1641 visiting.remove(&key);
1642 if reference.category == ReferenceCategory::Type
1643 && matches!(
1644 reference.name.as_str(),
1645 "number" | "integer" | "length" | "percentage" | "length-percentage" | "angle" | "time"
1646 )
1647 {
1648 profile.mark_open(ClosedWorldTokenKindV0::FunctionName);
1649 }
1650 memo.insert(key, profile.clone());
1651 profile
1652}
1653
1654fn closed_world_builtin_profile(name: &str) -> Option<ClosedWorldTokenProfileV0> {
1655 let known_builtin = is_builtin_reference_name(name)
1656 || matches!(name, "declaration-value" | "any-value" | "whole-value");
1657 if !known_builtin {
1658 return None;
1659 }
1660
1661 let Some(manifest) = closed_world_builtin_token_profiles() else {
1662 return Some(all_open_closed_world_token_profile());
1663 };
1664 let Some(witnessed) = manifest
1665 .profiles
1666 .iter()
1667 .find(|profile| profile.name == name)
1668 else {
1669 return Some(all_open_closed_world_token_profile());
1670 };
1671 if witnessed.authority == "registryDerived" {
1672 return None;
1673 }
1674 if !matches!(
1675 witnessed.authority.as_str(),
1676 "cssTreeWitness" | "defaultOpen"
1677 ) {
1678 return Some(all_open_closed_world_token_profile());
1679 }
1680
1681 let mut profile = ClosedWorldTokenProfileV0::default();
1682 for kind in &witnessed.open_token_kinds {
1683 let Some(kind) = closed_world_token_kind_from_data_name(kind) else {
1684 return Some(all_open_closed_world_token_profile());
1685 };
1686 profile.mark_open(kind);
1687 }
1688 for (kind, values) in &witnessed.allowed_values {
1689 let Some(kind) = closed_world_token_kind_from_data_name(kind) else {
1690 return Some(all_open_closed_world_token_profile());
1691 };
1692 for value in values {
1693 profile.allow(kind, value);
1694 }
1695 }
1696 if witnessed.authority == "defaultOpen"
1697 && CLOSED_WORLD_TOKEN_KINDS
1698 .iter()
1699 .any(|kind| !profile.domain(*kind).open)
1700 {
1701 return Some(all_open_closed_world_token_profile());
1702 }
1703 Some(profile)
1704}
1705
1706fn closed_world_builtin_token_profiles() -> Option<&'static ClosedWorldBuiltinTokenProfilesV0> {
1707 static PROFILES: OnceLock<Option<ClosedWorldBuiltinTokenProfilesV0>> = OnceLock::new();
1708 PROFILES
1709 .get_or_init(|| {
1710 let profiles = serde_json::from_str::<ClosedWorldBuiltinTokenProfilesV0>(
1711 CLOSED_WORLD_BUILTIN_TOKEN_PROFILES_SOURCE,
1712 )
1713 .ok()?;
1714 (profiles.schema_version == "0"
1715 && profiles.product == "omena-abstract-value.closed-world-builtin-token-profiles"
1716 && profiles.oracle.name == ClosedWorldKeywordClosureOracleNameV0::CssTree
1717 && profiles.oracle.version == "3.2.1"
1718 && profiles.profile_count == profiles.profiles.len())
1719 .then_some(profiles)
1720 })
1721 .as_ref()
1722}
1723
1724fn closed_world_token_kind_from_data_name(name: &str) -> Option<ClosedWorldTokenKindV0> {
1725 match name {
1726 "ident" => Some(ClosedWorldTokenKindV0::Ident),
1727 "hash" => Some(ClosedWorldTokenKindV0::Hash),
1728 "dimension" => Some(ClosedWorldTokenKindV0::Dimension),
1729 "number" => Some(ClosedWorldTokenKindV0::Number),
1730 "percentage" => Some(ClosedWorldTokenKindV0::Percentage),
1731 "functionName" => Some(ClosedWorldTokenKindV0::FunctionName),
1732 "string" => Some(ClosedWorldTokenKindV0::String),
1733 "url" => Some(ClosedWorldTokenKindV0::Url),
1734 _ => None,
1735 }
1736}
1737
1738fn all_open_closed_world_token_profile() -> ClosedWorldTokenProfileV0 {
1739 let mut profile = ClosedWorldTokenProfileV0::default();
1740 profile.mark_all_open();
1741 profile
1742}
1743
1744fn closed_world_literal_profile(literal: &str) -> ClosedWorldTokenProfileV0 {
1745 let mut profile = ClosedWorldTokenProfileV0::default();
1746 let Ok(components) = css_value_component_stream(literal, 0) else {
1747 return profile;
1748 };
1749 if let [component] = components.as_slice()
1750 && let Some((kind, value)) = closed_world_component_identity(component)
1751 {
1752 profile.allow(kind, value.as_str());
1753 }
1754 profile
1755}
1756
1757fn closed_world_component_identity(
1758 component: &CssValueComponentV0,
1759) -> Option<(ClosedWorldTokenKindV0, String)> {
1760 let kind = match &component.kind {
1761 CssValueComponentKindV0::Ident => ClosedWorldTokenKindV0::Ident,
1762 CssValueComponentKindV0::Hash => ClosedWorldTokenKindV0::Hash,
1763 CssValueComponentKindV0::Dimension => ClosedWorldTokenKindV0::Dimension,
1764 CssValueComponentKindV0::Number => ClosedWorldTokenKindV0::Number,
1765 CssValueComponentKindV0::Percentage => ClosedWorldTokenKindV0::Percentage,
1766 CssValueComponentKindV0::Function { name, .. } => {
1767 return Some((
1768 ClosedWorldTokenKindV0::FunctionName,
1769 name.to_ascii_lowercase(),
1770 ));
1771 }
1772 CssValueComponentKindV0::String => ClosedWorldTokenKindV0::String,
1773 CssValueComponentKindV0::Url => ClosedWorldTokenKindV0::Url,
1774 CssValueComponentKindV0::Parenthesized { .. }
1775 | CssValueComponentKindV0::Bracketed { .. }
1776 | CssValueComponentKindV0::Braced { .. }
1777 | CssValueComponentKindV0::Comma
1778 | CssValueComponentKindV0::Slash
1779 | CssValueComponentKindV0::Delimiter => return None,
1780 };
1781 Some((kind, component.text.to_ascii_lowercase()))
1782}
1783
1784#[derive(Debug, Clone, PartialEq, Eq)]
1785struct VdsReference {
1786 category: ReferenceCategory,
1787 name: String,
1788 range: Option<NumericRange>,
1789}
1790
1791#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1792enum ReferenceCategory {
1793 Type,
1794 Property,
1795 Function,
1796}
1797
1798#[derive(Debug, Clone, PartialEq, Eq)]
1799struct NumericRange {
1800 min: Option<String>,
1801 max: Option<String>,
1802}
1803
1804#[derive(Debug, Clone, PartialEq, Eq)]
1805struct VdsParseError {
1806 offset: usize,
1807 code: &'static str,
1808 detail: String,
1809}
1810
1811type CachedVdsExpression = Result<Arc<VdsExpression>, VdsParseError>;
1812
1813fn cached_pinned_vds_expression(source: &str) -> CachedVdsExpression {
1814 static CACHE: OnceLock<RwLock<HashMap<String, CachedVdsExpression>>> = OnceLock::new();
1815 let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
1816 if let Some(parsed) = cache
1817 .read()
1818 .unwrap_or_else(std::sync::PoisonError::into_inner)
1819 .get(source)
1820 {
1821 return parsed.clone();
1822 }
1823
1824 let parsed = VdsParser::new(source).parse().map(Arc::new);
1825 cache
1826 .write()
1827 .unwrap_or_else(std::sync::PoisonError::into_inner)
1828 .entry(source.to_string())
1829 .or_insert(parsed)
1830 .clone()
1831}
1832
1833#[derive(Debug, Clone, PartialEq, Eq)]
1834struct VdsToken {
1835 kind: VdsTokenKind,
1836 offset: usize,
1837}
1838
1839#[derive(Debug, Clone, PartialEq, Eq)]
1840enum VdsTokenKind {
1841 Word(String),
1842 Reference(String),
1843 Literal(String),
1844 OpenBracket,
1845 CloseBracket,
1846 OpenParen,
1847 CloseParen,
1848 Or,
1849 OrOr,
1850 AndAnd,
1851 Question,
1852 Star,
1853 Plus,
1854 Hash,
1855 Range(usize, Option<usize>),
1856 Bang,
1857 End,
1858}
1859
1860struct VdsParser<'a> {
1861 source: &'a str,
1862 tokens: Vec<VdsToken>,
1863 cursor: usize,
1864}
1865
1866impl<'a> VdsParser<'a> {
1867 fn new(source: &'a str) -> Self {
1868 Self {
1869 source,
1870 tokens: Vec::new(),
1871 cursor: 0,
1872 }
1873 }
1874
1875 fn parse(mut self) -> Result<VdsExpression, VdsParseError> {
1876 self.tokens = lex_vds(self.source)?;
1877 let expression = self.parse_choice()?;
1878 if !matches!(self.peek(), VdsTokenKind::End) {
1879 return Err(self.error(
1880 "unexpectedGrammarToken",
1881 "unexpected trailing grammar token",
1882 ));
1883 }
1884 Ok(expression)
1885 }
1886
1887 fn parse_choice(&mut self) -> Result<VdsExpression, VdsParseError> {
1888 let mut values = vec![self.parse_one_or_more_in_any_order()?];
1889 while matches!(self.peek(), VdsTokenKind::Or) {
1890 self.cursor += 1;
1891 values.push(self.parse_one_or_more_in_any_order()?);
1892 }
1893 Ok(flatten_expression(values, VdsExpression::Choice))
1894 }
1895
1896 fn parse_one_or_more_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
1897 let mut values = vec![self.parse_all_in_any_order()?];
1898 while matches!(self.peek(), VdsTokenKind::OrOr) {
1899 self.cursor += 1;
1900 values.push(self.parse_all_in_any_order()?);
1901 }
1902 Ok(flatten_expression(
1903 values,
1904 VdsExpression::OneOrMoreInAnyOrder,
1905 ))
1906 }
1907
1908 fn parse_all_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
1909 let mut values = vec![self.parse_sequence()?];
1910 while matches!(self.peek(), VdsTokenKind::AndAnd) {
1911 self.cursor += 1;
1912 values.push(self.parse_sequence()?);
1913 }
1914 Ok(flatten_expression(values, VdsExpression::AllInAnyOrder))
1915 }
1916
1917 fn parse_sequence(&mut self) -> Result<VdsExpression, VdsParseError> {
1918 let mut values = Vec::new();
1919 while self.starts_primary() {
1920 values.push(self.parse_postfix()?);
1921 }
1922 if values.is_empty() {
1923 return Err(self.error("missingGrammarTerm", "expected a grammar term"));
1924 }
1925 Ok(flatten_expression(values, VdsExpression::Sequence))
1926 }
1927
1928 fn parse_postfix(&mut self) -> Result<VdsExpression, VdsParseError> {
1929 let mut expression = self.parse_primary()?;
1930 loop {
1931 expression = match self.peek() {
1932 VdsTokenKind::Question => {
1933 self.cursor += 1;
1934 repeat(expression, 0, Some(1), false)
1935 }
1936 VdsTokenKind::Star => {
1937 self.cursor += 1;
1938 repeat(expression, 0, None, false)
1939 }
1940 VdsTokenKind::Plus => {
1941 self.cursor += 1;
1942 repeat(expression, 1, None, false)
1943 }
1944 VdsTokenKind::Hash => {
1945 self.cursor += 1;
1946 let (min, max) = match self.peek().clone() {
1947 VdsTokenKind::Range(min, max) => {
1948 self.cursor += 1;
1949 (min, max)
1950 }
1951 _ => (1, None),
1952 };
1953 repeat(expression, min, max, true)
1954 }
1955 VdsTokenKind::Range(min, max) => {
1956 let min = *min;
1957 let max = *max;
1958 self.cursor += 1;
1959 repeat(expression, min, max, false)
1960 }
1961 VdsTokenKind::Bang => {
1962 self.cursor += 1;
1963 VdsExpression::Required(Box::new(expression))
1964 }
1965 _ => break,
1966 };
1967 }
1968 Ok(expression)
1969 }
1970
1971 fn parse_primary(&mut self) -> Result<VdsExpression, VdsParseError> {
1972 let token = self.tokens[self.cursor].clone();
1973 self.cursor += 1;
1974 match token.kind {
1975 VdsTokenKind::Reference(source) => Ok(VdsExpression::Reference(parse_reference(
1976 source.as_str(),
1977 token.offset,
1978 )?)),
1979 VdsTokenKind::Word(word) => {
1980 if matches!(self.peek(), VdsTokenKind::OpenParen) {
1981 self.cursor += 1;
1982 if matches!(self.peek(), VdsTokenKind::CloseParen) {
1983 self.cursor += 1;
1984 return Ok(VdsExpression::Function {
1985 name: word,
1986 arguments: Box::new(VdsExpression::Sequence(Vec::new())),
1987 });
1988 }
1989 let arguments = self.parse_choice()?;
1990 self.expect_close_paren()?;
1991 Ok(VdsExpression::Function {
1992 name: word,
1993 arguments: Box::new(arguments),
1994 })
1995 } else {
1996 Ok(VdsExpression::Literal(word))
1997 }
1998 }
1999 VdsTokenKind::Literal(literal) => Ok(VdsExpression::Literal(literal)),
2000 VdsTokenKind::OpenBracket => {
2001 let expression = self.parse_choice()?;
2002 if !matches!(self.peek(), VdsTokenKind::CloseBracket) {
2003 return Err(self.error("unclosedGrammarGroup", "missing closing ]"));
2004 }
2005 self.cursor += 1;
2006 Ok(expression)
2007 }
2008 VdsTokenKind::OpenParen => {
2009 let expression = self.parse_choice()?;
2010 self.expect_close_paren()?;
2011 Ok(expression)
2012 }
2013 _ => Err(VdsParseError {
2014 offset: token.offset,
2015 code: "unexpectedGrammarPrimary",
2016 detail: "expected a literal, reference, function, or group".to_string(),
2017 }),
2018 }
2019 }
2020
2021 fn expect_close_paren(&mut self) -> Result<(), VdsParseError> {
2022 if !matches!(self.peek(), VdsTokenKind::CloseParen) {
2023 return Err(self.error("unclosedGrammarFunction", "missing closing )"));
2024 }
2025 self.cursor += 1;
2026 Ok(())
2027 }
2028
2029 fn starts_primary(&self) -> bool {
2030 matches!(
2031 self.peek(),
2032 VdsTokenKind::Reference(_)
2033 | VdsTokenKind::Word(_)
2034 | VdsTokenKind::Literal(_)
2035 | VdsTokenKind::OpenBracket
2036 | VdsTokenKind::OpenParen
2037 )
2038 }
2039
2040 fn peek(&self) -> &VdsTokenKind {
2041 &self.tokens[self.cursor].kind
2042 }
2043
2044 fn error(&self, code: &'static str, detail: &str) -> VdsParseError {
2045 VdsParseError {
2046 offset: self.tokens[self.cursor].offset,
2047 code,
2048 detail: detail.to_string(),
2049 }
2050 }
2051}
2052
2053fn flatten_expression(
2054 mut values: Vec<VdsExpression>,
2055 wrap: impl FnOnce(Vec<VdsExpression>) -> VdsExpression,
2056) -> VdsExpression {
2057 if values.len() == 1 {
2058 values.pop().unwrap_or(VdsExpression::Sequence(Vec::new()))
2059 } else {
2060 wrap(values)
2061 }
2062}
2063
2064fn repeat(
2065 expression: VdsExpression,
2066 min: usize,
2067 max: Option<usize>,
2068 comma_separated: bool,
2069) -> VdsExpression {
2070 VdsExpression::Repeat {
2071 expression: Box::new(expression),
2072 min,
2073 max,
2074 comma_separated,
2075 }
2076}
2077
2078fn lex_vds(source: &str) -> Result<Vec<VdsToken>, VdsParseError> {
2079 let mut tokens = Vec::new();
2080 let mut cursor = 0usize;
2081 while cursor < source.len() {
2082 let Some(character) = source[cursor..].chars().next() else {
2083 break;
2084 };
2085 if character.is_whitespace() {
2086 cursor += character.len_utf8();
2087 continue;
2088 }
2089 let offset = cursor;
2090 let rest = &source[cursor..];
2091 if rest.starts_with("||") {
2092 tokens.push(token(VdsTokenKind::OrOr, offset));
2093 cursor += 2;
2094 continue;
2095 }
2096 if rest.starts_with("&&") {
2097 tokens.push(token(VdsTokenKind::AndAnd, offset));
2098 cursor += 2;
2099 continue;
2100 }
2101 if character == '<' {
2102 let Some(relative_end) = rest.find('>') else {
2103 return Err(VdsParseError {
2104 offset,
2105 code: "unclosedGrammarReference",
2106 detail: "missing closing >".to_string(),
2107 });
2108 };
2109 let end = cursor + relative_end;
2110 tokens.push(token(
2111 VdsTokenKind::Reference(source[cursor + 1..end].trim().to_string()),
2112 offset,
2113 ));
2114 cursor = end + 1;
2115 continue;
2116 }
2117 if character == '{' {
2118 let Some(relative_end) =
2119 rest.char_indices()
2120 .find_map(|(relative_offset, candidate)| {
2121 (candidate == '}').then_some(relative_offset)
2122 })
2123 else {
2124 return Err(VdsParseError {
2125 offset,
2126 code: "unclosedGrammarRange",
2127 detail: "missing closing }".to_string(),
2128 });
2129 };
2130 let end = cursor + relative_end;
2131 let range = parse_repeat_range(&source[cursor + 1..end], offset)?;
2132 tokens.push(token(VdsTokenKind::Range(range.0, range.1), offset));
2133 cursor = end + 1;
2134 continue;
2135 }
2136 let simple = match character {
2137 '[' => Some(VdsTokenKind::OpenBracket),
2138 ']' => Some(VdsTokenKind::CloseBracket),
2139 '(' => Some(VdsTokenKind::OpenParen),
2140 ')' => Some(VdsTokenKind::CloseParen),
2141 '|' => Some(VdsTokenKind::Or),
2142 '?' => Some(VdsTokenKind::Question),
2143 '*' => Some(VdsTokenKind::Star),
2144 '+' => Some(VdsTokenKind::Plus),
2145 '#' => Some(VdsTokenKind::Hash),
2146 '!' => Some(VdsTokenKind::Bang),
2147 ',' | '/' | ':' | ';' | '=' | '@' | '~' | '^' | '$' | '&' => {
2148 Some(VdsTokenKind::Literal(character.to_string()))
2149 }
2150 _ => None,
2151 };
2152 if let Some(kind) = simple {
2153 tokens.push(token(kind, offset));
2154 cursor += character.len_utf8();
2155 continue;
2156 }
2157 if character == '\'' || character == '"' {
2158 let quote = character;
2159 cursor += character.len_utf8();
2160 let content_start = cursor;
2161 let mut escaped = false;
2162 let mut found_end = None;
2163 while cursor < source.len() {
2164 let Some(current) = source[cursor..].chars().next() else {
2165 break;
2166 };
2167 if escaped {
2168 escaped = false;
2169 } else if current == '\\' {
2170 escaped = true;
2171 } else if current == quote {
2172 found_end = Some(cursor);
2173 break;
2174 }
2175 cursor += current.len_utf8();
2176 }
2177 let Some(end) = found_end else {
2178 return Err(VdsParseError {
2179 offset,
2180 code: "unclosedGrammarString",
2181 detail: "missing closing quote".to_string(),
2182 });
2183 };
2184 tokens.push(token(
2185 VdsTokenKind::Literal(source[content_start..end].to_string()),
2186 offset,
2187 ));
2188 cursor = end + quote.len_utf8();
2189 continue;
2190 }
2191 let start = cursor;
2192 while cursor < source.len() {
2193 let Some(current) = source[cursor..].chars().next() else {
2194 break;
2195 };
2196 if current.is_whitespace()
2197 || matches!(
2198 current,
2199 '<' | '>'
2200 | '['
2201 | ']'
2202 | '('
2203 | ')'
2204 | '{'
2205 | '}'
2206 | '|'
2207 | '&'
2208 | '?'
2209 | '*'
2210 | '+'
2211 | '#'
2212 | '!'
2213 | ','
2214 | '/'
2215 | ':'
2216 | ';'
2217 | '='
2218 | '@'
2219 | '~'
2220 | '^'
2221 | '$'
2222 | '\''
2223 | '"'
2224 )
2225 {
2226 break;
2227 }
2228 cursor += current.len_utf8();
2229 }
2230 if start == cursor {
2231 return Err(VdsParseError {
2232 offset,
2233 code: "unsupportedGrammarCharacter",
2234 detail: format!("unsupported grammar character {character:?}"),
2235 });
2236 }
2237 tokens.push(token(
2238 VdsTokenKind::Word(source[start..cursor].to_string()),
2239 offset,
2240 ));
2241 }
2242 tokens.push(token(VdsTokenKind::End, source.len()));
2243 Ok(tokens)
2244}
2245
2246fn token(kind: VdsTokenKind, offset: usize) -> VdsToken {
2247 VdsToken { kind, offset }
2248}
2249
2250fn parse_repeat_range(
2251 source: &str,
2252 offset: usize,
2253) -> Result<(usize, Option<usize>), VdsParseError> {
2254 let mut parts = source.split(',').map(str::trim);
2255 let first = parts.next().unwrap_or_default();
2256 let second = parts.next();
2257 if parts.next().is_some() || first.is_empty() {
2258 return Err(VdsParseError {
2259 offset,
2260 code: "invalidGrammarRange",
2261 detail: format!("invalid repeat range {{{source}}}"),
2262 });
2263 }
2264 let min = first.parse::<usize>().map_err(|_| VdsParseError {
2265 offset,
2266 code: "invalidGrammarRange",
2267 detail: format!("invalid repeat range minimum {first:?}"),
2268 })?;
2269 let max = match second {
2270 None => Some(min),
2271 Some("") => None,
2272 Some(value) => Some(value.parse::<usize>().map_err(|_| VdsParseError {
2273 offset,
2274 code: "invalidGrammarRange",
2275 detail: format!("invalid repeat range maximum {value:?}"),
2276 })?),
2277 };
2278 if max.is_some_and(|max| max < min) {
2279 return Err(VdsParseError {
2280 offset,
2281 code: "invalidGrammarRange",
2282 detail: format!("repeat range maximum precedes minimum in {{{source}}}"),
2283 });
2284 }
2285 Ok((min, max))
2286}
2287
2288fn parse_reference(source: &str, offset: usize) -> Result<VdsReference, VdsParseError> {
2289 let source = source.trim();
2290 if source.is_empty() {
2291 return Err(VdsParseError {
2292 offset,
2293 code: "emptyGrammarReference",
2294 detail: "empty grammar reference".to_string(),
2295 });
2296 }
2297 if let Some(property) = source
2298 .strip_prefix('\'')
2299 .and_then(|value| value.strip_suffix('\''))
2300 {
2301 return Ok(VdsReference {
2302 category: ReferenceCategory::Property,
2303 name: PropertyNameV0::from_authored(property)
2304 .canonical_name()
2305 .to_string(),
2306 range: None,
2307 });
2308 }
2309 let (name, range) = split_reference_range(source, offset)?;
2310 let category = if name.ends_with("()") {
2311 ReferenceCategory::Function
2312 } else {
2313 ReferenceCategory::Type
2314 };
2315 Ok(VdsReference {
2316 category,
2317 name: name.to_ascii_lowercase(),
2318 range,
2319 })
2320}
2321
2322fn split_reference_range(
2323 source: &str,
2324 offset: usize,
2325) -> Result<(&str, Option<NumericRange>), VdsParseError> {
2326 let Some(open) = source.find('[') else {
2327 return Ok((source.trim(), None));
2328 };
2329 let Some(close) = source.rfind(']') else {
2330 return Err(VdsParseError {
2331 offset,
2332 code: "unclosedReferenceRange",
2333 detail: format!("missing ] in reference <{source}>"),
2334 });
2335 };
2336 if close + 1 != source.len() {
2337 return Err(VdsParseError {
2338 offset,
2339 code: "trailingReferenceRangeContent",
2340 detail: format!("unexpected content after range in <{source}>"),
2341 });
2342 }
2343 let name = source[..open].trim();
2344 let mut bounds = source[open + 1..close].split(',').map(str::trim);
2345 let min = bounds.next().unwrap_or_default();
2346 let max = bounds.next();
2347 if name.is_empty() || min.is_empty() || max.is_none() || bounds.next().is_some() {
2348 return Err(VdsParseError {
2349 offset,
2350 code: "invalidReferenceRange",
2351 detail: format!("invalid numeric range in <{source}>"),
2352 });
2353 }
2354 let max = max.unwrap_or_default();
2355 Ok((
2356 name,
2357 Some(NumericRange {
2358 min: finite_range_bound(min),
2359 max: finite_range_bound(max),
2360 }),
2361 ))
2362}
2363
2364fn finite_range_bound(source: &str) -> Option<String> {
2365 (!matches!(source, "∞" | "+∞" | "-∞")).then(|| source.to_string())
2366}
2367
2368#[derive(Debug, Clone, PartialEq, Eq)]
2369enum MatchStop {
2370 Budget {
2371 kind: CssValueGrammarBudgetKindV0,
2372 limit: usize,
2373 reference: Option<String>,
2374 },
2375 GrammarDefect {
2376 offset: usize,
2377 code: &'static str,
2378 detail: String,
2379 },
2380}
2381
2382struct MatchContext<'a> {
2383 registry: &'a SpecGrammarRegistryV0,
2384 budget: CssValueGrammarBudgetV0,
2385 match_steps: usize,
2386 first_stop: Option<MatchStop>,
2387 grammar_cache: HashMap<(ReferenceCategory, String), CachedVdsExpression>,
2388 cache_registered_grammars: bool,
2389 allow_unvalidated_standard_function_references: bool,
2390}
2391
2392#[derive(Debug, Clone, Copy)]
2393struct RepeatMatchPlan<'a> {
2394 expression: &'a VdsExpression,
2395 min: usize,
2396 max: Option<usize>,
2397 comma_separated: bool,
2398}
2399
2400impl MatchContext<'_> {
2401 fn match_expression(
2402 &mut self,
2403 expression: &VdsExpression,
2404 components: &[CssValueComponentV0],
2405 position: usize,
2406 reference_depth: usize,
2407 ) -> BTreeSet<usize> {
2408 if !self.consume_step(None) {
2409 return BTreeSet::new();
2410 }
2411 let positions = match expression {
2412 VdsExpression::Literal(literal) => match_literal(literal, components, position),
2413 VdsExpression::Reference(reference) => {
2414 self.match_reference(reference, components, position, reference_depth)
2415 }
2416 VdsExpression::Function { name, arguments } => {
2417 self.match_function(name, arguments, components, position, reference_depth)
2418 }
2419 VdsExpression::Sequence(expressions) => {
2420 self.match_sequence(expressions, components, position, reference_depth)
2421 }
2422 VdsExpression::AllInAnyOrder(expressions) => {
2423 self.match_any_order(expressions, components, position, reference_depth, true)
2424 }
2425 VdsExpression::OneOrMoreInAnyOrder(expressions) => {
2426 self.match_any_order(expressions, components, position, reference_depth, false)
2427 }
2428 VdsExpression::Choice(expressions) => expressions
2429 .iter()
2430 .flat_map(|expression| {
2431 self.match_expression(expression, components, position, reference_depth)
2432 })
2433 .collect(),
2434 VdsExpression::Repeat {
2435 expression,
2436 min,
2437 max,
2438 comma_separated,
2439 } => self.match_repeat(
2440 RepeatMatchPlan {
2441 expression,
2442 min: *min,
2443 max: *max,
2444 comma_separated: *comma_separated,
2445 },
2446 components,
2447 position,
2448 reference_depth,
2449 ),
2450 VdsExpression::Required(expression) => self
2451 .match_expression(expression, components, position, reference_depth)
2452 .into_iter()
2453 .filter(|end| *end > position)
2454 .collect(),
2455 };
2456 self.cap_states(positions, None)
2457 }
2458
2459 fn match_sequence(
2460 &mut self,
2461 expressions: &[VdsExpression],
2462 components: &[CssValueComponentV0],
2463 position: usize,
2464 reference_depth: usize,
2465 ) -> BTreeSet<usize> {
2466 let mut states = BTreeSet::from([(position, false)]);
2467 for expression in expressions {
2468 let mut next = BTreeSet::new();
2469 for (position, previous_was_omitted) in states {
2470 if previous_was_omitted && is_comma_literal(expression) {
2471 next.insert((position, false));
2472 continue;
2473 }
2474 for end in self.match_expression(expression, components, position, reference_depth)
2475 {
2476 next.insert((end, end == position));
2477 }
2478 }
2479 if next.len() > self.budget.max_states {
2480 self.record_stop(MatchStop::Budget {
2481 kind: CssValueGrammarBudgetKindV0::CandidateStates,
2482 limit: self.budget.max_states,
2483 reference: None,
2484 });
2485 next.clear();
2486 }
2487 states = next;
2488 if states.is_empty() {
2489 break;
2490 }
2491 }
2492 states.into_iter().map(|(position, _)| position).collect()
2493 }
2494
2495 fn match_repeat(
2496 &mut self,
2497 plan: RepeatMatchPlan<'_>,
2498 components: &[CssValueComponentV0],
2499 position: usize,
2500 reference_depth: usize,
2501 ) -> BTreeSet<usize> {
2502 let effective_max = plan
2503 .max
2504 .unwrap_or_else(|| components.len().saturating_add(1));
2505 let mut accepted = BTreeSet::new();
2506 let mut frontier = BTreeSet::from([position]);
2507 if plan.min == 0 {
2508 accepted.insert(position);
2509 }
2510 for count in 1..=effective_max {
2511 let mut next = BTreeSet::new();
2512 for current in &frontier {
2513 let item_start = if plan.comma_separated && count > 1 {
2514 if components.get(*current).is_some_and(|component| {
2515 matches!(component.kind, CssValueComponentKindV0::Comma)
2516 }) {
2517 *current + 1
2518 } else {
2519 continue;
2520 }
2521 } else {
2522 *current
2523 };
2524 for end in
2525 self.match_expression(plan.expression, components, item_start, reference_depth)
2526 {
2527 if end > item_start {
2528 next.insert(end);
2529 }
2530 }
2531 }
2532 frontier = self.cap_states(next, None);
2533 if frontier.is_empty() {
2534 break;
2535 }
2536 if count >= plan.min {
2537 accepted.extend(frontier.iter().copied());
2538 }
2539 }
2540 accepted
2541 }
2542
2543 fn match_any_order(
2544 &mut self,
2545 expressions: &[VdsExpression],
2546 components: &[CssValueComponentV0],
2547 position: usize,
2548 reference_depth: usize,
2549 require_all: bool,
2550 ) -> BTreeSet<usize> {
2551 if expressions.len() > 63 {
2552 self.record_stop(MatchStop::Budget {
2553 kind: CssValueGrammarBudgetKindV0::CandidateStates,
2554 limit: self.budget.max_states,
2555 reference: None,
2556 });
2557 return BTreeSet::new();
2558 }
2559 let required_mask = (1u64 << expressions.len()) - 1;
2560 let mut accepted = BTreeSet::new();
2561 let mut stack = vec![(position, 0u64)];
2562 let mut visited = BTreeSet::new();
2563 while let Some((current, mask)) = stack.pop() {
2564 if !visited.insert((current, mask)) {
2565 continue;
2566 }
2567 if visited.len() > self.budget.max_states {
2568 self.record_stop(MatchStop::Budget {
2569 kind: CssValueGrammarBudgetKindV0::CandidateStates,
2570 limit: self.budget.max_states,
2571 reference: None,
2572 });
2573 break;
2574 }
2575 if (require_all && mask == required_mask) || (!require_all && mask != 0) {
2576 accepted.insert(current);
2577 }
2578 for (index, expression) in expressions.iter().enumerate() {
2579 let bit = 1u64 << index;
2580 if mask & bit != 0 {
2581 continue;
2582 }
2583 for end in self.match_expression(expression, components, current, reference_depth) {
2584 if end > current || (require_all && end == current) {
2585 stack.push((end, mask | bit));
2586 }
2587 }
2588 }
2589 }
2590 accepted
2591 }
2592
2593 fn match_function(
2594 &mut self,
2595 name: &str,
2596 arguments: &VdsExpression,
2597 components: &[CssValueComponentV0],
2598 position: usize,
2599 reference_depth: usize,
2600 ) -> BTreeSet<usize> {
2601 let Some(component) = components.get(position) else {
2602 return BTreeSet::new();
2603 };
2604 let CssValueComponentKindV0::Function {
2605 name: actual,
2606 arguments: actual_arguments,
2607 } = &component.kind
2608 else {
2609 return BTreeSet::new();
2610 };
2611 if !actual.eq_ignore_ascii_case(name) {
2612 return BTreeSet::new();
2613 }
2614 self.match_expression(arguments, actual_arguments, 0, reference_depth)
2615 .contains(&actual_arguments.len())
2616 .then_some(position + 1)
2617 .into_iter()
2618 .collect()
2619 }
2620
2621 fn match_reference(
2622 &mut self,
2623 reference: &VdsReference,
2624 components: &[CssValueComponentV0],
2625 position: usize,
2626 reference_depth: usize,
2627 ) -> BTreeSet<usize> {
2628 if self.allow_unvalidated_standard_function_references
2629 && reference.category != ReferenceCategory::Function
2630 && components
2631 .get(position)
2632 .is_some_and(component_is_recognized_standard_function)
2633 {
2634 return BTreeSet::from([position + 1]);
2635 }
2636 if let Some(positions) =
2637 match_builtin_reference(reference, components, position, self.registry)
2638 {
2639 return positions;
2640 }
2641 if reference_depth >= self.budget.max_reference_depth {
2642 self.record_stop(MatchStop::Budget {
2643 kind: CssValueGrammarBudgetKindV0::ReferenceDepth,
2644 limit: self.budget.max_reference_depth,
2645 reference: Some(reference.name.clone()),
2646 });
2647 return BTreeSet::new();
2648 }
2649 let category = match reference.category {
2650 ReferenceCategory::Type => "types",
2651 ReferenceCategory::Property => "properties",
2652 ReferenceCategory::Function => "functions",
2653 };
2654 let Some(entry) = self.registry.entry(category, reference.name.as_str()) else {
2655 self.record_stop(MatchStop::GrammarDefect {
2656 offset: 0,
2657 code: "unknownGrammarReference",
2658 detail: format!("unknown {category} reference <{}>", reference.name),
2659 });
2660 return BTreeSet::new();
2661 };
2662 let Some(source) = entry.syntax.as_deref() else {
2663 self.record_stop(MatchStop::GrammarDefect {
2664 offset: 0,
2665 code: "missingReferencedGrammar",
2666 detail: format!("{category} reference <{}> has no syntax", reference.name),
2667 });
2668 return BTreeSet::new();
2669 };
2670 let key = (reference.category, reference.name.clone());
2671 let expression = match self
2672 .grammar_cache
2673 .entry(key)
2674 .or_insert_with(|| {
2675 if self.cache_registered_grammars {
2676 cached_pinned_vds_expression(source)
2677 } else {
2678 VdsParser::new(source).parse().map(Arc::new)
2679 }
2680 })
2681 .clone()
2682 {
2683 Ok(expression) => expression,
2684 Err(error) => {
2685 self.record_stop(MatchStop::GrammarDefect {
2686 offset: error.offset,
2687 code: error.code,
2688 detail: format!("referenced grammar <{}>: {}", reference.name, error.detail),
2689 });
2690 return BTreeSet::new();
2691 }
2692 };
2693 if reference.category == ReferenceCategory::Function {
2694 return self.match_function_reference(
2695 reference,
2696 expression.as_ref(),
2697 components,
2698 position,
2699 reference_depth + 1,
2700 );
2701 }
2702 self.match_expression(
2703 expression.as_ref(),
2704 components,
2705 position,
2706 reference_depth + 1,
2707 )
2708 }
2709
2710 fn match_function_reference(
2711 &mut self,
2712 reference: &VdsReference,
2713 expression: &VdsExpression,
2714 components: &[CssValueComponentV0],
2715 position: usize,
2716 reference_depth: usize,
2717 ) -> BTreeSet<usize> {
2718 let name = reference.name.trim_end_matches("()");
2719 let whole_component =
2720 self.match_expression(expression, components, position, reference_depth);
2721 if !whole_component.is_empty() {
2722 return whole_component;
2723 }
2724 self.match_function(name, expression, components, position, reference_depth)
2725 }
2726
2727 fn consume_step(&mut self, reference: Option<String>) -> bool {
2728 self.match_steps += 1;
2729 if self.match_steps <= self.budget.max_match_steps {
2730 return true;
2731 }
2732 self.record_stop(MatchStop::Budget {
2733 kind: CssValueGrammarBudgetKindV0::MatchSteps,
2734 limit: self.budget.max_match_steps,
2735 reference,
2736 });
2737 false
2738 }
2739
2740 fn cap_states(
2741 &mut self,
2742 mut states: BTreeSet<usize>,
2743 reference: Option<String>,
2744 ) -> BTreeSet<usize> {
2745 if states.len() <= self.budget.max_states {
2746 return states;
2747 }
2748 self.record_stop(MatchStop::Budget {
2749 kind: CssValueGrammarBudgetKindV0::CandidateStates,
2750 limit: self.budget.max_states,
2751 reference,
2752 });
2753 states.clear();
2754 states
2755 }
2756
2757 fn record_stop(&mut self, stop: MatchStop) {
2758 if self.first_stop.is_none() {
2759 self.first_stop = Some(stop);
2760 }
2761 }
2762}
2763
2764fn match_literal(
2765 literal: &str,
2766 components: &[CssValueComponentV0],
2767 position: usize,
2768) -> BTreeSet<usize> {
2769 components
2770 .get(position)
2771 .filter(|component| component.text.eq_ignore_ascii_case(literal))
2772 .map(|_| BTreeSet::from([position + 1]))
2773 .unwrap_or_default()
2774}
2775
2776fn is_comma_literal(expression: &VdsExpression) -> bool {
2777 matches!(expression, VdsExpression::Literal(literal) if literal == ",")
2778}
2779
2780fn is_unitless_zero(component: &CssValueComponentV0) -> bool {
2781 matches!(component.kind, CssValueComponentKindV0::Number)
2782 && parse_numeric_value_with_unit(component.text.as_str())
2783 .is_some_and(|numeric| numeric.value == 0.0 && numeric.unit.is_empty())
2784}
2785
2786fn match_builtin_reference(
2787 reference: &VdsReference,
2788 components: &[CssValueComponentV0],
2789 position: usize,
2790 registry: &SpecGrammarRegistryV0,
2791) -> Option<BTreeSet<usize>> {
2792 if reference.category != ReferenceCategory::Type {
2793 return None;
2794 }
2795 if matches!(
2796 reference.name.as_str(),
2797 "declaration-value" | "any-value" | "whole-value"
2798 ) {
2799 return Some(((position + 1)..=components.len()).collect());
2800 }
2801 if !is_builtin_reference_name(reference.name.as_str()) {
2802 return None;
2803 }
2804 let Some(component) = components.get(position) else {
2805 return Some(BTreeSet::new());
2806 };
2807 if math_function_matches_reference(reference, component, registry) {
2808 return Some(BTreeSet::from([position + 1]));
2809 }
2810 let kind = classify_registered_property_declared_value_v0(component.text.as_str());
2811 let accepted = match reference.name.as_str() {
2812 "number" | "number-token" => {
2813 matches!(
2814 kind,
2815 DeclaredValueKindV0::Number | DeclaredValueKindV0::Integer
2816 )
2817 }
2818 "integer" => matches!(kind, DeclaredValueKindV0::Integer),
2819 "length" => {
2820 matches!(
2821 kind,
2822 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length)
2823 ) || is_unitless_zero(component)
2824 }
2825 "percentage" | "percentage-token" => matches!(
2826 kind,
2827 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
2828 ),
2829 "length-percentage" => {
2830 matches!(
2831 kind,
2832 DeclaredValueKindV0::Dimension(
2833 DeclaredNumericTypeV0::Length | DeclaredNumericTypeV0::Percentage
2834 )
2835 ) || is_unitless_zero(component)
2836 }
2837 "angle" => matches!(
2838 kind,
2839 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle)
2840 ),
2841 "time" => matches!(
2842 kind,
2843 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time)
2844 ),
2845 "resolution" => matches!(
2846 kind,
2847 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Resolution)
2848 ),
2849 "flex" => parse_numeric_value_with_unit(component.text.as_str())
2850 .is_some_and(|numeric| numeric.unit.eq_ignore_ascii_case("fr")),
2851 "hex-color" => matches!(kind, DeclaredValueKindV0::HexColor),
2852 "named-color" => matches!(kind, DeclaredValueKindV0::ColorKeyword(_)),
2853 "custom-ident" => {
2854 matches!(component.kind, CssValueComponentKindV0::Ident)
2855 && !matches!(kind, DeclaredValueKindV0::CssWide)
2856 }
2857 "ident" | "ident-token" => matches!(component.kind, CssValueComponentKindV0::Ident),
2858 "dashed-ident" | "custom-property-name" => {
2859 matches!(component.kind, CssValueComponentKindV0::Ident)
2860 && is_custom_property_name(&component.text)
2861 }
2862 "string" | "string-token" => matches!(kind, DeclaredValueKindV0::QuotedString),
2863 "url" | "url-token" => matches!(kind, DeclaredValueKindV0::Url),
2864 "image" => matches!(
2865 kind,
2866 DeclaredValueKindV0::ImageFunction | DeclaredValueKindV0::Url
2867 ),
2868 "transform-function" => matches!(kind, DeclaredValueKindV0::TransformFunction),
2869 "alpha-value" => matches!(
2870 kind,
2871 DeclaredValueKindV0::Number
2872 | DeclaredValueKindV0::Integer
2873 | DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
2874 ),
2875 "zero" => parse_numeric_value_with_unit(component.text.as_str())
2876 .is_some_and(|numeric| numeric.value == 0.0),
2877 "dimension-token" => matches!(component.kind, CssValueComponentKindV0::Dimension),
2878 "hash-token" => matches!(component.kind, CssValueComponentKindV0::Hash),
2879 "function-token" => matches!(component.kind, CssValueComponentKindV0::Function { .. }),
2880 "comma-token" => matches!(component.kind, CssValueComponentKindV0::Comma),
2881 _ => false,
2882 };
2883 let accepted =
2884 accepted && numeric_range_accepts(reference.range.as_ref(), component.text.as_str());
2885 Some(accepted.then_some(position + 1).into_iter().collect())
2886}
2887
2888fn is_builtin_reference_name(name: &str) -> bool {
2889 matches!(
2890 name,
2891 "number"
2892 | "number-token"
2893 | "integer"
2894 | "length"
2895 | "percentage"
2896 | "percentage-token"
2897 | "length-percentage"
2898 | "angle"
2899 | "time"
2900 | "resolution"
2901 | "flex"
2902 | "hex-color"
2903 | "named-color"
2904 | "custom-ident"
2905 | "ident"
2906 | "ident-token"
2907 | "dashed-ident"
2908 | "custom-property-name"
2909 | "string"
2910 | "string-token"
2911 | "url"
2912 | "url-token"
2913 | "image"
2914 | "transform-function"
2915 | "alpha-value"
2916 | "zero"
2917 | "dimension-token"
2918 | "hash-token"
2919 | "function-token"
2920 | "comma-token"
2921 )
2922}
2923
2924fn math_function_matches_reference(
2925 reference: &VdsReference,
2926 component: &CssValueComponentV0,
2927 registry: &SpecGrammarRegistryV0,
2928) -> bool {
2929 if reference.category != ReferenceCategory::Type
2930 || !matches!(
2931 reference.name.as_str(),
2932 "number" | "length" | "percentage" | "length-percentage" | "time" | "angle"
2933 )
2934 {
2935 return false;
2936 }
2937 let CssValueComponentKindV0::Function { name, arguments } = &component.kind else {
2938 return false;
2939 };
2940 if !matches!(name.as_str(), "calc" | "min" | "max" | "clamp") {
2941 return false;
2942 }
2943 let registry_name = format!("{name}()");
2944 if !registry
2945 .entry("functions", registry_name.as_str())
2946 .is_some_and(|entry| {
2947 entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
2948 && entry.syntax.is_some()
2949 })
2950 {
2951 return false;
2952 }
2953 math_function_result_kind(name, arguments, registry)
2954 .is_some_and(|kind| math_kind_matches_reference(kind, reference.name.as_str()))
2955 && math_range_is_provably_accepted(reference.range.as_ref(), arguments)
2956}
2957
2958fn split_math_argument_groups(arguments: &[CssValueComponentV0]) -> Vec<&[CssValueComponentV0]> {
2959 let mut groups = Vec::new();
2960 let mut start = 0;
2961 for (index, component) in arguments.iter().enumerate() {
2962 if matches!(component.kind, CssValueComponentKindV0::Comma) {
2963 groups.push(&arguments[start..index]);
2964 start = index + 1;
2965 }
2966 }
2967 groups.push(&arguments[start..]);
2968 groups
2969}
2970
2971#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2972enum MathValueKind {
2973 Number,
2974 Length,
2975 Percentage,
2976 LengthPercentage,
2977 Time,
2978 Angle,
2979}
2980
2981fn math_kind_matches_reference(kind: MathValueKind, reference: &str) -> bool {
2982 matches!(
2983 (kind, reference),
2984 (MathValueKind::Number, "number")
2985 | (MathValueKind::Length, "length" | "length-percentage")
2986 | (
2987 MathValueKind::Percentage,
2988 "percentage" | "length-percentage"
2989 )
2990 | (MathValueKind::LengthPercentage, "length-percentage")
2991 | (MathValueKind::Time, "time")
2992 | (MathValueKind::Angle, "angle")
2993 )
2994}
2995
2996fn math_function_result_kind(
2997 name: &str,
2998 arguments: &[CssValueComponentV0],
2999 registry: &SpecGrammarRegistryV0,
3000) -> Option<MathValueKind> {
3001 if !matches!(name, "calc" | "min" | "max" | "clamp") {
3002 return None;
3003 }
3004 let registry_name = format!("{name}()");
3005 if !registry
3006 .entry("functions", registry_name.as_str())
3007 .is_some_and(|entry| {
3008 entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
3009 && entry.syntax.is_some()
3010 })
3011 {
3012 return None;
3013 }
3014 let groups = split_math_argument_groups(arguments);
3015 let arity_matches = match name {
3016 "calc" => groups.len() == 1,
3017 "min" | "max" => !groups.is_empty(),
3018 "clamp" => groups.len() == 3,
3019 _ => false,
3020 };
3021 if !arity_matches {
3022 return None;
3023 }
3024 let mut kinds = groups
3025 .iter()
3026 .map(|group| MathExpressionParser::new(group, registry).parse())
3027 .collect::<Option<Vec<_>>>()?
3028 .into_iter();
3029 let first = kinds.next()?;
3030 kinds.try_fold(first, unify_additive_math_kinds)
3031}
3032
3033fn unify_additive_math_kinds(left: MathValueKind, right: MathValueKind) -> Option<MathValueKind> {
3034 if left == right {
3035 return Some(left);
3036 }
3037 if matches!(
3038 (left, right),
3039 (MathValueKind::Length, MathValueKind::Percentage)
3040 | (MathValueKind::Percentage, MathValueKind::Length)
3041 | (MathValueKind::LengthPercentage, MathValueKind::Length)
3042 | (MathValueKind::Length, MathValueKind::LengthPercentage)
3043 | (MathValueKind::LengthPercentage, MathValueKind::Percentage)
3044 | (MathValueKind::Percentage, MathValueKind::LengthPercentage)
3045 ) {
3046 return Some(MathValueKind::LengthPercentage);
3047 }
3048 None
3049}
3050
3051struct MathExpressionParser<'a> {
3052 components: &'a [CssValueComponentV0],
3053 cursor: usize,
3054 registry: &'a SpecGrammarRegistryV0,
3055}
3056
3057impl<'a> MathExpressionParser<'a> {
3058 fn new(components: &'a [CssValueComponentV0], registry: &'a SpecGrammarRegistryV0) -> Self {
3059 Self {
3060 components,
3061 cursor: 0,
3062 registry,
3063 }
3064 }
3065
3066 fn parse(mut self) -> Option<MathValueKind> {
3067 let kind = self.parse_sum()?;
3068 (self.cursor == self.components.len()).then_some(kind)
3069 }
3070
3071 fn parse_sum(&mut self) -> Option<MathValueKind> {
3072 let mut left = self.parse_product()?;
3073 loop {
3074 if self.peek_binary_additive_operator().is_none() {
3075 break;
3076 }
3077 self.cursor += 1;
3078 let right = self.parse_product()?;
3079 left = unify_additive_math_kinds(left, right)?;
3080 }
3081 Some(left)
3082 }
3083
3084 fn parse_product(&mut self) -> Option<MathValueKind> {
3085 let mut left = self.parse_unary()?;
3086 while let Some(operator) = self.peek_operator(&["*", "/"]).map(str::to_owned) {
3087 self.cursor += 1;
3088 let right = self.parse_unary()?;
3089 left = match operator.as_str() {
3090 "*" if left == MathValueKind::Number => right,
3091 "*" if right == MathValueKind::Number => left,
3092 "/" if right == MathValueKind::Number => left,
3093 _ => return None,
3094 };
3095 }
3096 Some(left)
3097 }
3098
3099 fn parse_unary(&mut self) -> Option<MathValueKind> {
3100 if self.peek_operator(&["+", "-"]).is_some() {
3101 self.cursor += 1;
3102 return self.parse_unary();
3103 }
3104 self.parse_operand()
3105 }
3106
3107 fn parse_operand(&mut self) -> Option<MathValueKind> {
3108 let component = self.components.get(self.cursor)?;
3109 self.cursor += 1;
3110 match &component.kind {
3111 CssValueComponentKindV0::Number => Some(MathValueKind::Number),
3112 CssValueComponentKindV0::Percentage => Some(MathValueKind::Percentage),
3113 CssValueComponentKindV0::Dimension => {
3114 match classify_registered_property_declared_value_v0(component.text.as_str()) {
3115 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length) => {
3116 Some(MathValueKind::Length)
3117 }
3118 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage) => {
3119 Some(MathValueKind::Percentage)
3120 }
3121 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time) => {
3122 Some(MathValueKind::Time)
3123 }
3124 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle) => {
3125 Some(MathValueKind::Angle)
3126 }
3127 _ => None,
3128 }
3129 }
3130 CssValueComponentKindV0::Function { name, arguments } => {
3131 math_function_result_kind(name, arguments, self.registry)
3132 }
3133 CssValueComponentKindV0::Parenthesized { values } => {
3134 MathExpressionParser::new(values, self.registry).parse()
3135 }
3136 CssValueComponentKindV0::Ident
3137 | CssValueComponentKindV0::Hash
3138 | CssValueComponentKindV0::String
3139 | CssValueComponentKindV0::Url
3140 | CssValueComponentKindV0::Bracketed { .. }
3141 | CssValueComponentKindV0::Braced { .. }
3142 | CssValueComponentKindV0::Comma
3143 | CssValueComponentKindV0::Slash
3144 | CssValueComponentKindV0::Delimiter => None,
3145 }
3146 }
3147
3148 fn peek_operator(&self, expected: &[&str]) -> Option<&str> {
3149 let component = self.components.get(self.cursor)?;
3150 (matches!(
3151 component.kind,
3152 CssValueComponentKindV0::Delimiter | CssValueComponentKindV0::Slash
3153 ) || (matches!(component.kind, CssValueComponentKindV0::Ident)
3154 && matches!(component.text.as_str(), "+" | "-")))
3155 .then_some(component.text.as_str())
3156 .filter(|operator| expected.contains(operator))
3157 }
3158
3159 fn peek_binary_additive_operator(&self) -> Option<&str> {
3160 let previous = self
3161 .cursor
3162 .checked_sub(1)
3163 .and_then(|index| self.components.get(index))?;
3164 let operator_component = self.components.get(self.cursor)?;
3165 let next = self.components.get(self.cursor + 1)?;
3166 self.peek_operator(&["+", "-"]).filter(|_| {
3167 previous.span.end < operator_component.span.start
3168 && operator_component.span.end < next.span.start
3169 })
3170 }
3171}
3172
3173fn math_range_is_provably_accepted(
3174 range: Option<&NumericRange>,
3175 components: &[CssValueComponentV0],
3176) -> bool {
3177 let Some(range) = range else {
3178 return true;
3179 };
3180 if !range
3181 .min
3182 .as_deref()
3183 .and_then(parse_numeric_value_with_unit)
3184 .is_some_and(|numeric| numeric.value == 0.0)
3185 || range.max.is_some()
3186 {
3187 return false;
3188 }
3189 components.iter().all(|component| match &component.kind {
3190 CssValueComponentKindV0::Number
3191 | CssValueComponentKindV0::Percentage
3192 | CssValueComponentKindV0::Dimension => parse_numeric_value_with_unit(&component.text)
3193 .is_some_and(|numeric| numeric.value >= 0.0),
3194 CssValueComponentKindV0::Function { arguments, .. }
3195 | CssValueComponentKindV0::Parenthesized { values: arguments } => {
3196 math_range_is_provably_accepted(Some(range), arguments)
3197 }
3198 CssValueComponentKindV0::Delimiter => matches!(component.text.as_str(), "+" | "-"),
3199 CssValueComponentKindV0::Comma | CssValueComponentKindV0::Slash => true,
3200 CssValueComponentKindV0::Ident => matches!(component.text.as_str(), "+" | "-"),
3201 CssValueComponentKindV0::Hash
3202 | CssValueComponentKindV0::String
3203 | CssValueComponentKindV0::Url
3204 | CssValueComponentKindV0::Bracketed { .. }
3205 | CssValueComponentKindV0::Braced { .. } => false,
3206 })
3207}
3208
3209fn numeric_range_accepts(range: Option<&NumericRange>, source: &str) -> bool {
3210 let Some(range) = range else {
3211 return true;
3212 };
3213 let Some(numeric) = parse_numeric_value_with_unit(source) else {
3214 return false;
3215 };
3216 let above_min = range
3217 .min
3218 .as_deref()
3219 .and_then(|value| value.parse::<f64>().ok())
3220 .is_none_or(|minimum| numeric.value >= minimum);
3221 let below_max = range
3222 .max
3223 .as_deref()
3224 .and_then(|value| value.parse::<f64>().ok())
3225 .is_none_or(|maximum| numeric.value <= maximum);
3226 above_min && below_max
3227}
3228
3229fn component_locus(components: &[CssValueComponentV0]) -> CssValueGrammarLocusV0 {
3230 match (components.first(), components.last()) {
3231 (Some(first), Some(last)) => CssValueGrammarLocusV0 {
3232 start: first.span.start,
3233 end: last.span.end,
3234 },
3235 _ => CssValueGrammarLocusV0 { start: 0, end: 0 },
3236 }
3237}
3238
3239fn grammar_defect(
3240 grammar: &str,
3241 offset: usize,
3242 code: impl Into<String>,
3243 detail: impl Into<String>,
3244) -> CssValueGrammarVerdictV0 {
3245 CssValueGrammarVerdictV0::GrammarDefect {
3246 grammar: grammar.to_string(),
3247 offset,
3248 code: code.into(),
3249 detail: detail.into(),
3250 }
3251}
3252
3253fn strip_matching_quotes(source: &str) -> &str {
3254 if source.len() >= 2 {
3255 let bytes = source.as_bytes();
3256 if matches!(
3257 (bytes[0], bytes[source.len() - 1]),
3258 (b'\'', b'\'') | (b'"', b'"')
3259 ) {
3260 return &source[1..source.len() - 1];
3261 }
3262 }
3263 source
3264}
3265
3266#[cfg(test)]
3267mod tests {
3268 use std::{collections::BTreeSet, sync::Arc};
3269
3270 use omena_cascade::{CascadeStandardValueValidatorV0, CascadeStandardValueVerdictV0};
3271 use omena_spec_audit::{SpecGrammarBoundaryClassificationV0, spec_grammar_registry};
3272 use omena_syntax::ident::PropertyNameV0;
3273 use omena_value_lattice::ValueNodeV0;
3274
3275 use super::{
3276 CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE, CLOSED_WORLD_TOKEN_KINDS,
3277 CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0, CssValueGrammarBudgetKindV0,
3278 CssValueGrammarBudgetV0, CssValueGrammarLocusV0, CssValueGrammarVerdictV0,
3279 CssValueValidationClassV0, CssValueValidationReasonV0,
3280 SpecStandardPropertyValueValidatorV0, adjudicate_css_value_validation,
3281 adjudicate_css_value_validation_with_boundary, audit_css_value_grammar_registry_v0,
3282 cached_pinned_vds_expression, cached_standard_property_closed_world_token_profile,
3283 closed_world_builtin_profile, closed_world_builtin_token_profiles,
3284 closed_world_keyword_authority, match_and_type_css_value_grammar_v0,
3285 match_and_type_standard_property_value_v0, match_css_value_grammar_v0,
3286 match_standard_property_value_v0, parse_closed_world_keyword_closure_certificate,
3287 standard_property_closed_world_token_kind_mismatch, validate_registered_property_value_v0,
3288 validate_standard_property_value_v0,
3289 };
3290 use crate::{
3291 AbstractCssTypedValueV0, AbstractCssValueV0, DeclaredValueKindV0,
3292 classify_registered_property_declared_value_v0,
3293 };
3294
3295 fn assert_matches(grammar: &str, value: &str) {
3296 let verdict = match_css_value_grammar_v0(
3297 grammar,
3298 value,
3299 spec_grammar_registry(),
3300 CssValueGrammarBudgetV0::default(),
3301 );
3302 assert!(
3303 verdict.is_matched(),
3304 "{grammar:?} should match {value:?}: {verdict:?}"
3305 );
3306 }
3307
3308 fn assert_unmatched(grammar: &str, value: &str) {
3309 let verdict = match_css_value_grammar_v0(
3310 grammar,
3311 value,
3312 spec_grammar_registry(),
3313 CssValueGrammarBudgetV0::default(),
3314 );
3315 assert!(
3316 verdict.is_definite_mismatch(),
3317 "{grammar:?} should reject {value:?}: {verdict:?}"
3318 );
3319 }
3320
3321 #[test]
3322 fn parsed_grammar_cache_reuses_the_immutable_expression() {
3323 let grammar = "<length> | cache-sentinel";
3324 let first = cached_pinned_vds_expression(grammar);
3325 let second = cached_pinned_vds_expression(grammar);
3326
3327 assert!(matches!(
3328 (&first, &second),
3329 (Ok(first), Ok(second)) if Arc::ptr_eq(first, second)
3330 ));
3331 }
3332
3333 #[test]
3334 fn grammar_conformance_covers_all_combinators_and_multipliers() {
3335 for (grammar, value) in [
3336 ("<length> <color>", "1px red"),
3337 ("<length> && <color>", "red 1px"),
3338 ("<length> || <color>", "red"),
3339 ("auto | <length>", "auto"),
3340 ("[ auto | <length> ]?", ""),
3341 ("<length>*", "1px 2px"),
3342 ("<length>+", "1px 2px"),
3343 ("<length>#", "1px, 2px"),
3344 ("<length>{2,3}", "1px 2px 3px"),
3345 ("<length>#{2}", "1px, 2px"),
3346 ("[ <length>? <color>? ]!", "red"),
3347 ("rgb( <number>#{3} )", "rgb(1, 2, 3)"),
3348 ] {
3349 assert_matches(grammar, value);
3350 }
3351 for (grammar, value) in [
3352 ("<length> <color>", "red 1px"),
3353 ("<length> && <color>", "1px"),
3354 ("<length> || <color>", "auto"),
3355 ("<length>+", ""),
3356 ("<length>#", "1px 2px"),
3357 ("<length>{2,3}", "1px"),
3358 ("[ <length>? <color>? ]!", ""),
3359 ("rgb( <number>#{3} )", "rgb(1, 2)"),
3360 ] {
3361 assert_unmatched(grammar, value);
3362 }
3363 }
3364
3365 #[test]
3366 fn combinator_precedence_is_juxtaposition_then_and_then_double_or_then_or() {
3367 let grammar = "a b && c || d | e";
3368 for value in ["c a b", "a b c", "d", "e"] {
3369 assert_matches(grammar, value);
3370 }
3371 for value in ["a c", "b c", "a b d"] {
3372 assert_unmatched(grammar, value);
3373 }
3374 }
3375
3376 #[test]
3377 fn reference_depth_exhaustion_is_typed_and_provenanced() {
3378 let verdict = match_css_value_grammar_v0(
3379 "<calc-sum>",
3380 "calc(1px + 2px)",
3381 spec_grammar_registry(),
3382 CssValueGrammarBudgetV0 {
3383 max_reference_depth: 0,
3384 ..CssValueGrammarBudgetV0::default()
3385 },
3386 );
3387 assert!(matches!(
3388 verdict,
3389 CssValueGrammarVerdictV0::NotMatchedWithinBudget {
3390 budget: CssValueGrammarBudgetKindV0::ReferenceDepth,
3391 limit: 0,
3392 reference: Some(reference),
3393 ..
3394 } if reference == "calc-sum"
3395 ));
3396 }
3397
3398 #[test]
3399 fn malformed_grammar_is_a_defect_not_a_mismatch() {
3400 let verdict = match_css_value_grammar_v0(
3401 "[ <length> | <color>",
3402 "1px",
3403 spec_grammar_registry(),
3404 CssValueGrammarBudgetV0::default(),
3405 );
3406 assert!(matches!(
3407 verdict,
3408 CssValueGrammarVerdictV0::GrammarDefect { .. }
3409 ));
3410 }
3411
3412 #[test]
3413 fn property_and_type_references_use_the_pinned_registry() {
3414 assert_matches("<'box-sizing'>", "border-box");
3415 assert_matches("<color>", "rebeccapurple");
3416 assert_matches("<rgb()>", "rgb(1 2 3)");
3417 assert!(match_standard_property_value_v0("box-sizing", "content-box").is_matched());
3418 assert!(
3419 match_standard_property_value_v0("box-sizing", "inline-box").is_definite_mismatch()
3420 );
3421 }
3422
3423 #[test]
3424 fn numeric_reference_ranges_are_enforced() {
3425 assert_matches("<number [0,1]>", "0.5");
3426 assert_unmatched("<number [0,1]>", "2");
3427 assert_matches("<length [0,∞]>", "0px");
3428 assert_unmatched("<length [0,∞]>", "-1px");
3429 }
3430
3431 #[test]
3432 fn unitless_zero_matches_length_references_without_widening_other_dimensions() {
3433 for grammar in ["<length>", "<length-percentage>"] {
3434 assert_matches(grammar, "0");
3435 }
3436 for grammar in ["<percentage>", "<angle>", "<time>", "<resolution>"] {
3437 assert_unmatched(grammar, "0");
3438 }
3439 for value in ["1", "-1", "0.5"] {
3440 assert_unmatched("<length>", value);
3441 assert_unmatched("<length-percentage>", value);
3442 }
3443 let calc_verdict = match_css_value_grammar_v0(
3444 "<length> | <calc-sum>",
3445 "calc(0 + 2px)",
3446 spec_grammar_registry(),
3447 CssValueGrammarBudgetV0::default(),
3448 );
3449 assert!(
3450 !calc_verdict.is_matched(),
3451 "unitless zero cannot be added to a dimension inside calc(): {calc_verdict:?}"
3452 );
3453 assert!(
3454 match_css_value_grammar_v0(
3455 "<length> | <calc-sum>",
3456 "calc(0px + 2px)",
3457 spec_grammar_registry(),
3458 CssValueGrammarBudgetV0::default(),
3459 )
3460 .is_matched()
3461 );
3462 }
3463
3464 #[test]
3465 fn standard_properties_accept_unitless_zero_without_retyping_integer_consumers() {
3466 for (property, value) in [
3467 ("padding", "0"),
3468 ("margin", "0 auto"),
3469 ("border-width", "0 4px 6px"),
3470 ("width", "0"),
3471 ("z-index", "0"),
3472 ("opacity", "0"),
3473 ] {
3474 let verdict = validate_standard_property_value_v0(property, value);
3475 assert_eq!(
3476 verdict.class,
3477 CssValueValidationClassV0::Valid,
3478 "{property}: {value} should be valid: {verdict:?}"
3479 );
3480 }
3481 assert_eq!(
3482 classify_registered_property_declared_value_v0("0"),
3483 DeclaredValueKindV0::Integer
3484 );
3485 }
3486
3487 #[test]
3488 fn nullable_all_in_any_order_operands_can_satisfy_their_slots() {
3489 let grammar = "[ alpha? && [ none | beta ] && gamma? ]";
3490 for value in ["none", "alpha none", "none gamma", "gamma none alpha"] {
3491 assert_matches(grammar, value);
3492 }
3493 assert_unmatched(grammar, "none unexpected");
3494 }
3495
3496 #[test]
3497 fn nested_property_references_keep_inner_comma_repetition_reachable() {
3498 assert_matches("<'box-shadow-color'>", "red, blue");
3499 assert_unmatched("<'box-shadow-color'>", "red blue");
3500
3501 let grammar = "[ <'box-shadow-color'>? && [ none | <length>{2} ] [ <'box-shadow-blur'> <'box-shadow-spread'>? ]? && <'box-shadow-position'>? ]";
3502 assert_matches(grammar, "red none");
3503 assert_unmatched(grammar, "red none unexpected");
3504 }
3505
3506 #[test]
3507 fn sequence_omits_a_comma_only_with_an_omitted_adjacent_component() {
3508 let grammar = "<length>? , <color>";
3509 assert_matches(grammar, "red");
3510 assert_matches(grammar, "1px, red");
3511 assert_unmatched(grammar, ", red");
3512 assert_unmatched(grammar, "1px red");
3513 }
3514
3515 #[test]
3516 fn standard_keyword_grammars_remain_precise_through_nested_expansion() {
3517 for (property, value) in [("box-shadow", "none"), ("background", "transparent")] {
3518 let verdict = validate_standard_property_value_v0(property, value);
3519 assert_eq!(
3520 verdict.class,
3521 CssValueValidationClassV0::Valid,
3522 "{property}: {value} should be valid: {verdict:?}"
3523 );
3524 }
3525 for (property, value) in [
3526 ("box-shadow", "1px nonsense"),
3527 ("background", ", transparent"),
3528 ] {
3529 let verdict = validate_standard_property_value_v0(property, value);
3530 assert_ne!(
3531 verdict.class,
3532 CssValueValidationClassV0::Valid,
3533 "{property}: {value} must not be accepted: {verdict:?}"
3534 );
3535 }
3536 }
3537
3538 #[test]
3539 fn reviewed_compatibility_syntax_and_boundary_policy_are_applied_independently() {
3540 let compatibility_value =
3541 validate_standard_property_value_v0("-webkit-background-clip", "text");
3542 assert_eq!(compatibility_value.class, CssValueValidationClassV0::Valid);
3543 assert_eq!(
3544 compatibility_value.reason,
3545 CssValueValidationReasonV0::GrammarMatched
3546 );
3547 let registry = spec_grammar_registry();
3548 let compatibility_entry = registry.entry("properties", "-webkit-background-clip");
3549 assert!(
3550 compatibility_entry.is_some(),
3551 "compatibility property must remain registered"
3552 );
3553 let Some(compatibility_entry) = compatibility_entry else {
3554 return;
3555 };
3556 assert!(compatibility_entry.override_provenance.is_some());
3557
3558 let forward_tier =
3559 validate_standard_property_value_v0("background", "definitely-not-a-background");
3560 assert_eq!(
3561 forward_tier.class,
3562 CssValueValidationClassV0::NotValidatable
3563 );
3564 assert_eq!(
3565 forward_tier.reason,
3566 CssValueValidationReasonV0::ForwardTierGrammar
3567 );
3568 assert!(forward_tier.verdict.is_definite_mismatch());
3569
3570 let in_boundary = validate_standard_property_value_v0("box-sizing", "inline-box");
3571 assert_eq!(in_boundary.class, CssValueValidationClassV0::Invalid);
3572 assert_eq!(
3573 in_boundary.reason,
3574 CssValueValidationReasonV0::GrammarUnmatched
3575 );
3576 }
3577
3578 #[test]
3579 fn pinned_registry_rows_are_all_accounted_for_by_the_grammar_parser() {
3580 const MIN_PINNED_REGISTRY_ENTRY_COUNT: usize = 1_700;
3581
3582 let registry = spec_grammar_registry();
3583 let audit = audit_css_value_grammar_registry_v0(registry);
3584 assert_eq!(audit.total_entry_count, registry.total_entry_count());
3585 assert!(
3586 audit.total_entry_count >= MIN_PINNED_REGISTRY_ENTRY_COUNT,
3587 "pinned registry unexpectedly shrank below the audited coverage floor"
3588 );
3589 assert_eq!(audit.categories.len(), 5);
3590 assert_eq!(
3591 audit.parsed_entry_count + audit.missing_syntax_count + audit.grammar_defect_count,
3592 audit.total_entry_count
3593 );
3594 let properties = audit
3595 .categories
3596 .iter()
3597 .find(|category| category.category == "properties");
3598 assert_eq!(
3599 properties.map(|category| (
3600 category.entry_count,
3601 category.parsed_entry_count,
3602 category.missing_syntax_count,
3603 category.grammar_defect_count,
3604 )),
3605 Some((817, 812, 5, 0))
3606 );
3607 assert_eq!(
3608 (
3609 audit.parsed_entry_count,
3610 audit.missing_syntax_count,
3611 audit.grammar_defect_count,
3612 ),
3613 (1_529, 131, 57)
3614 );
3615 }
3616
3617 #[test]
3618 fn matched_compounds_project_through_existing_typed_and_lattice_domains() {
3619 let border = match_and_type_standard_property_value_v0("border-top", "1px solid red");
3620 assert!(border.verdict.is_matched(), "{:?}", border.verdict);
3621 assert!(matches!(
3622 &border.abstract_value,
3623 AbstractCssValueV0::Exact {
3624 typed: Some(typed), ..
3625 } if matches!(
3626 typed.as_ref(),
3627 AbstractCssTypedValueV0::Compound { leaves } if leaves.len() == 3
3628 )
3629 ));
3630 assert!(matches!(
3631 border.projection.as_ref().map(|projection| projection.lattice.root()),
3632 Some(ValueNodeV0::List { items, .. }) if items.len() == 3
3633 ));
3634
3635 let calc = match_and_type_css_value_grammar_v0(
3636 "calc( <length> '+' <length> )",
3637 "calc(1px + 2px)",
3638 spec_grammar_registry(),
3639 CssValueGrammarBudgetV0::default(),
3640 );
3641 assert!(calc.verdict.is_matched(), "{:?}", calc.verdict);
3642 assert!(matches!(
3643 calc.projection.as_ref().map(|projection| projection.lattice.root()),
3644 Some(ValueNodeV0::Function { name, arguments, .. })
3645 if *name == "calc" && arguments.len() == 3
3646 ));
3647
3648 let font_families =
3649 match_and_type_standard_property_value_v0("font-family", "serif, sans-serif");
3650 assert!(
3651 font_families.verdict.is_matched(),
3652 "{:?}",
3653 font_families.verdict
3654 );
3655 assert!(matches!(
3656 font_families
3657 .projection
3658 .as_ref()
3659 .map(|projection| projection.lattice.root()),
3660 Some(ValueNodeV0::List { .. })
3661 ));
3662 }
3663
3664 #[test]
3665 fn rejected_value_preserves_raw_bytes_and_carries_the_match_locus() {
3666 let source = " 1px nonsense red ";
3667 let result = match_and_type_standard_property_value_v0("border-top", source);
3668 assert!(matches!(
3669 result.verdict,
3670 CssValueGrammarVerdictV0::Unmatched {
3671 grammar,
3672 locus,
3673 } if grammar == "<line-width> || <line-style> || <color>"
3674 && locus.start == 2
3675 && locus.end == source.len() - 2
3676 ));
3677 assert_eq!(
3678 result.abstract_value,
3679 AbstractCssValueV0::Raw {
3680 value: source.to_string(),
3681 }
3682 );
3683 assert!(result.projection.is_none());
3684 }
3685
3686 #[test]
3687 fn validation_keeps_invalid_and_not_validatable_outcomes_distinct() {
3688 let invalid = validate_standard_property_value_v0("box-sizing", "inline-box");
3689 assert_eq!(invalid.class, CssValueValidationClassV0::Invalid);
3690 assert_eq!(invalid.reason, CssValueValidationReasonV0::GrammarUnmatched);
3691
3692 let closed_world_mismatch = validate_standard_property_value_v0("z-index", "banana");
3693 assert_eq!(
3694 closed_world_mismatch.class,
3695 CssValueValidationClassV0::Invalid
3696 );
3697 assert_eq!(
3698 closed_world_mismatch.reason,
3699 CssValueValidationReasonV0::GrammarUnmatched
3700 );
3701
3702 let defect = validate_registered_property_value_v0("<future-value>", "1px");
3703 assert_eq!(defect.class, CssValueValidationClassV0::NotValidatable);
3704 assert_eq!(defect.reason, CssValueValidationReasonV0::GrammarDefect);
3705
3706 let budget_verdict = match_css_value_grammar_v0(
3707 "<calc-sum>",
3708 "calc(1px + 2px)",
3709 spec_grammar_registry(),
3710 CssValueGrammarBudgetV0 {
3711 max_reference_depth: 0,
3712 ..CssValueGrammarBudgetV0::default()
3713 },
3714 );
3715 let budget = adjudicate_css_value_validation("1px", budget_verdict);
3716 assert_eq!(budget.class, CssValueValidationClassV0::NotValidatable);
3717 assert_eq!(
3718 budget.reason,
3719 CssValueValidationReasonV0::MatchBudgetExhausted
3720 );
3721
3722 let deferred = validate_standard_property_value_v0("width", "var(--width)");
3723 assert_eq!(deferred.class, CssValueValidationClassV0::NotValidatable);
3724 assert_eq!(
3725 deferred.reason,
3726 CssValueValidationReasonV0::DeferredSubstitution
3727 );
3728 }
3729
3730 #[test]
3731 fn validation_distinguishes_negative_dimensions_from_vendor_identifiers() {
3732 let valid_negative = validate_standard_property_value_v0("margin", "-10px");
3733 assert_eq!(valid_negative.class, CssValueValidationClassV0::Valid);
3734 assert_eq!(
3735 valid_negative.reason,
3736 CssValueValidationReasonV0::GrammarMatched
3737 );
3738 assert!(valid_negative.verdict.is_matched());
3739
3740 let invalid_negative = validate_standard_property_value_v0("margin", "-10px totally-bogus");
3741 assert_eq!(invalid_negative.class, CssValueValidationClassV0::Invalid);
3742 assert_eq!(
3743 invalid_negative.reason,
3744 CssValueValidationReasonV0::GrammarUnmatched
3745 );
3746 assert!(invalid_negative.verdict.is_definite_mismatch());
3747
3748 let vendor_identifier =
3749 validate_standard_property_value_v0("box-sizing", "-webkit-border-box");
3750 assert_eq!(
3751 vendor_identifier.class,
3752 CssValueValidationClassV0::NotValidatable
3753 );
3754 assert_eq!(
3755 vendor_identifier.reason,
3756 CssValueValidationReasonV0::VendorExtension
3757 );
3758 assert!(vendor_identifier.verdict.is_definite_mismatch());
3759 }
3760
3761 #[test]
3762 fn function_tokens_preserve_validation_boundaries() {
3763 let math_function = validate_standard_property_value_v0("width", "round(up, 101px, 10px)");
3764 assert_eq!(
3765 math_function.class,
3766 CssValueValidationClassV0::NotValidatable
3767 );
3768 assert_eq!(
3769 math_function.reason,
3770 CssValueValidationReasonV0::UnvalidatedStandardFunction
3771 );
3772 assert!(math_function.verdict.is_definite_mismatch());
3773
3774 let quoted_text = validate_standard_property_value_v0("content", "\"var(\"");
3775 assert_eq!(quoted_text.class, CssValueValidationClassV0::Valid);
3776 assert_eq!(
3777 quoted_text.reason,
3778 CssValueValidationReasonV0::GrammarMatched
3779 );
3780 assert!(quoted_text.verdict.is_matched());
3781
3782 let grid_function =
3783 validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
3784 assert_eq!(grid_function.class, CssValueValidationClassV0::Valid);
3785 assert_eq!(
3786 grid_function.reason,
3787 CssValueValidationReasonV0::GrammarMatched
3788 );
3789 }
3790
3791 #[test]
3792 fn recognized_functions_do_not_mask_adjacent_invalid_components() {
3793 for value in [
3794 "round(up, 101px, 10px)",
3795 "mod(10px, 3px)",
3796 "rem(10px, 3px)",
3797 "sin(45deg)",
3798 "pow(2, 3)",
3799 "sqrt(4)",
3800 "hypot(3px, 4px)",
3801 "abs(-10px)",
3802 ] {
3803 let validation = validate_standard_property_value_v0("width", value);
3804 assert_eq!(
3805 validation.class,
3806 CssValueValidationClassV0::NotValidatable,
3807 "{value} must remain non-definite until its function semantics are modeled"
3808 );
3809 assert_eq!(
3810 validation.reason,
3811 CssValueValidationReasonV0::UnvalidatedStandardFunction,
3812 "{value} must be attributed to the unvalidated standard-function channel"
3813 );
3814 }
3815
3816 let adjacent_scalar =
3817 validate_standard_property_value_v0("margin", "round(1, 2) totally-bogus");
3818 assert_eq!(adjacent_scalar.class, CssValueValidationClassV0::Invalid);
3819 assert_eq!(
3820 adjacent_scalar.reason,
3821 CssValueValidationReasonV0::GrammarUnmatched
3822 );
3823
3824 let unregistered_function =
3825 validate_standard_property_value_v0("width", "totally-unknown(1px)");
3826 assert_eq!(
3827 unregistered_function.class,
3828 CssValueValidationClassV0::NotValidatable
3829 );
3830 assert_eq!(
3831 unregistered_function.reason,
3832 CssValueValidationReasonV0::MatcherCoverageIncomplete
3833 );
3834
3835 let compound_value =
3836 validate_standard_property_value_v0("margin", "round(up, 10px, 1px) auto");
3837 assert_eq!(
3838 compound_value.class,
3839 CssValueValidationClassV0::NotValidatable
3840 );
3841 assert_eq!(
3842 compound_value.reason,
3843 CssValueValidationReasonV0::UnvalidatedStandardFunction
3844 );
3845 }
3846
3847 #[test]
3848 fn deferred_validation_uses_parsed_function_names() {
3849 for value in [
3850 "var(--width)",
3851 "env(safe-area-inset-top)",
3852 "attr(data-width type(<length>))",
3853 ] {
3854 let validation = validate_standard_property_value_v0("width", value);
3855 assert_eq!(
3856 validation.class,
3857 CssValueValidationClassV0::NotValidatable,
3858 "{value} must remain deferred"
3859 );
3860 assert_eq!(
3861 validation.reason,
3862 CssValueValidationReasonV0::DeferredSubstitution,
3863 "{value} must be attributed to an actual deferred function component"
3864 );
3865 }
3866
3867 let unvalidated_outer_function =
3868 validate_standard_property_value_v0("width", "round(up, calc(101px), 10px)");
3869 assert_eq!(
3870 unvalidated_outer_function.class,
3871 CssValueValidationClassV0::NotValidatable
3872 );
3873 assert_eq!(
3874 unvalidated_outer_function.reason,
3875 CssValueValidationReasonV0::UnvalidatedStandardFunction
3876 );
3877
3878 let similarly_named =
3879 validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
3880 assert_eq!(
3881 similarly_named.reason,
3882 CssValueValidationReasonV0::GrammarMatched
3883 );
3884 }
3885
3886 #[test]
3887 fn pinned_matcher_resolves_paint_math_and_grid_function_shapes() {
3888 for (property, value) in [
3889 ("fill", "#ff00aa"),
3890 ("stroke", "rgb(10 20 30)"),
3891 ("fill", "context-fill"),
3892 ("fill", "context-stroke"),
3893 ("stroke", "context-fill"),
3894 ("stroke", "context-stroke"),
3895 ("width", "calc(1px + 2px)"),
3896 ("width", "calc(100% - 8px)"),
3897 ("width", "calc(8px - 4px)"),
3898 ("width", "min(1px, 2px)"),
3899 ("width", "max(10%, 20%)"),
3900 ("width", "clamp(1px, 2px, 3px)"),
3901 ("opacity", "calc(0.4 + 0.1)"),
3902 ("animation-duration", "max(1s, 2s)"),
3903 ("rotate", "calc(10deg + 5deg)"),
3904 ("grid-template-columns", "minmax(101px, 1fr)"),
3905 ("grid-template-columns", "repeat(3, 1fr)"),
3906 ("grid-template-columns", "repeat(2, minmax(0, 1fr))"),
3907 ] {
3908 let validation = validate_standard_property_value_v0(property, value);
3909 assert_eq!(
3910 validation.class,
3911 CssValueValidationClassV0::Valid,
3912 "{property}: {value}: {validation:?}"
3913 );
3914 }
3915
3916 for (property, value) in [
3917 ("width", "calc(1px + 2)"),
3918 ("width", "calc(1px * 2px)"),
3919 ("width", "calc(100% -8px)"),
3920 ("width", "calc(100%- 8px)"),
3921 ("opacity", "calc(1 + 1px)"),
3922 ("animation-duration", "min(1s, 2px)"),
3923 ] {
3924 let validation = validate_standard_property_value_v0(property, value);
3925 assert_ne!(
3926 validation.class,
3927 CssValueValidationClassV0::Valid,
3928 "dimensionally invalid math was accepted: {property}: {value}: {validation:?}"
3929 );
3930 }
3931 }
3932
3933 #[test]
3934 fn css_tree_keyword_closure_regressions_are_valid_across_the_validator_adapter() {
3935 let validator = SpecStandardPropertyValueValidatorV0;
3936 let regressions = [
3937 ("fill", "context-fill"),
3938 ("fill", "context-stroke"),
3939 ("stroke", "context-fill"),
3940 ("stroke", "context-stroke"),
3941 ("zoom", "normal"),
3942 ("zoom", "reset"),
3943 ("baseline-shift", "baseline"),
3944 ("-webkit-mask", "border"),
3945 ("-webkit-mask", "content"),
3946 ("-webkit-mask", "padding"),
3947 ("-webkit-mask", "text"),
3948 ];
3949 assert_eq!(regressions.len(), 11);
3950 assert_eq!(
3951 regressions
3952 .iter()
3953 .map(|(_, value)| *value)
3954 .collect::<BTreeSet<_>>()
3955 .len(),
3956 9
3957 );
3958 for (property, value) in regressions {
3959 let validation = validate_standard_property_value_v0(property, value);
3960 assert_eq!(
3961 validation.class,
3962 CssValueValidationClassV0::Valid,
3963 "{property}: {value}: {validation:?}"
3964 );
3965 assert_eq!(
3966 validator
3967 .validate_standard_property_value(&PropertyNameV0::standard(property), value,),
3968 CascadeStandardValueVerdictV0::Matched,
3969 "{property}: {value} must cross the cascade validator adapter as matched"
3970 );
3971 }
3972 }
3973
3974 #[test]
3975 fn keyword_closure_certificate_binds_the_tested_pairs_and_nonempty_certification() {
3976 let authority = parse_closed_world_keyword_closure_certificate(
3977 CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
3978 );
3979 assert!(
3980 authority.is_some(),
3981 "the embedded keyword-closure certificate must pass its in-binary integrity checks"
3982 );
3983 let Some(authority) = authority else {
3984 return;
3985 };
3986 assert_eq!(authority.certified_properties.len(), 388);
3987 assert_eq!(authority.accepted_keywords_by_property.len(), 704);
3988 assert_eq!(
3989 authority
3990 .accepted_keywords_by_property
3991 .values()
3992 .map(BTreeSet::len)
3993 .sum::<usize>(),
3994 16_445
3995 );
3996 assert!(
3997 authority
3998 .accepted_keywords_by_property
3999 .get(&PropertyNameV0::canonical_standard_key("content"))
4000 .is_some_and(|keywords| keywords.contains("open-quote"))
4001 );
4002
4003 let wrong_digest = serde_json::from_str::<serde_json::Value>(
4004 CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
4005 );
4006 assert!(wrong_digest.is_ok(), "embedded certificate JSON must parse");
4007 let Ok(mut wrong_digest) = wrong_digest else {
4008 return;
4009 };
4010 wrong_digest["acceptedPairDigest"] = serde_json::Value::String("0".repeat(64));
4011 let wrong_digest_source = serde_json::to_string(&wrong_digest);
4012 assert!(
4013 wrong_digest_source.is_ok(),
4014 "mutated certificate JSON must serialize"
4015 );
4016 let Ok(wrong_digest_source) = wrong_digest_source else {
4017 return;
4018 };
4019 assert!(
4020 parse_closed_world_keyword_closure_certificate(&wrong_digest_source).is_none(),
4021 "the accepted-pair digest must be checked by the in-binary loader"
4022 );
4023
4024 let zero_sample = serde_json::from_str::<serde_json::Value>(
4025 CLOSED_WORLD_KEYWORD_CLOSURE_CERTIFICATE_SOURCE,
4026 );
4027 assert!(zero_sample.is_ok(), "embedded certificate JSON must parse");
4028 let Ok(mut zero_sample) = zero_sample else {
4029 return;
4030 };
4031 let zero_sample_property = zero_sample["propertyTests"].as_array().and_then(|entries| {
4032 entries.iter().find_map(|entry| {
4033 if entry["testedPairCount"].as_u64() == Some(0) {
4034 entry["property"].as_str().map(str::to_owned)
4035 } else {
4036 None
4037 }
4038 })
4039 });
4040 assert!(
4041 zero_sample_property.is_some(),
4042 "the exhaustive property table must include a zero-accepted property"
4043 );
4044 let Some(zero_sample_property) = zero_sample_property else {
4045 return;
4046 };
4047 let certified_properties = zero_sample["certifiedProperties"].as_array_mut();
4048 assert!(
4049 certified_properties.is_some(),
4050 "certified property list must be an array"
4051 );
4052 let Some(certified_properties) = certified_properties else {
4053 return;
4054 };
4055 certified_properties.push(serde_json::Value::String(zero_sample_property));
4056 let zero_sample_source = serde_json::to_string(&zero_sample);
4057 assert!(
4058 zero_sample_source.is_ok(),
4059 "mutated certificate JSON must serialize"
4060 );
4061 let Ok(zero_sample_source) = zero_sample_source else {
4062 return;
4063 };
4064 assert!(
4065 parse_closed_world_keyword_closure_certificate(&zero_sample_source).is_none(),
4066 "a property with zero tested pairs must never be certified"
4067 );
4068 }
4069
4070 #[test]
4071 fn closed_world_builtin_domains_are_bound_to_the_css_tree_witness_manifest() {
4072 let manifest = closed_world_builtin_token_profiles();
4073 assert!(
4074 manifest.is_some(),
4075 "the css-tree builtin token witness manifest must parse"
4076 );
4077 let Some(manifest) = manifest else {
4078 return;
4079 };
4080 assert_eq!(manifest.profile_count, 33);
4081
4082 let length = closed_world_builtin_profile("length");
4083 assert!(
4084 length.is_some(),
4085 "length must have a witnessed builtin profile"
4086 );
4087 let Some(length) = length else {
4088 return;
4089 };
4090 assert!(length.dimension.open);
4091 assert!(length.function_name.open);
4092 assert!(!length.number.open);
4093 assert_eq!(length.number.allowed, BTreeSet::from(["0".to_string()]));
4094
4095 let unknown_css_tree_type = closed_world_builtin_profile("whole-value");
4096 assert!(
4097 unknown_css_tree_type.is_some(),
4098 "an unknown css-tree type must receive a default-open profile"
4099 );
4100 let Some(unknown_css_tree_type) = unknown_css_tree_type else {
4101 return;
4102 };
4103 assert!(
4104 CLOSED_WORLD_TOKEN_KINDS
4105 .iter()
4106 .all(|kind| unknown_css_tree_type.domain(*kind).open)
4107 );
4108 assert!(closed_world_builtin_profile("named-color").is_none());
4109 }
4110
4111 #[test]
4112 fn incomplete_matcher_coverage_cannot_promote_unmatched_to_invalid() {
4113 let validation = adjudicate_css_value_validation_with_boundary(
4114 "fixture-value",
4115 CssValueGrammarVerdictV0::Unmatched {
4116 grammar: "<partially-modeled-type>".to_string(),
4117 locus: CssValueGrammarLocusV0 { start: 0, end: 13 },
4118 },
4119 SpecGrammarBoundaryClassificationV0::InBoundary,
4120 false,
4121 false,
4122 );
4123 assert_eq!(
4124 validation.class,
4125 CssValueValidationClassV0::NotValidatable,
4126 "MatcherCoverageIncomplete must remain non-definite"
4127 );
4128 assert_eq!(
4129 validation.reason,
4130 CssValueValidationReasonV0::MatcherCoverageIncomplete
4131 );
4132 }
4133
4134 #[test]
4135 fn accepted_keyword_authority_prevents_oracle_valid_ident_rejection() {
4136 let validation = validate_standard_property_value_v0("content", "open-quote");
4137 assert_eq!(
4138 validation.class,
4139 CssValueValidationClassV0::NotValidatable,
4140 "an oracle-accepted matcher gap cannot become a definite rejection: {validation:?}"
4141 );
4142 assert_eq!(
4143 validation.reason,
4144 CssValueValidationReasonV0::MatcherCoverageIncomplete
4145 );
4146 }
4147
4148 #[test]
4149 fn identifier_rejection_authority_property_surface_is_pinned() {
4150 let registry = spec_grammar_registry();
4151 let property_count = registry
4152 .entries("properties")
4153 .iter()
4154 .filter(|entry| {
4155 let property = PropertyNameV0::standard(entry.name.as_str());
4156 let property_key = PropertyNameV0::canonical_standard_key(entry.name.as_str());
4157 closed_world_keyword_authority().is_some_and(|authority| {
4158 authority
4159 .accepted_keywords_by_property
4160 .contains_key(&property_key)
4161 }) && cached_standard_property_closed_world_token_profile(&property, registry)
4162 .is_some_and(|profile| !profile.ident.open)
4163 })
4164 .count();
4165
4166 println!("identifierRejectionAuthorityPropertyCount={property_count}");
4167 assert_eq!(property_count, 505);
4168 }
4169
4170 #[test]
4171 fn open_preprocessor_function_keeps_compound_ident_rejection_non_definite() {
4172 let validation =
4173 validate_standard_property_value_v0("box-shadow", "inset 0 0 0 2px fade(#0ea5e9, 24%)");
4174 assert_eq!(
4175 validation.class,
4176 CssValueValidationClassV0::NotValidatable,
4177 "an unevaluated preprocessor function must preserve uncertainty: {validation:?}"
4178 );
4179 assert_eq!(
4180 validation.reason,
4181 CssValueValidationReasonV0::MatcherCoverageIncomplete
4182 );
4183 }
4184
4185 #[test]
4186 fn closed_world_token_kinds_certify_impossible_standard_values() {
4187 for (property, value) in [
4188 ("color", "12px"),
4189 ("color", "definitely-not-a-color"),
4190 ("width", "red"),
4191 ("border-top", "1px nonsense red"),
4192 ("fill", "bogusvalue"),
4193 ("z-index", "banana"),
4194 ("margin", "-10px totally-bogus"),
4195 ] {
4196 let validation = validate_standard_property_value_v0(property, value);
4197 assert_eq!(
4198 validation.class,
4199 CssValueValidationClassV0::Invalid,
4200 "{property}: {value}: {validation:?}"
4201 );
4202 assert_eq!(
4203 validation.reason,
4204 CssValueValidationReasonV0::GrammarUnmatched,
4205 "{property}: {value}: {validation:?}"
4206 );
4207 }
4208 }
4209
4210 #[test]
4211 fn valid_declaration_corpus_covers_closed_ident_edges_without_definite_rejection() {
4212 let declarations = [
4213 ("color", "red"),
4214 ("color", "#ff00aa"),
4215 ("fill", "#f0f"),
4216 ("stroke", "#ff00ff"),
4217 ("width", "calc(10px + 2px)"),
4218 ("width", "min(10px, 20px)"),
4219 ("width", "max(10%, 20%)"),
4220 ("width", "clamp(1px, 2px, 3px)"),
4221 ("height", "calc(50% + 2px)"),
4222 ("margin", "calc(1rem + 2px)"),
4223 ("padding", "min(1rem, 2rem)"),
4224 ("row-gap", "clamp(1px, 2px, 3px)"),
4225 ("column-gap", "max(1%, 2%)"),
4226 ("gap", "clamp(1px, 2px, 3px)"),
4227 ("opacity", "calc(0.5 + 0.1)"),
4228 ("line-height", "min(1.2, 1.5)"),
4229 ("animation-duration", "calc(1s + 200ms)"),
4230 ("transition-duration", "max(1s, 2s)"),
4231 ("rotate", "calc(10deg + 5deg)"),
4232 ("grid-template-columns", "minmax(101px, 1fr)"),
4233 ("grid-template-columns", "repeat(3, 1fr)"),
4234 ("grid-template-columns", "repeat(2, minmax(0, 1fr))"),
4235 ("grid-template-columns", "1fr 2fr"),
4236 ("border-top", "1px solid red"),
4237 ("margin", "0 auto"),
4238 ("padding", "1px 2px"),
4239 ("display", "grid"),
4240 ("position", "absolute"),
4241 ("inset", "0"),
4242 ("top", "1px"),
4243 ("z-index", "2"),
4244 ("font-weight", "700"),
4245 ("font-size", "16px"),
4246 ("background-color", "rebeccapurple"),
4247 ("border-radius", "4px"),
4248 ("flex-grow", "1"),
4249 ("flex-shrink", "0"),
4250 ("order", "-1"),
4251 ("transform", "rotate(45deg)"),
4252 ("background-image", "linear-gradient(red, blue)"),
4253 ("color", "CanvasText"),
4254 ("fill", "context-fill"),
4255 ("stroke", "context-stroke"),
4256 ("zoom", "normal"),
4257 ("baseline-shift", "baseline"),
4258 ("-webkit-mask", "border"),
4259 ("-webkit-mask", "text"),
4260 ("content", "open-quote"),
4261 ];
4262 assert_eq!(declarations.len(), 48);
4263 let definite_rejections = declarations
4264 .iter()
4265 .filter_map(|(property, value)| {
4266 let property_name = PropertyNameV0::from_authored(*property);
4267 assert!(
4268 !standard_property_closed_world_token_kind_mismatch(
4269 &property_name,
4270 value,
4271 spec_grammar_registry(),
4272 ),
4273 "a valid declaration was outside the derived open/closed token profile: {property}: {value}"
4274 );
4275 let validation = validate_standard_property_value_v0(property, value);
4276 (validation.class == CssValueValidationClassV0::Invalid)
4277 .then_some((*property, *value, validation))
4278 })
4279 .collect::<Vec<_>>();
4280 assert!(
4281 definite_rejections.is_empty(),
4282 "valid declarations were definitely rejected: {definite_rejections:?}"
4283 );
4284 }
4285
4286 #[test]
4287 fn validation_consumer_policy_table_covers_every_live_consumer() {
4288 assert_eq!(CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0.len(), 5);
4289 assert_eq!(
4290 CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0
4291 .iter()
4292 .map(|policy| policy.consumer)
4293 .collect::<Vec<_>>(),
4294 vec![
4295 "checker.registeredPropertyTypeMismatch",
4296 "checker.invalidPropertyValue",
4297 "cascade.postSubstitutionStandardProperty",
4298 "scss.nativeCssFunctionParameter",
4299 "scss.nativeCssFunctionReturn",
4300 ]
4301 );
4302 for policy in CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0 {
4303 assert_eq!(policy.matched, "accept");
4304 assert!(matches!(policy.unmatched, "diagnostic" | "reject"));
4305 assert!(matches!(
4306 policy.forward_tier_unmatched,
4307 "not-applicable" | "not-validatable"
4308 ));
4309 assert!(matches!(policy.grammar_defect, "silent" | "unknown"));
4310 assert!(matches!(policy.budget_exhausted, "silent" | "unknown"));
4311 }
4312 }
4313
4314 #[test]
4315 fn cascade_validator_adapter_preserves_spec_grammar_outcomes() {
4316 let validator = SpecStandardPropertyValueValidatorV0;
4317
4318 assert_eq!(
4319 validator.validate_standard_property_value(&PropertyNameV0::standard("color"), "red"),
4320 CascadeStandardValueVerdictV0::Matched
4321 );
4322 assert_eq!(
4323 validator.validate_standard_property_value(&PropertyNameV0::standard("color"), "12px"),
4324 CascadeStandardValueVerdictV0::Unmatched
4325 );
4326 assert_eq!(
4327 validator.validate_standard_property_value(
4328 &PropertyNameV0::standard("box-sizing"),
4329 "inline-box",
4330 ),
4331 CascadeStandardValueVerdictV0::Unmatched
4332 );
4333 assert_eq!(
4334 validator.validate_standard_property_value(
4335 &PropertyNameV0::standard("color"),
4336 "var(--tone)",
4337 ),
4338 CascadeStandardValueVerdictV0::Unknown
4339 );
4340 }
4341}