1use crate::{
2 AttrFunction, CssAtomSet, CssMetadata, EnvFunction, FirstValidFunction, IdentFunction, IfFunction, MathFunction,
3 TreeCountingFunction, Unresolved, VarFunction,
4};
5use css_lexer::ToSpan;
6use css_parse::{
7 Box, Cursor, DeclarationValue, NodeWithMetadata, Parse, Parser, Peek, Result, SemanticEq, ToCursors,
8 ToNormalisedValue, ToNumberValue,
9};
10use csskit_derives::*;
11use csskit_proc_macro::node;
12
13#[inline]
15pub(crate) fn is_substitution_function(atom: CssAtomSet) -> bool {
16 matches!(atom, CssAtomSet::Var | CssAtomSet::Env | CssAtomSet::Attr | CssAtomSet::If | CssAtomSet::FirstValid)
17}
18
19macro_rules! impl_value_slot_parse {
22 ($ty:ident, $sub:ident, $lit:ty) => {
23 impl<'a, T: ::css_parse::Parse<'a> + ::css_parse::Peek<'a>> ::css_parse::Parse<'a> for $ty<'a, T> {
24 fn parse<I>(p: &mut ::css_parse::Parser<'a, I>) -> ::css_parse::Result<Self>
25 where
26 I: ::std::iter::Iterator<Item = ::css_parse::Cursor> + ::std::clone::Clone,
27 {
28 if p.peek::<$sub<T>>() {
29 if !p.enter_substitution() {
30 return Ok(Self::Unresolved(::css_parse::Box::new_in(p.alloc(), p.parse::<Unresolved>()?)));
31 }
32 let sub = p.parse::<$sub<T>>();
33 p.exit_substitution();
34 return Ok(Self::Substituted(::css_parse::Box::new_in(p.alloc(), sub?)));
35 }
36 Ok(Self::Literal(p.parse::<$lit>()?))
37 }
38 }
39 };
40}
41pub(crate) use impl_value_slot_parse;
42
43#[node]
48#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
50#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
51pub enum Value<'a, T> {
52 Literal(T),
53 Substituted(Box<'a, SubstitutionFunction<'a, T>>),
54 #[peek(skip)]
55 Unresolved(Box<'a, Unresolved<'a>>),
56}
57
58impl_value_slot_parse!(Value, SubstitutionFunction, T);
59
60#[node]
65#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
66#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
67#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
68#[derive(csskit_derives::NodeWithMetadata)]
69pub enum SubstitutionFunction<'a, T> {
70 Var(VarFunction<'a, Value<'a, T>>),
71 Env(EnvFunction<'a, Value<'a, T>>),
72 Attr(Box<'a, AttrFunction<'a>>),
73 If(IfFunction<'a, Value<'a, T>>),
74 FirstValid(FirstValidFunction<'a, Value<'a, T>>),
75}
76
77#[node]
85#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
87#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
88pub enum CalcableValue<'a, T> {
89 Literal(T),
90 Substituted(Box<'a, CalcableSubstitutionFunction<'a, T>>),
91 #[peek(skip)]
92 Unresolved(Box<'a, Unresolved<'a>>),
93}
94
95impl_value_slot_parse!(CalcableValue, CalcableSubstitutionFunction, T);
96
97#[node]
104#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
105#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
106#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
107#[derive(csskit_derives::NodeWithMetadata)]
108pub enum CalcableSubstitutionFunction<'a, T> {
109 Math(MathFunction<'a, T>),
110 Var(VarFunction<'a, CalcableValue<'a, T>>),
111 Env(EnvFunction<'a, CalcableValue<'a, T>>),
112 Attr(Box<'a, AttrFunction<'a>>),
113 If(IfFunction<'a, CalcableValue<'a, T>>),
114 FirstValid(FirstValidFunction<'a, CalcableValue<'a, T>>),
115}
116
117#[node]
123#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
124#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
125#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
126pub enum NumericValue<'a, T> {
127 Literal(T),
128 Substituted(Box<'a, NumericSubstitutionFunction<'a, T>>),
129 #[peek(skip)]
130 Unresolved(Box<'a, Unresolved<'a>>),
131}
132
133impl_value_slot_parse!(NumericValue, NumericSubstitutionFunction, T);
134
135#[node]
140#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
141#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
142#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
143#[derive(csskit_derives::NodeWithMetadata)]
144pub enum NumericSubstitutionFunction<'a, T> {
145 TreeCounting(TreeCountingFunction),
146 Math(MathFunction<'a, T>),
147 Var(VarFunction<'a, NumericValue<'a, T>>),
148 Env(EnvFunction<'a, NumericValue<'a, T>>),
149 Attr(Box<'a, AttrFunction<'a>>),
150 If(IfFunction<'a, NumericValue<'a, T>>),
151 FirstValid(FirstValidFunction<'a, NumericValue<'a, T>>),
152}
153
154#[node]
163#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
164#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
165#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
166pub enum KeywordValue<'a, T> {
167 Literal(T),
168 Substituted(Box<'a, KeywordSubstitutionFunction<'a, T>>),
169 #[peek(skip)]
170 Unresolved(Box<'a, Unresolved<'a>>),
171}
172
173impl_value_slot_parse!(KeywordValue, KeywordSubstitutionFunction, T);
174
175#[node]
180#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
181#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
182#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
183#[derive(csskit_derives::NodeWithMetadata)]
184pub enum KeywordSubstitutionFunction<'a, T> {
185 Ident(IdentFunction<'a>),
186 Var(VarFunction<'a, KeywordValue<'a, T>>),
187 Env(EnvFunction<'a, KeywordValue<'a, T>>),
188 Attr(Box<'a, AttrFunction<'a>>),
189 If(IfFunction<'a, KeywordValue<'a, T>>),
190 FirstValid(FirstValidFunction<'a, KeywordValue<'a, T>>),
191}
192
193macro_rules! impl_value_slot_traits {
196 ($ty:ident) => {
197 impl<T: ToNumberValue> ToNumberValue for $ty<'_, T> {
198 fn to_number_value(&self) -> Option<f32> {
199 match self {
200 Self::Literal(t) => t.to_number_value(),
201 Self::Substituted(_) | Self::Unresolved(_) => None,
202 }
203 }
204 }
205
206 impl<T: ToNormalisedValue> ToNormalisedValue for $ty<'_, T> {
207 fn to_normalised_value(&self) -> Option<f32> {
208 match self {
209 Self::Literal(t) => t.to_normalised_value(),
210 Self::Substituted(_) | Self::Unresolved(_) => None,
211 }
212 }
213 }
214
215 impl<'a, T: NodeWithMetadata<CssMetadata>> NodeWithMetadata<CssMetadata> for $ty<'a, T> {
216 fn self_metadata(&self) -> CssMetadata {
217 match self {
218 Self::Literal(_) => CssMetadata::default(),
219 Self::Substituted(_) | Self::Unresolved(_) => CssMetadata {
220 uses_substitution: true,
221 declaration_kinds: crate::DeclarationKind::Computed,
222 ..CssMetadata::default()
223 },
224 }
225 }
226
227 fn metadata(&self) -> CssMetadata {
228 match self {
229 Self::Literal(t) => t.metadata(),
230 Self::Substituted(f) => css_parse::NodeMetadata::merge(f.metadata(), self.self_metadata()),
231 Self::Unresolved(_) => self.self_metadata(),
232 }
233 }
234 }
235
236 impl<'a, T> DeclarationValue<'a, CssMetadata> for $ty<'a, T>
237 where
238 T: Parse<'a>
239 + Peek<'a>
240 + ToCursors
241 + ToSpan
242 + SemanticEq
243 + NodeWithMetadata<CssMetadata>
244 + DeclarationValue<'a, CssMetadata>,
245 {
246 fn is_computed_declaration_value<I>(p: &Parser<'a, I>, c: Cursor) -> bool
247 where
248 I: Iterator<Item = Cursor> + Clone,
249 {
250 <Self as Peek>::peek(p, c)
251 }
252
253 fn is_initial(&self) -> bool {
254 matches!(self, Self::Literal(t) if t.is_initial())
255 }
256 fn is_inherit(&self) -> bool {
257 matches!(self, Self::Literal(t) if t.is_inherit())
258 }
259 fn is_unset(&self) -> bool {
260 matches!(self, Self::Literal(t) if t.is_unset())
261 }
262 fn is_revert(&self) -> bool {
263 matches!(self, Self::Literal(t) if t.is_revert())
264 }
265 fn is_revert_layer(&self) -> bool {
266 matches!(self, Self::Literal(t) if t.is_revert_layer())
267 }
268 fn is_revert_rule(&self) -> bool {
269 matches!(self, Self::Literal(t) if t.is_revert_rule())
270 }
271 fn needs_computing(&self) -> bool {
272 !matches!(self, Self::Literal(_))
273 }
274 fn parse_specified_declaration_value<Iter>(p: &mut Parser<'a, Iter>, name: Cursor) -> Result<Self>
275 where
276 Iter: Iterator<Item = Cursor> + Clone,
277 {
278 let _ = name;
279 p.parse()
280 }
281 }
282 };
283}
284
285impl_value_slot_traits!(Value);
286impl_value_slot_traits!(CalcableValue);
287impl_value_slot_traits!(NumericValue);
288impl_value_slot_traits!(KeywordValue);
289
290#[cfg(test)]
291mod tests {
292 use super::*;
293 use crate::{CSSInt, Color, Length};
294 use css_parse::{assert_parse, assert_peek_false};
295
296 type ValueColor<'a> = Value<'a, Color<'a>>;
297 type CalcLength<'a> = CalcableValue<'a, Length>;
298 type NumericInt<'a> = NumericValue<'a, CSSInt>;
299 type KeywordIdent<'a> = KeywordValue<'a, css_parse::T![Ident]>;
300
301 #[test]
302 fn value_literal() {
303 assert_parse!(CssAtomSet::ATOMS, ValueColor, "red", |v| {
304 assert!(matches!(v, Value::Literal(_)));
305 });
306 }
307
308 #[test]
309 fn value_substituted_var_no_fallback() {
310 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--c)", |v| {
311 assert!(matches!(v, Value::Substituted(_)));
312 });
313 }
314
315 #[test]
316 fn value_substituted_var_typed_fallback() {
317 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--c, red)", |v| {
319 let Value::Substituted(sub) = v else { panic!("expected Substituted") };
320 let SubstitutionFunction::Var(var) = &*sub else { panic!("expected Var") };
321 assert!(matches!(var.fallback.as_deref(), Some(Value::Literal(_))));
322 });
323 }
324
325 #[test]
326 fn value_substituted_env_and_others() {
327 assert_parse!(CssAtomSet::ATOMS, ValueColor, "env(my-color, red)");
328 assert_parse!(CssAtomSet::ATOMS, ValueColor, "attr(data-color)");
329 assert_parse!(CssAtomSet::ATOMS, ValueColor, "first-valid(red, blue)");
330 }
331
332 #[test]
333 fn value_nested_fallback() {
334 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--a, var(--b, red))", |v| {
336 let Value::Substituted(sub) = v else { panic!() };
337 let SubstitutionFunction::Var(outer) = &*sub else { panic!() };
338 let Some(inner) = outer.fallback.as_deref() else { panic!("missing outer fallback") };
339 assert!(matches!(inner, Value::Substituted(_)));
340 });
341 }
342
343 #[test]
344 fn calcable_literal_and_calc() {
345 assert_parse!(CssAtomSet::ATOMS, CalcLength, "10px", |v| {
346 assert!(matches!(v, CalcableValue::Literal(_)));
347 });
348 assert_parse!(CssAtomSet::ATOMS, CalcLength, "calc(1px + 2px)", |v| {
349 let CalcableValue::Substituted(sub) = v else { panic!() };
350 assert!(matches!(&*sub, CalcableSubstitutionFunction::Math(_)));
351 });
352 }
353
354 #[test]
355 fn calcable_var_typed_fallback() {
356 assert_parse!(CssAtomSet::ATOMS, CalcLength, "var(--w, 10px)", |v| {
357 let CalcableValue::Substituted(sub) = v else { panic!() };
358 let CalcableSubstitutionFunction::Var(var) = &*sub else { panic!() };
359 assert!(matches!(var.fallback.as_deref(), Some(CalcableValue::Literal(_))));
360 });
361 }
362
363 #[test]
364 fn numeric_tree_counting_functions() {
365 assert_parse!(CssAtomSet::ATOMS, NumericInt, "sibling-index()", |v| {
366 let NumericValue::Substituted(sub) = v else { panic!() };
367 assert!(matches!(&*sub, NumericSubstitutionFunction::TreeCounting(_)));
368 });
369 assert_parse!(CssAtomSet::ATOMS, NumericInt, "sibling-count()");
370 }
371
372 #[test]
373 fn numeric_covers_alternations_containing_number() {
374 type NumericNumberPercentage<'a> = NumericValue<'a, crate::NumberPercentage>;
377 type NumericAlpha<'a> = NumericValue<'a, crate::OpacityValue>;
378 assert_parse!(CssAtomSet::ATOMS, NumericNumberPercentage, "sibling-index()");
379 assert_parse!(CssAtomSet::ATOMS, NumericNumberPercentage, "50%");
380 assert_parse!(CssAtomSet::ATOMS, NumericAlpha, "sibling-count()");
381 assert_parse!(CssAtomSet::ATOMS, NumericAlpha, "0.5");
382 }
383
384 #[test]
385 fn numeric_keeps_literal_and_calcable_behaviour() {
386 assert_parse!(CssAtomSet::ATOMS, NumericInt, "3", |v| {
387 assert!(matches!(v, NumericValue::Literal(_)));
388 });
389 assert_parse!(CssAtomSet::ATOMS, NumericInt, "calc(sibling-index() + 1)", |v| {
390 let NumericValue::Substituted(sub) = v else { panic!() };
391 assert!(matches!(&*sub, NumericSubstitutionFunction::Math(_)));
392 });
393 }
394
395 #[test]
396 fn numeric_fallback_recurses_into_numeric_slot() {
397 assert_parse!(CssAtomSet::ATOMS, NumericInt, "var(--i, sibling-index())", |v| {
398 let NumericValue::Substituted(sub) = v else { panic!() };
399 let NumericSubstitutionFunction::Var(var) = &*sub else { panic!() };
400 assert!(matches!(var.fallback.as_deref(), Some(NumericValue::Substituted(_))));
401 });
402 }
403
404 #[test]
405 fn calcable_rejects_tree_counting_functions() {
406 assert_peek_false!(CssAtomSet::ATOMS, CalcLength, "sibling-index()");
407 }
408
409 #[test]
410 fn depth_limit_rejects_to_unresolved() {
411 use css_lexer::Lexer;
413 use css_parse::{Arena, Parser};
414
415 let depth = (Parser::<std::vec::IntoIter<css_parse::Cursor>>::MAX_SUBSTITUTION_DEPTH as usize) + 5;
416 let mut input = String::new();
417 for _ in 0..depth {
418 input.push_str("var(--a,");
419 }
420 input.push_str("red");
421 for _ in 0..depth {
422 input.push(')');
423 }
424
425 let alloc = Arena::new();
426 let lexer = Lexer::new(&CssAtomSet::ATOMS, &input);
427 let mut p = Parser::new(&alloc, &input, lexer);
428 let result = p.parse_entirely::<ValueColor>();
430 assert!(result.output.is_some(), "expected parse to succeed via Unresolved degradation");
431 }
432
433 #[test]
434 fn keyword_literal() {
435 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "flex", |v| {
436 assert!(matches!(v, KeywordValue::Literal(_)));
437 });
438 }
439
440 #[test]
441 fn keyword_ident_function() {
442 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "ident('vtl-'sibling-index())", |v| {
443 let KeywordValue::Substituted(sub) = v else { panic!("expected Substituted") };
444 assert!(matches!(&*sub, KeywordSubstitutionFunction::Ident(_)));
445 });
446 }
447
448 #[test]
449 fn keyword_substituted_var_typed_fallback() {
450 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "var(--k, flex)", |v| {
452 let KeywordValue::Substituted(sub) = v else { panic!("expected Substituted") };
453 let KeywordSubstitutionFunction::Var(var) = &*sub else { panic!("expected Var") };
454 assert!(matches!(var.fallback.as_deref(), Some(KeywordValue::Literal(_))));
455 });
456 }
457
458 #[test]
459 fn keyword_rejects_tree_counting_functions() {
460 assert_peek_false!(CssAtomSet::ATOMS, KeywordIdent, "sibling-index()");
461 }
462}