1use std::collections::{BTreeSet, HashMap};
2use std::sync::{Arc, OnceLock, RwLock};
3
4use omena_evidence_graph::{
5 EvidenceNodeKeyV0, EvidenceNodeSeedV0, ExternalToolRunWitnessV0, FamilyStampV0, GuaranteeKindV0,
6};
7use omena_spec_audit::{
8 SpecGrammarBoundaryClassificationV0, SpecGrammarRegistryV0, spec_grammar_registry,
9};
10use omena_value_lattice::{
11 CssValueComponentKindV0, CssValueComponentV0, DeclarationValueLensV0, ValueNodeV0,
12 css_value_component_stream, declaration_value_lens, parse_numeric_value_with_unit,
13};
14use serde::{Deserialize, Serialize};
15
16use crate::{
17 AbstractCssTypedScalarValueV0, AbstractCssTypedValueV0, AbstractCssValueV0,
18 DeclaredNumericTypeV0, DeclaredValueKindV0, abstract_css_typed_scalar_from_text,
19 classify_registered_property_declared_value_v0,
20};
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
23#[serde(rename_all = "camelCase")]
24pub struct CssValueGrammarBudgetV0 {
25 pub max_match_steps: usize,
26 pub max_reference_depth: usize,
27 pub max_states: usize,
28}
29
30impl Default for CssValueGrammarBudgetV0 {
31 fn default() -> Self {
32 Self {
33 max_match_steps: 50_000,
34 max_reference_depth: 64,
35 max_states: 4_096,
36 }
37 }
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
41#[serde(rename_all = "camelCase")]
42pub enum CssValueGrammarBudgetKindV0 {
43 MatchSteps,
44 ReferenceDepth,
45 CandidateStates,
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
49#[serde(rename_all = "camelCase")]
50pub struct CssValueGrammarLocusV0 {
51 pub start: usize,
52 pub end: usize,
53}
54
55#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
56#[serde(
57 tag = "kind",
58 rename_all = "camelCase",
59 rename_all_fields = "camelCase"
60)]
61pub enum CssValueGrammarVerdictV0 {
62 Matched {
63 grammar: String,
64 consumed_components: usize,
65 },
66 Unmatched {
67 grammar: String,
68 locus: CssValueGrammarLocusV0,
69 },
70 NotMatchedWithinBudget {
71 grammar: String,
72 locus: CssValueGrammarLocusV0,
73 budget: CssValueGrammarBudgetKindV0,
74 limit: usize,
75 reference: Option<String>,
76 },
77 GrammarDefect {
78 grammar: String,
79 offset: usize,
80 code: String,
81 detail: String,
82 },
83}
84
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
86#[serde(rename_all = "camelCase")]
87pub enum CssValueValidationClassV0 {
88 Valid,
89 Invalid,
90 NotValidatable,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
94#[serde(rename_all = "camelCase")]
95pub enum CssValueValidationReasonV0 {
96 GrammarMatched,
97 GrammarUnmatched,
98 GrammarDefect,
99 MatchBudgetExhausted,
100 DeferredSubstitution,
101 VendorExtension,
102 ForwardTierGrammar,
103 UnvalidatedStandardFunction,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
107#[serde(rename_all = "camelCase")]
108pub struct CssValueValidationV0 {
109 pub class: CssValueValidationClassV0,
110 pub reason: CssValueValidationReasonV0,
111 pub verdict: CssValueGrammarVerdictV0,
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
115#[serde(rename_all = "camelCase")]
116pub struct CssValueValidationConsumerPolicyV0 {
117 pub consumer: &'static str,
118 pub matched: &'static str,
119 pub unmatched: &'static str,
120 pub forward_tier_unmatched: &'static str,
121 pub grammar_defect: &'static str,
122 pub budget_exhausted: &'static str,
123}
124
125pub const CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0: [CssValueValidationConsumerPolicyV0; 4] = [
126 CssValueValidationConsumerPolicyV0 {
127 consumer: "checker.registeredPropertyTypeMismatch",
128 matched: "accept",
129 unmatched: "diagnostic",
130 forward_tier_unmatched: "not-applicable",
131 grammar_defect: "silent",
132 budget_exhausted: "silent",
133 },
134 CssValueValidationConsumerPolicyV0 {
135 consumer: "checker.invalidPropertyValue",
136 matched: "accept",
137 unmatched: "diagnostic",
138 forward_tier_unmatched: "not-validatable",
139 grammar_defect: "silent",
140 budget_exhausted: "silent",
141 },
142 CssValueValidationConsumerPolicyV0 {
143 consumer: "scss.nativeCssFunctionParameter",
144 matched: "accept",
145 unmatched: "reject",
146 forward_tier_unmatched: "not-applicable",
147 grammar_defect: "unknown",
148 budget_exhausted: "unknown",
149 },
150 CssValueValidationConsumerPolicyV0 {
151 consumer: "scss.nativeCssFunctionReturn",
152 matched: "accept",
153 unmatched: "reject",
154 forward_tier_unmatched: "not-applicable",
155 grammar_defect: "unknown",
156 budget_exhausted: "unknown",
157 },
158];
159
160pub fn css_value_grammar_external_tool_evidence_v0(
163 tool_name: &str,
164 tool_version: &str,
165 input_digest: &str,
166 exit_status: i32,
167) -> EvidenceNodeSeedV0 {
168 let witness = ExternalToolRunWitnessV0 {
169 tool_name: tool_name.to_string(),
170 tool_version: tool_version.to_string(),
171 input_digest: input_digest.to_string(),
172 exit_status,
173 };
174 EvidenceNodeSeedV0::with_family(
175 EvidenceNodeKeyV0::new(
176 "omena-abstract-value.value-grammar-differential",
177 input_digest,
178 ),
179 vec![
180 format!("externalTool:{tool_name}"),
181 format!("toolVersion:{tool_version}"),
182 format!("exitStatus:{exit_status}"),
183 ],
184 GuaranteeKindV0::for_label_less_family(),
185 FamilyStampV0::external_tool(&witness),
186 )
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
190#[serde(rename_all = "camelCase")]
191pub struct CssValueGrammarRegistryAuditV0 {
192 pub total_entry_count: usize,
193 pub parsed_entry_count: usize,
194 pub missing_syntax_count: usize,
195 pub grammar_defect_count: usize,
196 pub categories: Vec<CssValueGrammarCategoryAuditV0>,
197 pub defects: Vec<CssValueGrammarDefectV0>,
198}
199
200#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
201#[serde(rename_all = "camelCase")]
202pub struct CssValueGrammarCategoryAuditV0 {
203 pub category: String,
204 pub entry_count: usize,
205 pub parsed_entry_count: usize,
206 pub missing_syntax_count: usize,
207 pub grammar_defect_count: usize,
208}
209
210#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
211#[serde(rename_all = "camelCase")]
212pub struct CssValueGrammarDefectV0 {
213 pub category: String,
214 pub name: String,
215 pub offset: usize,
216 pub code: String,
217 pub detail: String,
218}
219
220#[derive(Debug, Clone, PartialEq)]
221pub struct CssValueGrammarTypedMatchV0<'a> {
222 pub verdict: CssValueGrammarVerdictV0,
223 pub abstract_value: AbstractCssValueV0,
224 pub projection: Option<CssValueTypedProjectionV0<'a>>,
225}
226
227#[derive(Debug, Clone, PartialEq)]
228pub struct CssValueTypedProjectionV0<'a> {
229 pub lattice: DeclarationValueLensV0<'a>,
230 pub scalar_leaves: Vec<AbstractCssTypedScalarValueV0>,
231}
232
233impl CssValueGrammarVerdictV0 {
234 pub const fn is_matched(&self) -> bool {
235 matches!(self, Self::Matched { .. })
236 }
237
238 pub const fn is_definite_mismatch(&self) -> bool {
239 matches!(self, Self::Unmatched { .. })
240 }
241
242 pub const fn is_validatable(&self) -> bool {
243 matches!(self, Self::Matched { .. } | Self::Unmatched { .. })
244 }
245}
246
247pub fn audit_css_value_grammar_registry_v0(
251 registry: &SpecGrammarRegistryV0,
252) -> CssValueGrammarRegistryAuditV0 {
253 let mut categories = Vec::new();
254 let mut defects = Vec::new();
255 let mut parsed_entry_count = 0usize;
256 let mut missing_syntax_count = 0usize;
257 for category in ["atrules", "functions", "properties", "selectors", "types"] {
258 let entries = registry.entries(category);
259 let mut category_parsed = 0usize;
260 let mut category_missing = 0usize;
261 let defect_start = defects.len();
262 for entry in entries {
263 let Some(grammar) = entry.syntax.as_deref() else {
264 category_missing += 1;
265 missing_syntax_count += 1;
266 continue;
267 };
268 match VdsParser::new(strip_matching_quotes(grammar.trim())).parse() {
269 Ok(_) => {
270 category_parsed += 1;
271 parsed_entry_count += 1;
272 }
273 Err(error) => defects.push(CssValueGrammarDefectV0 {
274 category: category.to_string(),
275 name: entry.name.clone(),
276 offset: error.offset,
277 code: error.code.to_string(),
278 detail: error.detail,
279 }),
280 }
281 }
282 categories.push(CssValueGrammarCategoryAuditV0 {
283 category: category.to_string(),
284 entry_count: entries.len(),
285 parsed_entry_count: category_parsed,
286 missing_syntax_count: category_missing,
287 grammar_defect_count: defects.len() - defect_start,
288 });
289 }
290 CssValueGrammarRegistryAuditV0 {
291 total_entry_count: registry.total_entry_count(),
292 parsed_entry_count,
293 missing_syntax_count,
294 grammar_defect_count: defects.len(),
295 categories,
296 defects,
297 }
298}
299
300pub fn match_standard_property_value_v0(property: &str, value: &str) -> CssValueGrammarVerdictV0 {
303 let registry = spec_grammar_registry();
304 let Some(entry) = registry.entry("properties", property) else {
305 return grammar_defect(
306 "",
307 0,
308 "unknownProperty",
309 format!("property {property:?} is absent from the pinned registry"),
310 );
311 };
312 let Some(grammar) = entry.syntax.as_deref() else {
313 return grammar_defect(
314 "",
315 0,
316 "missingPropertyGrammar",
317 format!("property {property:?} has no syntax in the pinned registry"),
318 );
319 };
320 if matches!(
321 classify_registered_property_declared_value_v0(value),
322 DeclaredValueKindV0::CssWide
323 ) {
324 return CssValueGrammarVerdictV0::Matched {
325 grammar: grammar.to_string(),
326 consumed_components: 1,
327 };
328 }
329 let components = match css_value_component_stream(value, 0) {
330 Ok(components) => components,
331 Err(error) => {
332 return grammar_defect(
333 grammar,
334 error.span.start,
335 "invalidValueTokenStream",
336 error.message,
337 );
338 }
339 };
340 let normalized = strip_matching_quotes(grammar.trim());
341 let expression = match cached_pinned_vds_expression(normalized) {
342 Ok(expression) => expression,
343 Err(error) => {
344 return grammar_defect(grammar, error.offset, error.code, error.detail);
345 }
346 };
347 match_css_value_grammar_components_with_expression_v0(
348 grammar,
349 &components,
350 registry,
351 CssValueGrammarBudgetV0::default(),
352 expression.as_ref(),
353 true,
354 )
355}
356
357pub fn match_registered_property_value_v0(syntax: &str, value: &str) -> CssValueGrammarVerdictV0 {
359 let grammar = strip_matching_quotes(syntax.trim()).trim();
360 if grammar == "*" {
361 return match css_value_component_stream(value, 0) {
362 Ok(components) => CssValueGrammarVerdictV0::Matched {
363 grammar: syntax.to_string(),
364 consumed_components: components.len(),
365 },
366 Err(error) => grammar_defect(
367 syntax,
368 error.span.start,
369 "invalidValueTokenStream",
370 error.message,
371 ),
372 };
373 }
374 if matches!(
375 classify_registered_property_declared_value_v0(value),
376 DeclaredValueKindV0::CssWide
377 ) {
378 return CssValueGrammarVerdictV0::Matched {
379 grammar: syntax.to_string(),
380 consumed_components: 1,
381 };
382 }
383 match_css_value_grammar_v0(
384 grammar,
385 value,
386 spec_grammar_registry(),
387 CssValueGrammarBudgetV0::default(),
388 )
389}
390
391pub fn validate_standard_property_value_v0(property: &str, value: &str) -> CssValueValidationV0 {
392 let classification = spec_grammar_registry()
393 .entry("properties", property)
394 .map(|entry| entry.boundary.classification)
395 .unwrap_or(SpecGrammarBoundaryClassificationV0::InBoundary);
396 adjudicate_css_value_validation_with_boundary(
397 value,
398 match_standard_property_value_v0(property, value),
399 classification,
400 )
401}
402
403pub fn validate_registered_property_value_v0(syntax: &str, value: &str) -> CssValueValidationV0 {
404 adjudicate_css_value_validation(value, match_registered_property_value_v0(syntax, value))
405}
406
407fn adjudicate_css_value_validation(
408 value: &str,
409 verdict: CssValueGrammarVerdictV0,
410) -> CssValueValidationV0 {
411 adjudicate_css_value_validation_with_boundary(
412 value,
413 verdict,
414 SpecGrammarBoundaryClassificationV0::InBoundary,
415 )
416}
417
418fn adjudicate_css_value_validation_with_boundary(
419 value: &str,
420 verdict: CssValueGrammarVerdictV0,
421 classification: SpecGrammarBoundaryClassificationV0,
422) -> CssValueValidationV0 {
423 let components = css_value_component_stream(value, 0).ok();
424 let has_unvalidated_standard_function = matches!(
425 &verdict,
426 CssValueGrammarVerdictV0::Unmatched { grammar, locus }
427 if classification == SpecGrammarBoundaryClassificationV0::InBoundary
428 && components.as_deref().is_some_and(|components| {
429 recognized_standard_functions_explain_unmatched_value(
430 grammar,
431 components,
432 *locus,
433 )
434 })
435 );
436 let (class, reason) = if components
437 .as_deref()
438 .is_some_and(contains_deferred_css_value)
439 {
440 (
441 CssValueValidationClassV0::NotValidatable,
442 CssValueValidationReasonV0::DeferredSubstitution,
443 )
444 } else if components
445 .as_deref()
446 .is_some_and(has_leading_vendor_identifier)
447 {
448 (
449 CssValueValidationClassV0::NotValidatable,
450 CssValueValidationReasonV0::VendorExtension,
451 )
452 } else if has_unvalidated_standard_function {
453 (
454 CssValueValidationClassV0::NotValidatable,
455 CssValueValidationReasonV0::UnvalidatedStandardFunction,
456 )
457 } else {
458 match verdict {
459 CssValueGrammarVerdictV0::Matched { .. } => (
460 CssValueValidationClassV0::Valid,
461 CssValueValidationReasonV0::GrammarMatched,
462 ),
463 CssValueGrammarVerdictV0::Unmatched { .. }
464 if classification == SpecGrammarBoundaryClassificationV0::ForwardTier =>
465 {
466 (
467 CssValueValidationClassV0::NotValidatable,
468 CssValueValidationReasonV0::ForwardTierGrammar,
469 )
470 }
471 CssValueGrammarVerdictV0::Unmatched { .. } => (
472 CssValueValidationClassV0::Invalid,
473 CssValueValidationReasonV0::GrammarUnmatched,
474 ),
475 CssValueGrammarVerdictV0::NotMatchedWithinBudget { .. } => (
476 CssValueValidationClassV0::NotValidatable,
477 CssValueValidationReasonV0::MatchBudgetExhausted,
478 ),
479 CssValueGrammarVerdictV0::GrammarDefect { .. } => (
480 CssValueValidationClassV0::NotValidatable,
481 CssValueValidationReasonV0::GrammarDefect,
482 ),
483 }
484 };
485 CssValueValidationV0 {
486 class,
487 reason,
488 verdict,
489 }
490}
491
492fn recognized_standard_functions_explain_unmatched_value(
493 grammar: &str,
494 components: &[CssValueComponentV0],
495 locus: CssValueGrammarLocusV0,
496) -> bool {
497 let has_locus_function = components.iter().any(|component| {
498 component.span.start < locus.end
499 && locus.start < component.span.end
500 && component_is_recognized_standard_function(component)
501 });
502 if !has_locus_function {
503 return false;
504 }
505
506 let normalized = strip_matching_quotes(grammar.trim());
507 let Ok(expression) = cached_pinned_vds_expression(normalized) else {
508 return false;
509 };
510 let mut context = MatchContext {
511 registry: spec_grammar_registry(),
512 budget: CssValueGrammarBudgetV0::default(),
513 match_steps: 0,
514 first_stop: None,
515 grammar_cache: HashMap::new(),
516 cache_registered_grammars: true,
517 allow_unvalidated_standard_function_references: true,
518 };
519 context
520 .match_expression(expression.as_ref(), components, 0, 0)
521 .contains(&components.len())
522}
523
524fn component_is_recognized_standard_function(component: &CssValueComponentV0) -> bool {
525 matches!(
526 &component.kind,
527 CssValueComponentKindV0::Function { name, .. }
528 if recognized_standard_function_names().contains(name)
529 )
530}
531
532fn recognized_standard_function_names() -> &'static BTreeSet<String> {
533 static NAMES: OnceLock<BTreeSet<String>> = OnceLock::new();
534 NAMES.get_or_init(|| {
535 spec_grammar_registry()
536 .entries("functions")
537 .iter()
538 .filter(|entry| {
539 entry.boundary.classification == SpecGrammarBoundaryClassificationV0::InBoundary
540 })
541 .filter_map(|entry| entry.name.strip_suffix("()"))
542 .filter(|name| !name.starts_with('-') && !is_deferred_css_function_name(name))
543 .map(str::to_string)
544 .collect()
545 })
546}
547
548fn is_deferred_css_function_name(name: &str) -> bool {
549 matches!(
550 name,
551 "var" | "env" | "attr" | "calc" | "min" | "max" | "clamp"
552 )
553}
554
555fn contains_deferred_css_value(components: &[CssValueComponentV0]) -> bool {
556 components.iter().any(|component| match &component.kind {
557 CssValueComponentKindV0::Function { name, arguments } => {
558 is_deferred_css_function_name(name) || contains_deferred_css_value(arguments)
559 }
560 CssValueComponentKindV0::Parenthesized { values }
561 | CssValueComponentKindV0::Bracketed { values }
562 | CssValueComponentKindV0::Braced { values } => contains_deferred_css_value(values),
563 CssValueComponentKindV0::Ident
564 | CssValueComponentKindV0::Number
565 | CssValueComponentKindV0::Percentage
566 | CssValueComponentKindV0::Dimension
567 | CssValueComponentKindV0::Hash
568 | CssValueComponentKindV0::String
569 | CssValueComponentKindV0::Url
570 | CssValueComponentKindV0::Comma
571 | CssValueComponentKindV0::Slash
572 | CssValueComponentKindV0::Delimiter => false,
573 })
574}
575
576fn has_leading_vendor_identifier(components: &[CssValueComponentV0]) -> bool {
577 components.first().is_some_and(|component| {
578 matches!(component.kind, CssValueComponentKindV0::Ident) && component.text.starts_with('-')
579 })
580}
581
582pub fn match_and_type_standard_property_value_v0<'a>(
585 property: &str,
586 value: &'a str,
587) -> CssValueGrammarTypedMatchV0<'a> {
588 typed_match_result(match_standard_property_value_v0(property, value), value)
589}
590
591pub fn match_and_type_css_value_grammar_v0<'a>(
593 grammar: &str,
594 value: &'a str,
595 registry: &SpecGrammarRegistryV0,
596 budget: CssValueGrammarBudgetV0,
597) -> CssValueGrammarTypedMatchV0<'a> {
598 typed_match_result(
599 match_css_value_grammar_v0(grammar, value, registry, budget),
600 value,
601 )
602}
603
604fn typed_match_result<'a>(
605 verdict: CssValueGrammarVerdictV0,
606 value: &'a str,
607) -> CssValueGrammarTypedMatchV0<'a> {
608 if !verdict.is_matched() {
609 return CssValueGrammarTypedMatchV0 {
610 verdict,
611 abstract_value: AbstractCssValueV0::Raw {
612 value: value.to_string(),
613 },
614 projection: None,
615 };
616 }
617 let components = match css_value_component_stream(value, 0) {
618 Ok(components) => components,
619 Err(error) => {
620 return CssValueGrammarTypedMatchV0 {
621 verdict: grammar_defect(
622 verdict_grammar(&verdict),
623 error.span.start,
624 "typedProjectionTokenStreamDrift",
625 error.message,
626 ),
627 abstract_value: AbstractCssValueV0::Raw {
628 value: value.to_string(),
629 },
630 projection: None,
631 };
632 }
633 };
634 let mut scalar_leaves = Vec::new();
635 collect_typed_scalar_leaves(&components, &mut scalar_leaves);
636 let lattice = declaration_value_lens(value, 0);
637 let typed = typed_value_from_projection(&lattice, &scalar_leaves).map(Box::new);
638 CssValueGrammarTypedMatchV0 {
639 verdict,
640 abstract_value: AbstractCssValueV0::Exact {
641 value: value.to_string(),
642 typed,
643 },
644 projection: Some(CssValueTypedProjectionV0 {
645 lattice,
646 scalar_leaves,
647 }),
648 }
649}
650
651fn verdict_grammar(verdict: &CssValueGrammarVerdictV0) -> &str {
652 match verdict {
653 CssValueGrammarVerdictV0::Matched { grammar, .. }
654 | CssValueGrammarVerdictV0::Unmatched { grammar, .. }
655 | CssValueGrammarVerdictV0::NotMatchedWithinBudget { grammar, .. }
656 | CssValueGrammarVerdictV0::GrammarDefect { grammar, .. } => grammar,
657 }
658}
659
660fn collect_typed_scalar_leaves(
661 components: &[CssValueComponentV0],
662 leaves: &mut Vec<AbstractCssTypedScalarValueV0>,
663) {
664 for component in components {
665 if let Some(value) = abstract_css_typed_scalar_from_text(component.text.as_str()) {
666 leaves.push(value);
667 continue;
668 }
669 match &component.kind {
670 CssValueComponentKindV0::Function { arguments, .. }
671 | CssValueComponentKindV0::Parenthesized { values: arguments }
672 | CssValueComponentKindV0::Bracketed { values: arguments }
673 | CssValueComponentKindV0::Braced { values: arguments } => {
674 collect_typed_scalar_leaves(arguments, leaves);
675 }
676 CssValueComponentKindV0::Ident
677 | CssValueComponentKindV0::Number
678 | CssValueComponentKindV0::Percentage
679 | CssValueComponentKindV0::Dimension
680 | CssValueComponentKindV0::Hash
681 | CssValueComponentKindV0::String
682 | CssValueComponentKindV0::Url
683 | CssValueComponentKindV0::Comma
684 | CssValueComponentKindV0::Slash
685 | CssValueComponentKindV0::Delimiter => {}
686 }
687 }
688}
689
690fn typed_value_from_projection(
691 lattice: &DeclarationValueLensV0<'_>,
692 scalar_leaves: &[AbstractCssTypedScalarValueV0],
693) -> Option<AbstractCssTypedValueV0> {
694 match (lattice.root(), scalar_leaves) {
695 (ValueNodeV0::List { .. } | ValueNodeV0::Function { .. }, [_, ..]) | (_, [_, _, ..]) => {
696 Some(AbstractCssTypedValueV0::Compound {
697 leaves: scalar_leaves.to_vec(),
698 })
699 }
700 (_, [value]) => Some(AbstractCssTypedValueV0::Exact {
701 value: value.clone(),
702 }),
703 (_, []) => None,
704 }
705}
706
707pub fn match_css_value_grammar_v0(
709 grammar: &str,
710 value: &str,
711 registry: &SpecGrammarRegistryV0,
712 budget: CssValueGrammarBudgetV0,
713) -> CssValueGrammarVerdictV0 {
714 let components = match css_value_component_stream(value, 0) {
715 Ok(components) => components,
716 Err(error) => {
717 return grammar_defect(
718 grammar,
719 error.span.start,
720 "invalidValueTokenStream",
721 error.message,
722 );
723 }
724 };
725 match_css_value_grammar_components_v0(grammar, &components, registry, budget)
726}
727
728pub fn match_css_value_grammar_components_v0(
730 grammar: &str,
731 components: &[CssValueComponentV0],
732 registry: &SpecGrammarRegistryV0,
733 budget: CssValueGrammarBudgetV0,
734) -> CssValueGrammarVerdictV0 {
735 let normalized = strip_matching_quotes(grammar.trim());
736 let expression = match VdsParser::new(normalized).parse() {
737 Ok(expression) => expression,
738 Err(error) => {
739 return grammar_defect(grammar, error.offset, error.code, error.detail);
740 }
741 };
742 match_css_value_grammar_components_with_expression_v0(
743 grammar,
744 components,
745 registry,
746 budget,
747 &expression,
748 false,
749 )
750}
751
752fn match_css_value_grammar_components_with_expression_v0(
753 grammar: &str,
754 components: &[CssValueComponentV0],
755 registry: &SpecGrammarRegistryV0,
756 budget: CssValueGrammarBudgetV0,
757 expression: &VdsExpression,
758 cache_registered_grammars: bool,
759) -> CssValueGrammarVerdictV0 {
760 let locus = component_locus(components);
761 let mut context = MatchContext {
762 registry,
763 budget,
764 match_steps: 0,
765 first_stop: None,
766 grammar_cache: HashMap::new(),
767 cache_registered_grammars,
768 allow_unvalidated_standard_function_references: false,
769 };
770 let ends = context.match_expression(expression, components, 0, 0);
771 if ends.contains(&components.len()) {
772 return CssValueGrammarVerdictV0::Matched {
773 grammar: grammar.to_string(),
774 consumed_components: components.len(),
775 };
776 }
777 if let Some(stop) = context.first_stop {
778 return match stop {
779 MatchStop::Budget {
780 kind,
781 limit,
782 reference,
783 } => CssValueGrammarVerdictV0::NotMatchedWithinBudget {
784 grammar: grammar.to_string(),
785 locus,
786 budget: kind,
787 limit,
788 reference,
789 },
790 MatchStop::GrammarDefect {
791 offset,
792 code,
793 detail,
794 } => grammar_defect(grammar, offset, code, detail),
795 };
796 }
797 CssValueGrammarVerdictV0::Unmatched {
798 grammar: grammar.to_string(),
799 locus,
800 }
801}
802
803#[derive(Debug, Clone, PartialEq, Eq)]
804enum VdsExpression {
805 Literal(String),
806 Reference(VdsReference),
807 Function {
808 name: String,
809 arguments: Box<VdsExpression>,
810 },
811 Sequence(Vec<VdsExpression>),
812 AllInAnyOrder(Vec<VdsExpression>),
813 OneOrMoreInAnyOrder(Vec<VdsExpression>),
814 Choice(Vec<VdsExpression>),
815 Repeat {
816 expression: Box<VdsExpression>,
817 min: usize,
818 max: Option<usize>,
819 comma_separated: bool,
820 },
821 Required(Box<VdsExpression>),
822}
823
824#[derive(Debug, Clone, PartialEq, Eq)]
825struct VdsReference {
826 category: ReferenceCategory,
827 name: String,
828 range: Option<NumericRange>,
829}
830
831#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
832enum ReferenceCategory {
833 Type,
834 Property,
835 Function,
836}
837
838#[derive(Debug, Clone, PartialEq, Eq)]
839struct NumericRange {
840 min: Option<String>,
841 max: Option<String>,
842}
843
844#[derive(Debug, Clone, PartialEq, Eq)]
845struct VdsParseError {
846 offset: usize,
847 code: &'static str,
848 detail: String,
849}
850
851type CachedVdsExpression = Result<Arc<VdsExpression>, VdsParseError>;
852
853fn cached_pinned_vds_expression(source: &str) -> CachedVdsExpression {
854 static CACHE: OnceLock<RwLock<HashMap<String, CachedVdsExpression>>> = OnceLock::new();
855 let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
856 if let Some(parsed) = cache
857 .read()
858 .unwrap_or_else(std::sync::PoisonError::into_inner)
859 .get(source)
860 {
861 return parsed.clone();
862 }
863
864 let parsed = VdsParser::new(source).parse().map(Arc::new);
865 cache
866 .write()
867 .unwrap_or_else(std::sync::PoisonError::into_inner)
868 .entry(source.to_string())
869 .or_insert(parsed)
870 .clone()
871}
872
873#[derive(Debug, Clone, PartialEq, Eq)]
874struct VdsToken {
875 kind: VdsTokenKind,
876 offset: usize,
877}
878
879#[derive(Debug, Clone, PartialEq, Eq)]
880enum VdsTokenKind {
881 Word(String),
882 Reference(String),
883 Literal(String),
884 OpenBracket,
885 CloseBracket,
886 OpenParen,
887 CloseParen,
888 Or,
889 OrOr,
890 AndAnd,
891 Question,
892 Star,
893 Plus,
894 Hash,
895 Range(usize, Option<usize>),
896 Bang,
897 End,
898}
899
900struct VdsParser<'a> {
901 source: &'a str,
902 tokens: Vec<VdsToken>,
903 cursor: usize,
904}
905
906impl<'a> VdsParser<'a> {
907 fn new(source: &'a str) -> Self {
908 Self {
909 source,
910 tokens: Vec::new(),
911 cursor: 0,
912 }
913 }
914
915 fn parse(mut self) -> Result<VdsExpression, VdsParseError> {
916 self.tokens = lex_vds(self.source)?;
917 let expression = self.parse_choice()?;
918 if !matches!(self.peek(), VdsTokenKind::End) {
919 return Err(self.error(
920 "unexpectedGrammarToken",
921 "unexpected trailing grammar token",
922 ));
923 }
924 Ok(expression)
925 }
926
927 fn parse_choice(&mut self) -> Result<VdsExpression, VdsParseError> {
928 let mut values = vec![self.parse_one_or_more_in_any_order()?];
929 while matches!(self.peek(), VdsTokenKind::Or) {
930 self.cursor += 1;
931 values.push(self.parse_one_or_more_in_any_order()?);
932 }
933 Ok(flatten_expression(values, VdsExpression::Choice))
934 }
935
936 fn parse_one_or_more_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
937 let mut values = vec![self.parse_all_in_any_order()?];
938 while matches!(self.peek(), VdsTokenKind::OrOr) {
939 self.cursor += 1;
940 values.push(self.parse_all_in_any_order()?);
941 }
942 Ok(flatten_expression(
943 values,
944 VdsExpression::OneOrMoreInAnyOrder,
945 ))
946 }
947
948 fn parse_all_in_any_order(&mut self) -> Result<VdsExpression, VdsParseError> {
949 let mut values = vec![self.parse_sequence()?];
950 while matches!(self.peek(), VdsTokenKind::AndAnd) {
951 self.cursor += 1;
952 values.push(self.parse_sequence()?);
953 }
954 Ok(flatten_expression(values, VdsExpression::AllInAnyOrder))
955 }
956
957 fn parse_sequence(&mut self) -> Result<VdsExpression, VdsParseError> {
958 let mut values = Vec::new();
959 while self.starts_primary() {
960 values.push(self.parse_postfix()?);
961 }
962 if values.is_empty() {
963 return Err(self.error("missingGrammarTerm", "expected a grammar term"));
964 }
965 Ok(flatten_expression(values, VdsExpression::Sequence))
966 }
967
968 fn parse_postfix(&mut self) -> Result<VdsExpression, VdsParseError> {
969 let mut expression = self.parse_primary()?;
970 loop {
971 expression = match self.peek() {
972 VdsTokenKind::Question => {
973 self.cursor += 1;
974 repeat(expression, 0, Some(1), false)
975 }
976 VdsTokenKind::Star => {
977 self.cursor += 1;
978 repeat(expression, 0, None, false)
979 }
980 VdsTokenKind::Plus => {
981 self.cursor += 1;
982 repeat(expression, 1, None, false)
983 }
984 VdsTokenKind::Hash => {
985 self.cursor += 1;
986 let (min, max) = match self.peek().clone() {
987 VdsTokenKind::Range(min, max) => {
988 self.cursor += 1;
989 (min, max)
990 }
991 _ => (1, None),
992 };
993 repeat(expression, min, max, true)
994 }
995 VdsTokenKind::Range(min, max) => {
996 let min = *min;
997 let max = *max;
998 self.cursor += 1;
999 repeat(expression, min, max, false)
1000 }
1001 VdsTokenKind::Bang => {
1002 self.cursor += 1;
1003 VdsExpression::Required(Box::new(expression))
1004 }
1005 _ => break,
1006 };
1007 }
1008 Ok(expression)
1009 }
1010
1011 fn parse_primary(&mut self) -> Result<VdsExpression, VdsParseError> {
1012 let token = self.tokens[self.cursor].clone();
1013 self.cursor += 1;
1014 match token.kind {
1015 VdsTokenKind::Reference(source) => Ok(VdsExpression::Reference(parse_reference(
1016 source.as_str(),
1017 token.offset,
1018 )?)),
1019 VdsTokenKind::Word(word) => {
1020 if matches!(self.peek(), VdsTokenKind::OpenParen) {
1021 self.cursor += 1;
1022 if matches!(self.peek(), VdsTokenKind::CloseParen) {
1023 self.cursor += 1;
1024 return Ok(VdsExpression::Function {
1025 name: word,
1026 arguments: Box::new(VdsExpression::Sequence(Vec::new())),
1027 });
1028 }
1029 let arguments = self.parse_choice()?;
1030 self.expect_close_paren()?;
1031 Ok(VdsExpression::Function {
1032 name: word,
1033 arguments: Box::new(arguments),
1034 })
1035 } else {
1036 Ok(VdsExpression::Literal(word))
1037 }
1038 }
1039 VdsTokenKind::Literal(literal) => Ok(VdsExpression::Literal(literal)),
1040 VdsTokenKind::OpenBracket => {
1041 let expression = self.parse_choice()?;
1042 if !matches!(self.peek(), VdsTokenKind::CloseBracket) {
1043 return Err(self.error("unclosedGrammarGroup", "missing closing ]"));
1044 }
1045 self.cursor += 1;
1046 Ok(expression)
1047 }
1048 VdsTokenKind::OpenParen => {
1049 let expression = self.parse_choice()?;
1050 self.expect_close_paren()?;
1051 Ok(expression)
1052 }
1053 _ => Err(VdsParseError {
1054 offset: token.offset,
1055 code: "unexpectedGrammarPrimary",
1056 detail: "expected a literal, reference, function, or group".to_string(),
1057 }),
1058 }
1059 }
1060
1061 fn expect_close_paren(&mut self) -> Result<(), VdsParseError> {
1062 if !matches!(self.peek(), VdsTokenKind::CloseParen) {
1063 return Err(self.error("unclosedGrammarFunction", "missing closing )"));
1064 }
1065 self.cursor += 1;
1066 Ok(())
1067 }
1068
1069 fn starts_primary(&self) -> bool {
1070 matches!(
1071 self.peek(),
1072 VdsTokenKind::Reference(_)
1073 | VdsTokenKind::Word(_)
1074 | VdsTokenKind::Literal(_)
1075 | VdsTokenKind::OpenBracket
1076 | VdsTokenKind::OpenParen
1077 )
1078 }
1079
1080 fn peek(&self) -> &VdsTokenKind {
1081 &self.tokens[self.cursor].kind
1082 }
1083
1084 fn error(&self, code: &'static str, detail: &str) -> VdsParseError {
1085 VdsParseError {
1086 offset: self.tokens[self.cursor].offset,
1087 code,
1088 detail: detail.to_string(),
1089 }
1090 }
1091}
1092
1093fn flatten_expression(
1094 mut values: Vec<VdsExpression>,
1095 wrap: impl FnOnce(Vec<VdsExpression>) -> VdsExpression,
1096) -> VdsExpression {
1097 if values.len() == 1 {
1098 values.pop().unwrap_or(VdsExpression::Sequence(Vec::new()))
1099 } else {
1100 wrap(values)
1101 }
1102}
1103
1104fn repeat(
1105 expression: VdsExpression,
1106 min: usize,
1107 max: Option<usize>,
1108 comma_separated: bool,
1109) -> VdsExpression {
1110 VdsExpression::Repeat {
1111 expression: Box::new(expression),
1112 min,
1113 max,
1114 comma_separated,
1115 }
1116}
1117
1118fn lex_vds(source: &str) -> Result<Vec<VdsToken>, VdsParseError> {
1119 let mut tokens = Vec::new();
1120 let mut cursor = 0usize;
1121 while cursor < source.len() {
1122 let Some(character) = source[cursor..].chars().next() else {
1123 break;
1124 };
1125 if character.is_whitespace() {
1126 cursor += character.len_utf8();
1127 continue;
1128 }
1129 let offset = cursor;
1130 let rest = &source[cursor..];
1131 if rest.starts_with("||") {
1132 tokens.push(token(VdsTokenKind::OrOr, offset));
1133 cursor += 2;
1134 continue;
1135 }
1136 if rest.starts_with("&&") {
1137 tokens.push(token(VdsTokenKind::AndAnd, offset));
1138 cursor += 2;
1139 continue;
1140 }
1141 if character == '<' {
1142 let Some(relative_end) = rest.find('>') else {
1143 return Err(VdsParseError {
1144 offset,
1145 code: "unclosedGrammarReference",
1146 detail: "missing closing >".to_string(),
1147 });
1148 };
1149 let end = cursor + relative_end;
1150 tokens.push(token(
1151 VdsTokenKind::Reference(source[cursor + 1..end].trim().to_string()),
1152 offset,
1153 ));
1154 cursor = end + 1;
1155 continue;
1156 }
1157 if character == '{' {
1158 let Some(relative_end) =
1159 rest.char_indices()
1160 .find_map(|(relative_offset, candidate)| {
1161 (candidate == '}').then_some(relative_offset)
1162 })
1163 else {
1164 return Err(VdsParseError {
1165 offset,
1166 code: "unclosedGrammarRange",
1167 detail: "missing closing }".to_string(),
1168 });
1169 };
1170 let end = cursor + relative_end;
1171 let range = parse_repeat_range(&source[cursor + 1..end], offset)?;
1172 tokens.push(token(VdsTokenKind::Range(range.0, range.1), offset));
1173 cursor = end + 1;
1174 continue;
1175 }
1176 let simple = match character {
1177 '[' => Some(VdsTokenKind::OpenBracket),
1178 ']' => Some(VdsTokenKind::CloseBracket),
1179 '(' => Some(VdsTokenKind::OpenParen),
1180 ')' => Some(VdsTokenKind::CloseParen),
1181 '|' => Some(VdsTokenKind::Or),
1182 '?' => Some(VdsTokenKind::Question),
1183 '*' => Some(VdsTokenKind::Star),
1184 '+' => Some(VdsTokenKind::Plus),
1185 '#' => Some(VdsTokenKind::Hash),
1186 '!' => Some(VdsTokenKind::Bang),
1187 ',' | '/' | ':' | ';' | '=' | '@' | '~' | '^' | '$' | '&' => {
1188 Some(VdsTokenKind::Literal(character.to_string()))
1189 }
1190 _ => None,
1191 };
1192 if let Some(kind) = simple {
1193 tokens.push(token(kind, offset));
1194 cursor += character.len_utf8();
1195 continue;
1196 }
1197 if character == '\'' || character == '"' {
1198 let quote = character;
1199 cursor += character.len_utf8();
1200 let content_start = cursor;
1201 let mut escaped = false;
1202 let mut found_end = None;
1203 while cursor < source.len() {
1204 let Some(current) = source[cursor..].chars().next() else {
1205 break;
1206 };
1207 if escaped {
1208 escaped = false;
1209 } else if current == '\\' {
1210 escaped = true;
1211 } else if current == quote {
1212 found_end = Some(cursor);
1213 break;
1214 }
1215 cursor += current.len_utf8();
1216 }
1217 let Some(end) = found_end else {
1218 return Err(VdsParseError {
1219 offset,
1220 code: "unclosedGrammarString",
1221 detail: "missing closing quote".to_string(),
1222 });
1223 };
1224 tokens.push(token(
1225 VdsTokenKind::Literal(source[content_start..end].to_string()),
1226 offset,
1227 ));
1228 cursor = end + quote.len_utf8();
1229 continue;
1230 }
1231 let start = cursor;
1232 while cursor < source.len() {
1233 let Some(current) = source[cursor..].chars().next() else {
1234 break;
1235 };
1236 if current.is_whitespace()
1237 || matches!(
1238 current,
1239 '<' | '>'
1240 | '['
1241 | ']'
1242 | '('
1243 | ')'
1244 | '{'
1245 | '}'
1246 | '|'
1247 | '&'
1248 | '?'
1249 | '*'
1250 | '+'
1251 | '#'
1252 | '!'
1253 | ','
1254 | '/'
1255 | ':'
1256 | ';'
1257 | '='
1258 | '@'
1259 | '~'
1260 | '^'
1261 | '$'
1262 | '\''
1263 | '"'
1264 )
1265 {
1266 break;
1267 }
1268 cursor += current.len_utf8();
1269 }
1270 if start == cursor {
1271 return Err(VdsParseError {
1272 offset,
1273 code: "unsupportedGrammarCharacter",
1274 detail: format!("unsupported grammar character {character:?}"),
1275 });
1276 }
1277 tokens.push(token(
1278 VdsTokenKind::Word(source[start..cursor].to_string()),
1279 offset,
1280 ));
1281 }
1282 tokens.push(token(VdsTokenKind::End, source.len()));
1283 Ok(tokens)
1284}
1285
1286fn token(kind: VdsTokenKind, offset: usize) -> VdsToken {
1287 VdsToken { kind, offset }
1288}
1289
1290fn parse_repeat_range(
1291 source: &str,
1292 offset: usize,
1293) -> Result<(usize, Option<usize>), VdsParseError> {
1294 let mut parts = source.split(',').map(str::trim);
1295 let first = parts.next().unwrap_or_default();
1296 let second = parts.next();
1297 if parts.next().is_some() || first.is_empty() {
1298 return Err(VdsParseError {
1299 offset,
1300 code: "invalidGrammarRange",
1301 detail: format!("invalid repeat range {{{source}}}"),
1302 });
1303 }
1304 let min = first.parse::<usize>().map_err(|_| VdsParseError {
1305 offset,
1306 code: "invalidGrammarRange",
1307 detail: format!("invalid repeat range minimum {first:?}"),
1308 })?;
1309 let max = match second {
1310 None => Some(min),
1311 Some("") => None,
1312 Some(value) => Some(value.parse::<usize>().map_err(|_| VdsParseError {
1313 offset,
1314 code: "invalidGrammarRange",
1315 detail: format!("invalid repeat range maximum {value:?}"),
1316 })?),
1317 };
1318 if max.is_some_and(|max| max < min) {
1319 return Err(VdsParseError {
1320 offset,
1321 code: "invalidGrammarRange",
1322 detail: format!("repeat range maximum precedes minimum in {{{source}}}"),
1323 });
1324 }
1325 Ok((min, max))
1326}
1327
1328fn parse_reference(source: &str, offset: usize) -> Result<VdsReference, VdsParseError> {
1329 let source = source.trim();
1330 if source.is_empty() {
1331 return Err(VdsParseError {
1332 offset,
1333 code: "emptyGrammarReference",
1334 detail: "empty grammar reference".to_string(),
1335 });
1336 }
1337 if let Some(property) = source
1338 .strip_prefix('\'')
1339 .and_then(|value| value.strip_suffix('\''))
1340 {
1341 return Ok(VdsReference {
1342 category: ReferenceCategory::Property,
1343 name: property.to_ascii_lowercase(),
1344 range: None,
1345 });
1346 }
1347 let (name, range) = split_reference_range(source, offset)?;
1348 let category = if name.ends_with("()") {
1349 ReferenceCategory::Function
1350 } else {
1351 ReferenceCategory::Type
1352 };
1353 Ok(VdsReference {
1354 category,
1355 name: name.to_ascii_lowercase(),
1356 range,
1357 })
1358}
1359
1360fn split_reference_range(
1361 source: &str,
1362 offset: usize,
1363) -> Result<(&str, Option<NumericRange>), VdsParseError> {
1364 let Some(open) = source.find('[') else {
1365 return Ok((source.trim(), None));
1366 };
1367 let Some(close) = source.rfind(']') else {
1368 return Err(VdsParseError {
1369 offset,
1370 code: "unclosedReferenceRange",
1371 detail: format!("missing ] in reference <{source}>"),
1372 });
1373 };
1374 if close + 1 != source.len() {
1375 return Err(VdsParseError {
1376 offset,
1377 code: "trailingReferenceRangeContent",
1378 detail: format!("unexpected content after range in <{source}>"),
1379 });
1380 }
1381 let name = source[..open].trim();
1382 let mut bounds = source[open + 1..close].split(',').map(str::trim);
1383 let min = bounds.next().unwrap_or_default();
1384 let max = bounds.next();
1385 if name.is_empty() || min.is_empty() || max.is_none() || bounds.next().is_some() {
1386 return Err(VdsParseError {
1387 offset,
1388 code: "invalidReferenceRange",
1389 detail: format!("invalid numeric range in <{source}>"),
1390 });
1391 }
1392 let max = max.unwrap_or_default();
1393 Ok((
1394 name,
1395 Some(NumericRange {
1396 min: finite_range_bound(min),
1397 max: finite_range_bound(max),
1398 }),
1399 ))
1400}
1401
1402fn finite_range_bound(source: &str) -> Option<String> {
1403 (!matches!(source, "∞" | "+∞" | "-∞")).then(|| source.to_string())
1404}
1405
1406#[derive(Debug, Clone, PartialEq, Eq)]
1407enum MatchStop {
1408 Budget {
1409 kind: CssValueGrammarBudgetKindV0,
1410 limit: usize,
1411 reference: Option<String>,
1412 },
1413 GrammarDefect {
1414 offset: usize,
1415 code: &'static str,
1416 detail: String,
1417 },
1418}
1419
1420struct MatchContext<'a> {
1421 registry: &'a SpecGrammarRegistryV0,
1422 budget: CssValueGrammarBudgetV0,
1423 match_steps: usize,
1424 first_stop: Option<MatchStop>,
1425 grammar_cache: HashMap<(ReferenceCategory, String), CachedVdsExpression>,
1426 cache_registered_grammars: bool,
1427 allow_unvalidated_standard_function_references: bool,
1428}
1429
1430#[derive(Debug, Clone, Copy)]
1431struct RepeatMatchPlan<'a> {
1432 expression: &'a VdsExpression,
1433 min: usize,
1434 max: Option<usize>,
1435 comma_separated: bool,
1436}
1437
1438impl MatchContext<'_> {
1439 fn match_expression(
1440 &mut self,
1441 expression: &VdsExpression,
1442 components: &[CssValueComponentV0],
1443 position: usize,
1444 reference_depth: usize,
1445 ) -> BTreeSet<usize> {
1446 if !self.consume_step(None) {
1447 return BTreeSet::new();
1448 }
1449 let positions = match expression {
1450 VdsExpression::Literal(literal) => match_literal(literal, components, position),
1451 VdsExpression::Reference(reference) => {
1452 self.match_reference(reference, components, position, reference_depth)
1453 }
1454 VdsExpression::Function { name, arguments } => {
1455 self.match_function(name, arguments, components, position, reference_depth)
1456 }
1457 VdsExpression::Sequence(expressions) => {
1458 self.match_sequence(expressions, components, position, reference_depth)
1459 }
1460 VdsExpression::AllInAnyOrder(expressions) => {
1461 self.match_any_order(expressions, components, position, reference_depth, true)
1462 }
1463 VdsExpression::OneOrMoreInAnyOrder(expressions) => {
1464 self.match_any_order(expressions, components, position, reference_depth, false)
1465 }
1466 VdsExpression::Choice(expressions) => expressions
1467 .iter()
1468 .flat_map(|expression| {
1469 self.match_expression(expression, components, position, reference_depth)
1470 })
1471 .collect(),
1472 VdsExpression::Repeat {
1473 expression,
1474 min,
1475 max,
1476 comma_separated,
1477 } => self.match_repeat(
1478 RepeatMatchPlan {
1479 expression,
1480 min: *min,
1481 max: *max,
1482 comma_separated: *comma_separated,
1483 },
1484 components,
1485 position,
1486 reference_depth,
1487 ),
1488 VdsExpression::Required(expression) => self
1489 .match_expression(expression, components, position, reference_depth)
1490 .into_iter()
1491 .filter(|end| *end > position)
1492 .collect(),
1493 };
1494 self.cap_states(positions, None)
1495 }
1496
1497 fn match_sequence(
1498 &mut self,
1499 expressions: &[VdsExpression],
1500 components: &[CssValueComponentV0],
1501 position: usize,
1502 reference_depth: usize,
1503 ) -> BTreeSet<usize> {
1504 let mut states = BTreeSet::from([(position, false)]);
1505 for expression in expressions {
1506 let mut next = BTreeSet::new();
1507 for (position, previous_was_omitted) in states {
1508 if previous_was_omitted && is_comma_literal(expression) {
1509 next.insert((position, false));
1510 continue;
1511 }
1512 for end in self.match_expression(expression, components, position, reference_depth)
1513 {
1514 next.insert((end, end == position));
1515 }
1516 }
1517 if next.len() > self.budget.max_states {
1518 self.record_stop(MatchStop::Budget {
1519 kind: CssValueGrammarBudgetKindV0::CandidateStates,
1520 limit: self.budget.max_states,
1521 reference: None,
1522 });
1523 next.clear();
1524 }
1525 states = next;
1526 if states.is_empty() {
1527 break;
1528 }
1529 }
1530 states.into_iter().map(|(position, _)| position).collect()
1531 }
1532
1533 fn match_repeat(
1534 &mut self,
1535 plan: RepeatMatchPlan<'_>,
1536 components: &[CssValueComponentV0],
1537 position: usize,
1538 reference_depth: usize,
1539 ) -> BTreeSet<usize> {
1540 let effective_max = plan
1541 .max
1542 .unwrap_or_else(|| components.len().saturating_add(1));
1543 let mut accepted = BTreeSet::new();
1544 let mut frontier = BTreeSet::from([position]);
1545 if plan.min == 0 {
1546 accepted.insert(position);
1547 }
1548 for count in 1..=effective_max {
1549 let mut next = BTreeSet::new();
1550 for current in &frontier {
1551 let item_start = if plan.comma_separated && count > 1 {
1552 if components.get(*current).is_some_and(|component| {
1553 matches!(component.kind, CssValueComponentKindV0::Comma)
1554 }) {
1555 *current + 1
1556 } else {
1557 continue;
1558 }
1559 } else {
1560 *current
1561 };
1562 for end in
1563 self.match_expression(plan.expression, components, item_start, reference_depth)
1564 {
1565 if end > item_start {
1566 next.insert(end);
1567 }
1568 }
1569 }
1570 frontier = self.cap_states(next, None);
1571 if frontier.is_empty() {
1572 break;
1573 }
1574 if count >= plan.min {
1575 accepted.extend(frontier.iter().copied());
1576 }
1577 }
1578 accepted
1579 }
1580
1581 fn match_any_order(
1582 &mut self,
1583 expressions: &[VdsExpression],
1584 components: &[CssValueComponentV0],
1585 position: usize,
1586 reference_depth: usize,
1587 require_all: bool,
1588 ) -> BTreeSet<usize> {
1589 if expressions.len() > 63 {
1590 self.record_stop(MatchStop::Budget {
1591 kind: CssValueGrammarBudgetKindV0::CandidateStates,
1592 limit: self.budget.max_states,
1593 reference: None,
1594 });
1595 return BTreeSet::new();
1596 }
1597 let required_mask = (1u64 << expressions.len()) - 1;
1598 let mut accepted = BTreeSet::new();
1599 let mut stack = vec![(position, 0u64)];
1600 let mut visited = BTreeSet::new();
1601 while let Some((current, mask)) = stack.pop() {
1602 if !visited.insert((current, mask)) {
1603 continue;
1604 }
1605 if visited.len() > self.budget.max_states {
1606 self.record_stop(MatchStop::Budget {
1607 kind: CssValueGrammarBudgetKindV0::CandidateStates,
1608 limit: self.budget.max_states,
1609 reference: None,
1610 });
1611 break;
1612 }
1613 if (require_all && mask == required_mask) || (!require_all && mask != 0) {
1614 accepted.insert(current);
1615 }
1616 for (index, expression) in expressions.iter().enumerate() {
1617 let bit = 1u64 << index;
1618 if mask & bit != 0 {
1619 continue;
1620 }
1621 for end in self.match_expression(expression, components, current, reference_depth) {
1622 if end > current || (require_all && end == current) {
1623 stack.push((end, mask | bit));
1624 }
1625 }
1626 }
1627 }
1628 accepted
1629 }
1630
1631 fn match_function(
1632 &mut self,
1633 name: &str,
1634 arguments: &VdsExpression,
1635 components: &[CssValueComponentV0],
1636 position: usize,
1637 reference_depth: usize,
1638 ) -> BTreeSet<usize> {
1639 let Some(component) = components.get(position) else {
1640 return BTreeSet::new();
1641 };
1642 let CssValueComponentKindV0::Function {
1643 name: actual,
1644 arguments: actual_arguments,
1645 } = &component.kind
1646 else {
1647 return BTreeSet::new();
1648 };
1649 if !actual.eq_ignore_ascii_case(name) {
1650 return BTreeSet::new();
1651 }
1652 self.match_expression(arguments, actual_arguments, 0, reference_depth)
1653 .contains(&actual_arguments.len())
1654 .then_some(position + 1)
1655 .into_iter()
1656 .collect()
1657 }
1658
1659 fn match_reference(
1660 &mut self,
1661 reference: &VdsReference,
1662 components: &[CssValueComponentV0],
1663 position: usize,
1664 reference_depth: usize,
1665 ) -> BTreeSet<usize> {
1666 if self.allow_unvalidated_standard_function_references
1667 && reference.category != ReferenceCategory::Function
1668 && components
1669 .get(position)
1670 .is_some_and(component_is_recognized_standard_function)
1671 {
1672 return BTreeSet::from([position + 1]);
1673 }
1674 if let Some(positions) = match_builtin_reference(reference, components, position) {
1675 return positions;
1676 }
1677 if reference_depth >= self.budget.max_reference_depth {
1678 self.record_stop(MatchStop::Budget {
1679 kind: CssValueGrammarBudgetKindV0::ReferenceDepth,
1680 limit: self.budget.max_reference_depth,
1681 reference: Some(reference.name.clone()),
1682 });
1683 return BTreeSet::new();
1684 }
1685 let category = match reference.category {
1686 ReferenceCategory::Type => "types",
1687 ReferenceCategory::Property => "properties",
1688 ReferenceCategory::Function => "functions",
1689 };
1690 let Some(entry) = self.registry.entry(category, reference.name.as_str()) else {
1691 self.record_stop(MatchStop::GrammarDefect {
1692 offset: 0,
1693 code: "unknownGrammarReference",
1694 detail: format!("unknown {category} reference <{}>", reference.name),
1695 });
1696 return BTreeSet::new();
1697 };
1698 let Some(source) = entry.syntax.as_deref() else {
1699 self.record_stop(MatchStop::GrammarDefect {
1700 offset: 0,
1701 code: "missingReferencedGrammar",
1702 detail: format!("{category} reference <{}> has no syntax", reference.name),
1703 });
1704 return BTreeSet::new();
1705 };
1706 let key = (reference.category, reference.name.clone());
1707 let expression = match self
1708 .grammar_cache
1709 .entry(key)
1710 .or_insert_with(|| {
1711 if self.cache_registered_grammars {
1712 cached_pinned_vds_expression(source)
1713 } else {
1714 VdsParser::new(source).parse().map(Arc::new)
1715 }
1716 })
1717 .clone()
1718 {
1719 Ok(expression) => expression,
1720 Err(error) => {
1721 self.record_stop(MatchStop::GrammarDefect {
1722 offset: error.offset,
1723 code: error.code,
1724 detail: format!("referenced grammar <{}>: {}", reference.name, error.detail),
1725 });
1726 return BTreeSet::new();
1727 }
1728 };
1729 if reference.category == ReferenceCategory::Function {
1730 return self.match_function_reference(
1731 reference,
1732 expression.as_ref(),
1733 components,
1734 position,
1735 reference_depth + 1,
1736 );
1737 }
1738 self.match_expression(
1739 expression.as_ref(),
1740 components,
1741 position,
1742 reference_depth + 1,
1743 )
1744 }
1745
1746 fn match_function_reference(
1747 &mut self,
1748 reference: &VdsReference,
1749 expression: &VdsExpression,
1750 components: &[CssValueComponentV0],
1751 position: usize,
1752 reference_depth: usize,
1753 ) -> BTreeSet<usize> {
1754 let name = reference.name.trim_end_matches("()");
1755 let whole_component =
1756 self.match_expression(expression, components, position, reference_depth);
1757 if !whole_component.is_empty() {
1758 return whole_component;
1759 }
1760 self.match_function(name, expression, components, position, reference_depth)
1761 }
1762
1763 fn consume_step(&mut self, reference: Option<String>) -> bool {
1764 self.match_steps += 1;
1765 if self.match_steps <= self.budget.max_match_steps {
1766 return true;
1767 }
1768 self.record_stop(MatchStop::Budget {
1769 kind: CssValueGrammarBudgetKindV0::MatchSteps,
1770 limit: self.budget.max_match_steps,
1771 reference,
1772 });
1773 false
1774 }
1775
1776 fn cap_states(
1777 &mut self,
1778 mut states: BTreeSet<usize>,
1779 reference: Option<String>,
1780 ) -> BTreeSet<usize> {
1781 if states.len() <= self.budget.max_states {
1782 return states;
1783 }
1784 self.record_stop(MatchStop::Budget {
1785 kind: CssValueGrammarBudgetKindV0::CandidateStates,
1786 limit: self.budget.max_states,
1787 reference,
1788 });
1789 states.clear();
1790 states
1791 }
1792
1793 fn record_stop(&mut self, stop: MatchStop) {
1794 if self.first_stop.is_none() {
1795 self.first_stop = Some(stop);
1796 }
1797 }
1798}
1799
1800fn match_literal(
1801 literal: &str,
1802 components: &[CssValueComponentV0],
1803 position: usize,
1804) -> BTreeSet<usize> {
1805 components
1806 .get(position)
1807 .filter(|component| component.text.eq_ignore_ascii_case(literal))
1808 .map(|_| BTreeSet::from([position + 1]))
1809 .unwrap_or_default()
1810}
1811
1812fn is_comma_literal(expression: &VdsExpression) -> bool {
1813 matches!(expression, VdsExpression::Literal(literal) if literal == ",")
1814}
1815
1816fn is_unitless_zero(component: &CssValueComponentV0) -> bool {
1817 matches!(component.kind, CssValueComponentKindV0::Number)
1818 && parse_numeric_value_with_unit(component.text.as_str())
1819 .is_some_and(|numeric| numeric.value == 0.0 && numeric.unit.is_empty())
1820}
1821
1822fn match_builtin_reference(
1823 reference: &VdsReference,
1824 components: &[CssValueComponentV0],
1825 position: usize,
1826) -> Option<BTreeSet<usize>> {
1827 if reference.category != ReferenceCategory::Type {
1828 return None;
1829 }
1830 if matches!(
1831 reference.name.as_str(),
1832 "declaration-value" | "any-value" | "whole-value"
1833 ) {
1834 return Some(((position + 1)..=components.len()).collect());
1835 }
1836 if !is_builtin_reference_name(reference.name.as_str()) {
1837 return None;
1838 }
1839 let Some(component) = components.get(position) else {
1840 return Some(BTreeSet::new());
1841 };
1842 let kind = classify_registered_property_declared_value_v0(component.text.as_str());
1843 let accepted = match reference.name.as_str() {
1844 "number" | "number-token" => {
1845 matches!(
1846 kind,
1847 DeclaredValueKindV0::Number | DeclaredValueKindV0::Integer
1848 )
1849 }
1850 "integer" => matches!(kind, DeclaredValueKindV0::Integer),
1851 "length" => {
1852 matches!(
1853 kind,
1854 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Length)
1855 ) || is_unitless_zero(component)
1856 }
1857 "percentage" | "percentage-token" => matches!(
1858 kind,
1859 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
1860 ),
1861 "length-percentage" => {
1862 matches!(
1863 kind,
1864 DeclaredValueKindV0::Dimension(
1865 DeclaredNumericTypeV0::Length | DeclaredNumericTypeV0::Percentage
1866 )
1867 ) || is_unitless_zero(component)
1868 }
1869 "angle" => matches!(
1870 kind,
1871 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Angle)
1872 ),
1873 "time" => matches!(
1874 kind,
1875 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Time)
1876 ),
1877 "resolution" => matches!(
1878 kind,
1879 DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Resolution)
1880 ),
1881 "hex-color" => matches!(kind, DeclaredValueKindV0::HexColor),
1882 "named-color" => matches!(kind, DeclaredValueKindV0::ColorKeyword(_)),
1883 "custom-ident" => {
1884 matches!(component.kind, CssValueComponentKindV0::Ident)
1885 && !matches!(kind, DeclaredValueKindV0::CssWide)
1886 }
1887 "ident" | "ident-token" => matches!(component.kind, CssValueComponentKindV0::Ident),
1888 "dashed-ident" | "custom-property-name" => {
1889 matches!(component.kind, CssValueComponentKindV0::Ident)
1890 && component.text.starts_with("--")
1891 }
1892 "string" | "string-token" => matches!(kind, DeclaredValueKindV0::QuotedString),
1893 "url" | "url-token" => matches!(kind, DeclaredValueKindV0::Url),
1894 "image" => matches!(
1895 kind,
1896 DeclaredValueKindV0::ImageFunction | DeclaredValueKindV0::Url
1897 ),
1898 "transform-function" => matches!(kind, DeclaredValueKindV0::TransformFunction),
1899 "alpha-value" => matches!(
1900 kind,
1901 DeclaredValueKindV0::Number
1902 | DeclaredValueKindV0::Integer
1903 | DeclaredValueKindV0::Dimension(DeclaredNumericTypeV0::Percentage)
1904 ),
1905 "zero" => parse_numeric_value_with_unit(component.text.as_str())
1906 .is_some_and(|numeric| numeric.value == 0.0),
1907 "dimension-token" => matches!(component.kind, CssValueComponentKindV0::Dimension),
1908 "hash-token" => matches!(component.kind, CssValueComponentKindV0::Hash),
1909 "function-token" => matches!(component.kind, CssValueComponentKindV0::Function { .. }),
1910 "comma-token" => matches!(component.kind, CssValueComponentKindV0::Comma),
1911 _ => false,
1912 };
1913 let accepted =
1914 accepted && numeric_range_accepts(reference.range.as_ref(), component.text.as_str());
1915 Some(accepted.then_some(position + 1).into_iter().collect())
1916}
1917
1918fn is_builtin_reference_name(name: &str) -> bool {
1919 matches!(
1920 name,
1921 "number"
1922 | "number-token"
1923 | "integer"
1924 | "length"
1925 | "percentage"
1926 | "percentage-token"
1927 | "length-percentage"
1928 | "angle"
1929 | "time"
1930 | "resolution"
1931 | "hex-color"
1932 | "named-color"
1933 | "custom-ident"
1934 | "ident"
1935 | "ident-token"
1936 | "dashed-ident"
1937 | "custom-property-name"
1938 | "string"
1939 | "string-token"
1940 | "url"
1941 | "url-token"
1942 | "image"
1943 | "transform-function"
1944 | "alpha-value"
1945 | "zero"
1946 | "dimension-token"
1947 | "hash-token"
1948 | "function-token"
1949 | "comma-token"
1950 )
1951}
1952
1953fn numeric_range_accepts(range: Option<&NumericRange>, source: &str) -> bool {
1954 let Some(range) = range else {
1955 return true;
1956 };
1957 let Some(numeric) = parse_numeric_value_with_unit(source) else {
1958 return false;
1959 };
1960 let above_min = range
1961 .min
1962 .as_deref()
1963 .and_then(|value| value.parse::<f64>().ok())
1964 .is_none_or(|minimum| numeric.value >= minimum);
1965 let below_max = range
1966 .max
1967 .as_deref()
1968 .and_then(|value| value.parse::<f64>().ok())
1969 .is_none_or(|maximum| numeric.value <= maximum);
1970 above_min && below_max
1971}
1972
1973fn component_locus(components: &[CssValueComponentV0]) -> CssValueGrammarLocusV0 {
1974 match (components.first(), components.last()) {
1975 (Some(first), Some(last)) => CssValueGrammarLocusV0 {
1976 start: first.span.start,
1977 end: last.span.end,
1978 },
1979 _ => CssValueGrammarLocusV0 { start: 0, end: 0 },
1980 }
1981}
1982
1983fn grammar_defect(
1984 grammar: &str,
1985 offset: usize,
1986 code: impl Into<String>,
1987 detail: impl Into<String>,
1988) -> CssValueGrammarVerdictV0 {
1989 CssValueGrammarVerdictV0::GrammarDefect {
1990 grammar: grammar.to_string(),
1991 offset,
1992 code: code.into(),
1993 detail: detail.into(),
1994 }
1995}
1996
1997fn strip_matching_quotes(source: &str) -> &str {
1998 if source.len() >= 2 {
1999 let bytes = source.as_bytes();
2000 if matches!(
2001 (bytes[0], bytes[source.len() - 1]),
2002 (b'\'', b'\'') | (b'"', b'"')
2003 ) {
2004 return &source[1..source.len() - 1];
2005 }
2006 }
2007 source
2008}
2009
2010#[cfg(test)]
2011mod tests {
2012 use std::sync::Arc;
2013
2014 use omena_spec_audit::spec_grammar_registry;
2015 use omena_value_lattice::ValueNodeV0;
2016
2017 use super::{
2018 CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0, CssValueGrammarBudgetKindV0,
2019 CssValueGrammarBudgetV0, CssValueGrammarVerdictV0, CssValueValidationClassV0,
2020 CssValueValidationReasonV0, adjudicate_css_value_validation,
2021 audit_css_value_grammar_registry_v0, cached_pinned_vds_expression,
2022 match_and_type_css_value_grammar_v0, match_and_type_standard_property_value_v0,
2023 match_css_value_grammar_v0, match_standard_property_value_v0,
2024 validate_registered_property_value_v0, validate_standard_property_value_v0,
2025 };
2026 use crate::{
2027 AbstractCssTypedValueV0, AbstractCssValueV0, DeclaredValueKindV0,
2028 classify_registered_property_declared_value_v0,
2029 };
2030
2031 fn assert_matches(grammar: &str, value: &str) {
2032 let verdict = match_css_value_grammar_v0(
2033 grammar,
2034 value,
2035 spec_grammar_registry(),
2036 CssValueGrammarBudgetV0::default(),
2037 );
2038 assert!(
2039 verdict.is_matched(),
2040 "{grammar:?} should match {value:?}: {verdict:?}"
2041 );
2042 }
2043
2044 fn assert_unmatched(grammar: &str, value: &str) {
2045 let verdict = match_css_value_grammar_v0(
2046 grammar,
2047 value,
2048 spec_grammar_registry(),
2049 CssValueGrammarBudgetV0::default(),
2050 );
2051 assert!(
2052 verdict.is_definite_mismatch(),
2053 "{grammar:?} should reject {value:?}: {verdict:?}"
2054 );
2055 }
2056
2057 #[test]
2058 fn parsed_grammar_cache_reuses_the_immutable_expression() {
2059 let grammar = "<length> | cache-sentinel";
2060 let first = cached_pinned_vds_expression(grammar);
2061 let second = cached_pinned_vds_expression(grammar);
2062
2063 assert!(matches!(
2064 (&first, &second),
2065 (Ok(first), Ok(second)) if Arc::ptr_eq(first, second)
2066 ));
2067 }
2068
2069 #[test]
2070 fn grammar_conformance_covers_all_combinators_and_multipliers() {
2071 for (grammar, value) in [
2072 ("<length> <color>", "1px red"),
2073 ("<length> && <color>", "red 1px"),
2074 ("<length> || <color>", "red"),
2075 ("auto | <length>", "auto"),
2076 ("[ auto | <length> ]?", ""),
2077 ("<length>*", "1px 2px"),
2078 ("<length>+", "1px 2px"),
2079 ("<length>#", "1px, 2px"),
2080 ("<length>{2,3}", "1px 2px 3px"),
2081 ("<length>#{2}", "1px, 2px"),
2082 ("[ <length>? <color>? ]!", "red"),
2083 ("rgb( <number>#{3} )", "rgb(1, 2, 3)"),
2084 ] {
2085 assert_matches(grammar, value);
2086 }
2087 for (grammar, value) in [
2088 ("<length> <color>", "red 1px"),
2089 ("<length> && <color>", "1px"),
2090 ("<length> || <color>", "auto"),
2091 ("<length>+", ""),
2092 ("<length>#", "1px 2px"),
2093 ("<length>{2,3}", "1px"),
2094 ("[ <length>? <color>? ]!", ""),
2095 ("rgb( <number>#{3} )", "rgb(1, 2)"),
2096 ] {
2097 assert_unmatched(grammar, value);
2098 }
2099 }
2100
2101 #[test]
2102 fn combinator_precedence_is_juxtaposition_then_and_then_double_or_then_or() {
2103 let grammar = "a b && c || d | e";
2104 for value in ["c a b", "a b c", "d", "e"] {
2105 assert_matches(grammar, value);
2106 }
2107 for value in ["a c", "b c", "a b d"] {
2108 assert_unmatched(grammar, value);
2109 }
2110 }
2111
2112 #[test]
2113 fn reference_depth_exhaustion_is_typed_and_provenanced() {
2114 let verdict = match_css_value_grammar_v0(
2115 "<calc-sum>",
2116 "calc(1px + 2px)",
2117 spec_grammar_registry(),
2118 CssValueGrammarBudgetV0 {
2119 max_reference_depth: 0,
2120 ..CssValueGrammarBudgetV0::default()
2121 },
2122 );
2123 assert!(matches!(
2124 verdict,
2125 CssValueGrammarVerdictV0::NotMatchedWithinBudget {
2126 budget: CssValueGrammarBudgetKindV0::ReferenceDepth,
2127 limit: 0,
2128 reference: Some(reference),
2129 ..
2130 } if reference == "calc-sum"
2131 ));
2132 }
2133
2134 #[test]
2135 fn malformed_grammar_is_a_defect_not_a_mismatch() {
2136 let verdict = match_css_value_grammar_v0(
2137 "[ <length> | <color>",
2138 "1px",
2139 spec_grammar_registry(),
2140 CssValueGrammarBudgetV0::default(),
2141 );
2142 assert!(matches!(
2143 verdict,
2144 CssValueGrammarVerdictV0::GrammarDefect { .. }
2145 ));
2146 }
2147
2148 #[test]
2149 fn property_and_type_references_use_the_pinned_registry() {
2150 assert_matches("<'box-sizing'>", "border-box");
2151 assert_matches("<color>", "rebeccapurple");
2152 assert_matches("<rgb()>", "rgb(1 2 3)");
2153 assert!(match_standard_property_value_v0("box-sizing", "content-box").is_matched());
2154 assert!(
2155 match_standard_property_value_v0("box-sizing", "inline-box").is_definite_mismatch()
2156 );
2157 }
2158
2159 #[test]
2160 fn numeric_reference_ranges_are_enforced() {
2161 assert_matches("<number [0,1]>", "0.5");
2162 assert_unmatched("<number [0,1]>", "2");
2163 assert_matches("<length [0,∞]>", "0px");
2164 assert_unmatched("<length [0,∞]>", "-1px");
2165 }
2166
2167 #[test]
2168 fn unitless_zero_matches_length_references_without_widening_other_dimensions() {
2169 for grammar in ["<length>", "<length-percentage>"] {
2170 assert_matches(grammar, "0");
2171 }
2172 for grammar in ["<percentage>", "<angle>", "<time>", "<resolution>"] {
2173 assert_unmatched(grammar, "0");
2174 }
2175 for value in ["1", "-1", "0.5"] {
2176 assert_unmatched("<length>", value);
2177 assert_unmatched("<length-percentage>", value);
2178 }
2179 let calc_verdict = match_css_value_grammar_v0(
2180 "<length> | <calc-sum>",
2181 "calc(0 + 2px)",
2182 spec_grammar_registry(),
2183 CssValueGrammarBudgetV0::default(),
2184 );
2185 let calc_bytes = serde_json::to_vec(&calc_verdict);
2186 assert!(
2187 calc_bytes.is_ok(),
2188 "calc matcher verdict must remain serializable: {calc_bytes:?}"
2189 );
2190 let Ok(calc_bytes) = calc_bytes else {
2191 return;
2192 };
2193 assert_eq!(
2194 calc_bytes,
2195 br#"{"kind":"grammarDefect","grammar":"<length> | <calc-sum>","offset":0,"code":"missingReferencedGrammar","detail":"types reference <dimension> has no syntax"}"#
2196 );
2197 }
2198
2199 #[test]
2200 fn standard_properties_accept_unitless_zero_without_retyping_integer_consumers() {
2201 for (property, value) in [
2202 ("padding", "0"),
2203 ("margin", "0 auto"),
2204 ("border-width", "0 4px 6px"),
2205 ("width", "0"),
2206 ("z-index", "0"),
2207 ("opacity", "0"),
2208 ] {
2209 let verdict = validate_standard_property_value_v0(property, value);
2210 assert_eq!(
2211 verdict.class,
2212 CssValueValidationClassV0::Valid,
2213 "{property}: {value} should be valid: {verdict:?}"
2214 );
2215 }
2216 assert_eq!(
2217 classify_registered_property_declared_value_v0("0"),
2218 DeclaredValueKindV0::Integer
2219 );
2220 }
2221
2222 #[test]
2223 fn nullable_all_in_any_order_operands_can_satisfy_their_slots() {
2224 let grammar = "[ alpha? && [ none | beta ] && gamma? ]";
2225 for value in ["none", "alpha none", "none gamma", "gamma none alpha"] {
2226 assert_matches(grammar, value);
2227 }
2228 assert_unmatched(grammar, "none unexpected");
2229 }
2230
2231 #[test]
2232 fn nested_property_references_keep_inner_comma_repetition_reachable() {
2233 assert_matches("<'box-shadow-color'>", "red, blue");
2234 assert_unmatched("<'box-shadow-color'>", "red blue");
2235
2236 let grammar = "[ <'box-shadow-color'>? && [ none | <length>{2} ] [ <'box-shadow-blur'> <'box-shadow-spread'>? ]? && <'box-shadow-position'>? ]";
2237 assert_matches(grammar, "red none");
2238 assert_unmatched(grammar, "red none unexpected");
2239 }
2240
2241 #[test]
2242 fn sequence_omits_a_comma_only_with_an_omitted_adjacent_component() {
2243 let grammar = "<length>? , <color>";
2244 assert_matches(grammar, "red");
2245 assert_matches(grammar, "1px, red");
2246 assert_unmatched(grammar, ", red");
2247 assert_unmatched(grammar, "1px red");
2248 }
2249
2250 #[test]
2251 fn standard_keyword_grammars_remain_precise_through_nested_expansion() {
2252 for (property, value) in [("box-shadow", "none"), ("background", "transparent")] {
2253 let verdict = validate_standard_property_value_v0(property, value);
2254 assert_eq!(
2255 verdict.class,
2256 CssValueValidationClassV0::Valid,
2257 "{property}: {value} should be valid: {verdict:?}"
2258 );
2259 }
2260 for (property, value) in [
2261 ("box-shadow", "1px nonsense"),
2262 ("background", ", transparent"),
2263 ] {
2264 let verdict = validate_standard_property_value_v0(property, value);
2265 assert_ne!(
2266 verdict.class,
2267 CssValueValidationClassV0::Valid,
2268 "{property}: {value} must not be accepted: {verdict:?}"
2269 );
2270 }
2271 }
2272
2273 #[test]
2274 fn reviewed_compatibility_syntax_and_boundary_policy_are_applied_independently() {
2275 let compatibility_value =
2276 validate_standard_property_value_v0("-webkit-background-clip", "text");
2277 assert_eq!(compatibility_value.class, CssValueValidationClassV0::Valid);
2278 assert_eq!(
2279 compatibility_value.reason,
2280 CssValueValidationReasonV0::GrammarMatched
2281 );
2282 let registry = spec_grammar_registry();
2283 let compatibility_entry = registry.entry("properties", "-webkit-background-clip");
2284 assert!(
2285 compatibility_entry.is_some(),
2286 "compatibility property must remain registered"
2287 );
2288 let Some(compatibility_entry) = compatibility_entry else {
2289 return;
2290 };
2291 assert!(compatibility_entry.override_provenance.is_some());
2292
2293 let forward_tier =
2294 validate_standard_property_value_v0("background", "definitely-not-a-background");
2295 assert_eq!(
2296 forward_tier.class,
2297 CssValueValidationClassV0::NotValidatable
2298 );
2299 assert_eq!(
2300 forward_tier.reason,
2301 CssValueValidationReasonV0::ForwardTierGrammar
2302 );
2303 assert!(forward_tier.verdict.is_definite_mismatch());
2304
2305 let in_boundary = validate_standard_property_value_v0("border-top", "1px nonsense red");
2306 assert_eq!(in_boundary.class, CssValueValidationClassV0::Invalid);
2307 assert_eq!(
2308 in_boundary.reason,
2309 CssValueValidationReasonV0::GrammarUnmatched
2310 );
2311 }
2312
2313 #[test]
2314 fn pinned_registry_rows_are_all_accounted_for_by_the_grammar_parser() {
2315 const MIN_PINNED_REGISTRY_ENTRY_COUNT: usize = 1_700;
2316
2317 let registry = spec_grammar_registry();
2318 let audit = audit_css_value_grammar_registry_v0(registry);
2319 assert_eq!(audit.total_entry_count, registry.total_entry_count());
2320 assert!(
2321 audit.total_entry_count >= MIN_PINNED_REGISTRY_ENTRY_COUNT,
2322 "pinned registry unexpectedly shrank below the audited coverage floor"
2323 );
2324 assert_eq!(audit.categories.len(), 5);
2325 assert_eq!(
2326 audit.parsed_entry_count + audit.missing_syntax_count + audit.grammar_defect_count,
2327 audit.total_entry_count
2328 );
2329 let properties = audit
2330 .categories
2331 .iter()
2332 .find(|category| category.category == "properties");
2333 assert_eq!(
2334 properties.map(|category| (
2335 category.entry_count,
2336 category.parsed_entry_count,
2337 category.missing_syntax_count,
2338 category.grammar_defect_count,
2339 )),
2340 Some((817, 812, 5, 0))
2341 );
2342 assert_eq!(
2343 (
2344 audit.parsed_entry_count,
2345 audit.missing_syntax_count,
2346 audit.grammar_defect_count,
2347 ),
2348 (1_529, 131, 57)
2349 );
2350 }
2351
2352 #[test]
2353 fn matched_compounds_project_through_existing_typed_and_lattice_domains() {
2354 let border = match_and_type_standard_property_value_v0("border-top", "1px solid red");
2355 assert!(border.verdict.is_matched(), "{:?}", border.verdict);
2356 assert!(matches!(
2357 &border.abstract_value,
2358 AbstractCssValueV0::Exact {
2359 typed: Some(typed), ..
2360 } if matches!(
2361 typed.as_ref(),
2362 AbstractCssTypedValueV0::Compound { leaves } if leaves.len() == 3
2363 )
2364 ));
2365 assert!(matches!(
2366 border.projection.as_ref().map(|projection| projection.lattice.root()),
2367 Some(ValueNodeV0::List { items, .. }) if items.len() == 3
2368 ));
2369
2370 let calc = match_and_type_css_value_grammar_v0(
2371 "calc( <length> '+' <length> )",
2372 "calc(1px + 2px)",
2373 spec_grammar_registry(),
2374 CssValueGrammarBudgetV0::default(),
2375 );
2376 assert!(calc.verdict.is_matched(), "{:?}", calc.verdict);
2377 assert!(matches!(
2378 calc.projection.as_ref().map(|projection| projection.lattice.root()),
2379 Some(ValueNodeV0::Function { name, arguments, .. })
2380 if *name == "calc" && arguments.len() == 3
2381 ));
2382
2383 let font_families =
2384 match_and_type_standard_property_value_v0("font-family", "serif, sans-serif");
2385 assert!(
2386 font_families.verdict.is_matched(),
2387 "{:?}",
2388 font_families.verdict
2389 );
2390 assert!(matches!(
2391 font_families
2392 .projection
2393 .as_ref()
2394 .map(|projection| projection.lattice.root()),
2395 Some(ValueNodeV0::List { .. })
2396 ));
2397 }
2398
2399 #[test]
2400 fn rejected_value_preserves_raw_bytes_and_carries_the_match_locus() {
2401 let source = " 1px nonsense red ";
2402 let result = match_and_type_standard_property_value_v0("border-top", source);
2403 assert!(matches!(
2404 result.verdict,
2405 CssValueGrammarVerdictV0::Unmatched {
2406 grammar,
2407 locus,
2408 } if grammar == "<line-width> || <line-style> || <color>"
2409 && locus.start == 2
2410 && locus.end == source.len() - 2
2411 ));
2412 assert_eq!(
2413 result.abstract_value,
2414 AbstractCssValueV0::Raw {
2415 value: source.to_string(),
2416 }
2417 );
2418 assert!(result.projection.is_none());
2419 }
2420
2421 #[test]
2422 fn validation_keeps_invalid_and_not_validatable_outcomes_distinct() {
2423 let invalid = validate_standard_property_value_v0("border-top", "1px nonsense red");
2424 assert_eq!(invalid.class, CssValueValidationClassV0::Invalid);
2425 assert_eq!(invalid.reason, CssValueValidationReasonV0::GrammarUnmatched);
2426
2427 let defect = validate_registered_property_value_v0("<future-value>", "1px");
2428 assert_eq!(defect.class, CssValueValidationClassV0::NotValidatable);
2429 assert_eq!(defect.reason, CssValueValidationReasonV0::GrammarDefect);
2430
2431 let budget_verdict = match_css_value_grammar_v0(
2432 "<calc-sum>",
2433 "calc(1px + 2px)",
2434 spec_grammar_registry(),
2435 CssValueGrammarBudgetV0 {
2436 max_reference_depth: 0,
2437 ..CssValueGrammarBudgetV0::default()
2438 },
2439 );
2440 let budget = adjudicate_css_value_validation("1px", budget_verdict);
2441 assert_eq!(budget.class, CssValueValidationClassV0::NotValidatable);
2442 assert_eq!(
2443 budget.reason,
2444 CssValueValidationReasonV0::MatchBudgetExhausted
2445 );
2446
2447 let deferred = validate_standard_property_value_v0("width", "var(--width)");
2448 assert_eq!(deferred.class, CssValueValidationClassV0::NotValidatable);
2449 assert_eq!(
2450 deferred.reason,
2451 CssValueValidationReasonV0::DeferredSubstitution
2452 );
2453 }
2454
2455 #[test]
2456 fn validation_distinguishes_negative_dimensions_from_vendor_identifiers() {
2457 let valid_negative = validate_standard_property_value_v0("margin", "-10px");
2458 assert_eq!(valid_negative.class, CssValueValidationClassV0::Valid);
2459 assert_eq!(
2460 valid_negative.reason,
2461 CssValueValidationReasonV0::GrammarMatched
2462 );
2463 assert!(valid_negative.verdict.is_matched());
2464
2465 let invalid_negative = validate_standard_property_value_v0("margin", "-10px totally-bogus");
2466 assert_eq!(invalid_negative.class, CssValueValidationClassV0::Invalid);
2467 assert_eq!(
2468 invalid_negative.reason,
2469 CssValueValidationReasonV0::GrammarUnmatched
2470 );
2471 assert!(invalid_negative.verdict.is_definite_mismatch());
2472
2473 let vendor_identifier =
2474 validate_standard_property_value_v0("box-sizing", "-webkit-border-box");
2475 assert_eq!(
2476 vendor_identifier.class,
2477 CssValueValidationClassV0::NotValidatable
2478 );
2479 assert_eq!(
2480 vendor_identifier.reason,
2481 CssValueValidationReasonV0::VendorExtension
2482 );
2483 assert!(vendor_identifier.verdict.is_definite_mismatch());
2484 }
2485
2486 #[test]
2487 fn function_tokens_preserve_validation_boundaries() {
2488 let math_function = validate_standard_property_value_v0("width", "round(up, 101px, 10px)");
2489 assert_eq!(
2490 math_function.class,
2491 CssValueValidationClassV0::NotValidatable
2492 );
2493 assert_eq!(
2494 math_function.reason,
2495 CssValueValidationReasonV0::UnvalidatedStandardFunction
2496 );
2497 assert!(math_function.verdict.is_definite_mismatch());
2498
2499 let quoted_text = validate_standard_property_value_v0("content", "\"var(\"");
2500 assert_eq!(quoted_text.class, CssValueValidationClassV0::Valid);
2501 assert_eq!(
2502 quoted_text.reason,
2503 CssValueValidationReasonV0::GrammarMatched
2504 );
2505 assert!(quoted_text.verdict.is_matched());
2506
2507 let grid_function =
2508 validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
2509 assert_eq!(
2510 grid_function.class,
2511 CssValueValidationClassV0::NotValidatable
2512 );
2513 assert_eq!(
2514 grid_function.reason,
2515 CssValueValidationReasonV0::GrammarDefect
2516 );
2517 assert!(matches!(
2518 grid_function.verdict,
2519 CssValueGrammarVerdictV0::GrammarDefect { .. }
2520 ));
2521 }
2522
2523 #[test]
2524 fn recognized_functions_do_not_mask_adjacent_invalid_components() {
2525 for value in [
2526 "round(up, 101px, 10px)",
2527 "mod(10px, 3px)",
2528 "rem(10px, 3px)",
2529 "sin(45deg)",
2530 "pow(2, 3)",
2531 "sqrt(4)",
2532 "hypot(3px, 4px)",
2533 "abs(-10px)",
2534 ] {
2535 let validation = validate_standard_property_value_v0("width", value);
2536 assert_eq!(
2537 validation.class,
2538 CssValueValidationClassV0::NotValidatable,
2539 "{value} must remain non-definite until its function semantics are modeled"
2540 );
2541 assert_eq!(
2542 validation.reason,
2543 CssValueValidationReasonV0::UnvalidatedStandardFunction,
2544 "{value} must be attributed to the unvalidated standard-function channel"
2545 );
2546 }
2547
2548 let adjacent_scalar =
2549 validate_standard_property_value_v0("width", "round(1, 2) totally-bogus");
2550 assert_eq!(adjacent_scalar.class, CssValueValidationClassV0::Invalid);
2551 assert_eq!(
2552 adjacent_scalar.reason,
2553 CssValueValidationReasonV0::GrammarUnmatched
2554 );
2555
2556 let unregistered_function =
2557 validate_standard_property_value_v0("width", "totally-unknown(1px)");
2558 assert_eq!(
2559 unregistered_function.class,
2560 CssValueValidationClassV0::Invalid
2561 );
2562 assert_eq!(
2563 unregistered_function.reason,
2564 CssValueValidationReasonV0::GrammarUnmatched
2565 );
2566
2567 let compound_value =
2568 validate_standard_property_value_v0("margin", "round(up, 10px, 1px) auto");
2569 assert_eq!(
2570 compound_value.class,
2571 CssValueValidationClassV0::NotValidatable
2572 );
2573 assert_eq!(
2574 compound_value.reason,
2575 CssValueValidationReasonV0::UnvalidatedStandardFunction
2576 );
2577 }
2578
2579 #[test]
2580 fn deferred_validation_uses_parsed_function_names() {
2581 for value in [
2582 "var(--width)",
2583 "env(safe-area-inset-top)",
2584 "attr(data-width type(<length>))",
2585 "calc(1px + 2px)",
2586 "min(1px, 2px)",
2587 "max(1px, 2px)",
2588 "clamp(1px, 2px, 3px)",
2589 "round(up, calc(101px), 10px)",
2590 ] {
2591 let validation = validate_standard_property_value_v0("width", value);
2592 assert_eq!(
2593 validation.class,
2594 CssValueValidationClassV0::NotValidatable,
2595 "{value} must remain deferred"
2596 );
2597 assert_eq!(
2598 validation.reason,
2599 CssValueValidationReasonV0::DeferredSubstitution,
2600 "{value} must be attributed to an actual deferred function component"
2601 );
2602 }
2603
2604 let similarly_named =
2605 validate_standard_property_value_v0("grid-template-columns", "minmax(101px, 1fr)");
2606 assert_eq!(
2607 similarly_named.reason,
2608 CssValueValidationReasonV0::GrammarDefect
2609 );
2610 }
2611
2612 #[test]
2613 fn validation_consumer_policy_table_covers_every_live_consumer() {
2614 assert_eq!(CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0.len(), 4);
2615 assert_eq!(
2616 CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0
2617 .iter()
2618 .map(|policy| policy.consumer)
2619 .collect::<Vec<_>>(),
2620 vec![
2621 "checker.registeredPropertyTypeMismatch",
2622 "checker.invalidPropertyValue",
2623 "scss.nativeCssFunctionParameter",
2624 "scss.nativeCssFunctionReturn",
2625 ]
2626 );
2627 for policy in CSS_VALUE_VALIDATION_CONSUMER_POLICIES_V0 {
2628 assert_eq!(policy.matched, "accept");
2629 assert!(matches!(policy.unmatched, "diagnostic" | "reject"));
2630 assert!(matches!(
2631 policy.forward_tier_unmatched,
2632 "not-applicable" | "not-validatable"
2633 ));
2634 assert!(matches!(policy.grammar_defect, "silent" | "unknown"));
2635 assert!(matches!(policy.budget_exhausted, "silent" | "unknown"));
2636 }
2637 }
2638}