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