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(Box<'a, 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(Box<'a, 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
193#[node]
198#[derive(Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
199#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
200#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
201pub enum QueryValue<'a, T> {
202 Literal(T),
203 Substituted(Box<'a, QuerySubstitutionFunction<'a, T>>),
204 #[peek(skip)]
205 Unresolved(Box<'a, Unresolved<'a>>),
206}
207
208impl_value_slot_parse!(QueryValue, QuerySubstitutionFunction, T);
209
210#[node]
212#[derive(Peek, Parse, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
213#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
214#[cfg_attr(feature = "visitable", derive(Visitable), visit(children))]
215#[derive(csskit_derives::NodeWithMetadata)]
216pub enum QuerySubstitutionFunction<'a, T> {
217 Math(MathFunction<'a, T>),
218 Env(EnvFunction<'a, QueryValue<'a, T>>),
219}
220
221macro_rules! impl_value_slot_traits {
224 ($ty:ident) => {
225 impl<T: ToNumberValue> ToNumberValue for $ty<'_, T> {
226 fn to_number_value(&self) -> Option<f32> {
227 match self {
228 Self::Literal(t) => t.to_number_value(),
229 Self::Substituted(_) | Self::Unresolved(_) => None,
230 }
231 }
232 }
233
234 impl<T: ToNormalisedValue> ToNormalisedValue for $ty<'_, T> {
235 fn to_normalised_value(&self) -> Option<f32> {
236 match self {
237 Self::Literal(t) => t.to_normalised_value(),
238 Self::Substituted(_) | Self::Unresolved(_) => None,
239 }
240 }
241 }
242
243 impl<'a, T: NodeWithMetadata<CssMetadata>> NodeWithMetadata<CssMetadata> for $ty<'a, T> {
244 fn self_metadata(&self) -> CssMetadata {
245 match self {
246 Self::Literal(_) => CssMetadata::default(),
247 Self::Substituted(_) | Self::Unresolved(_) => CssMetadata {
248 uses_substitution: true,
249 declaration_kinds: crate::DeclarationKind::Computed,
250 ..CssMetadata::default()
251 },
252 }
253 }
254
255 fn metadata(&self) -> CssMetadata {
256 match self {
257 Self::Literal(t) => t.metadata(),
258 Self::Substituted(f) => css_parse::NodeMetadata::merge(f.metadata(), self.self_metadata()),
259 Self::Unresolved(_) => self.self_metadata(),
260 }
261 }
262 }
263
264 impl<'a, T> DeclarationValue<'a, CssMetadata> for $ty<'a, T>
265 where
266 T: Parse<'a>
267 + Peek<'a>
268 + ToCursors
269 + ToSpan
270 + SemanticEq
271 + NodeWithMetadata<CssMetadata>
272 + DeclarationValue<'a, CssMetadata>,
273 {
274 fn is_computed_declaration_value<I>(p: &Parser<'a, I>, c: Cursor) -> bool
275 where
276 I: Iterator<Item = Cursor> + Clone,
277 {
278 <Self as Peek>::peek(p, c)
279 }
280
281 fn is_initial(&self) -> bool {
282 matches!(self, Self::Literal(t) if t.is_initial())
283 }
284 fn is_inherit(&self) -> bool {
285 matches!(self, Self::Literal(t) if t.is_inherit())
286 }
287 fn is_unset(&self) -> bool {
288 matches!(self, Self::Literal(t) if t.is_unset())
289 }
290 fn is_revert(&self) -> bool {
291 matches!(self, Self::Literal(t) if t.is_revert())
292 }
293 fn is_revert_layer(&self) -> bool {
294 matches!(self, Self::Literal(t) if t.is_revert_layer())
295 }
296 fn is_revert_rule(&self) -> bool {
297 matches!(self, Self::Literal(t) if t.is_revert_rule())
298 }
299 fn needs_computing(&self) -> bool {
300 !matches!(self, Self::Literal(_))
301 }
302 fn parse_specified_declaration_value<Iter>(p: &mut Parser<'a, Iter>, name: Cursor) -> Result<Self>
303 where
304 Iter: Iterator<Item = Cursor> + Clone,
305 {
306 let _ = name;
307 p.parse()
308 }
309 }
310 };
311}
312
313impl_value_slot_traits!(Value);
314impl_value_slot_traits!(CalcableValue);
315impl_value_slot_traits!(NumericValue);
316impl_value_slot_traits!(KeywordValue);
317impl_value_slot_traits!(QueryValue);
318
319#[cfg(test)]
320mod tests {
321 use super::*;
322 use crate::{CSSInt, Color, Length};
323 use css_parse::{assert_parse, assert_peek_false};
324
325 type ValueColor<'a> = Value<'a, Color<'a>>;
326 type CalcLength<'a> = CalcableValue<'a, Length>;
327 type NumericInt<'a> = NumericValue<'a, CSSInt>;
328 type KeywordIdent<'a> = KeywordValue<'a, css_parse::T![Ident]>;
329 type QueryLength<'a> = QueryValue<'a, Length>;
330
331 #[test]
332 fn value_literal() {
333 assert_parse!(CssAtomSet::ATOMS, ValueColor, "red", |v| {
334 assert!(matches!(v, Value::Literal(_)));
335 });
336 }
337
338 #[test]
339 fn value_substituted_var_no_fallback() {
340 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--c)", |v| {
341 assert!(matches!(v, Value::Substituted(_)));
342 });
343 }
344
345 #[test]
346 fn value_substituted_var_typed_fallback() {
347 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--c, red)", |v| {
349 let Value::Substituted(sub) = v else { panic!("expected Substituted") };
350 let SubstitutionFunction::Var(var) = &*sub else { panic!("expected Var") };
351 assert!(matches!(var.fallback.as_deref(), Some(Value::Literal(_))));
352 });
353 }
354
355 #[test]
356 fn value_substituted_env_and_others() {
357 assert_parse!(CssAtomSet::ATOMS, ValueColor, "env(my-color, red)");
358 assert_parse!(CssAtomSet::ATOMS, ValueColor, "attr(data-color)");
359 assert_parse!(CssAtomSet::ATOMS, ValueColor, "first-valid(red, blue)");
360 }
361
362 #[test]
363 fn value_nested_fallback() {
364 assert_parse!(CssAtomSet::ATOMS, ValueColor, "var(--a, var(--b, red))", |v| {
366 let Value::Substituted(sub) = v else { panic!() };
367 let SubstitutionFunction::Var(outer) = &*sub else { panic!() };
368 let Some(inner) = outer.fallback.as_deref() else { panic!("missing outer fallback") };
369 assert!(matches!(inner, Value::Substituted(_)));
370 });
371 }
372
373 #[test]
374 fn calcable_literal_and_calc() {
375 assert_parse!(CssAtomSet::ATOMS, CalcLength, "10px", |v| {
376 assert!(matches!(v, CalcableValue::Literal(_)));
377 });
378 assert_parse!(CssAtomSet::ATOMS, CalcLength, "calc(1px + 2px)", |v| {
379 let CalcableValue::Substituted(sub) = v else { panic!() };
380 assert!(matches!(&*sub, CalcableSubstitutionFunction::Math(_)));
381 });
382 }
383
384 #[test]
385 fn calcable_var_typed_fallback() {
386 assert_parse!(CssAtomSet::ATOMS, CalcLength, "var(--w, 10px)", |v| {
387 let CalcableValue::Substituted(sub) = v else { panic!() };
388 let CalcableSubstitutionFunction::Var(var) = &*sub else { panic!() };
389 assert!(matches!(var.fallback.as_deref(), Some(CalcableValue::Literal(_))));
390 });
391 }
392
393 #[test]
394 fn numeric_tree_counting_functions() {
395 assert_parse!(CssAtomSet::ATOMS, NumericInt, "sibling-index()", |v| {
396 let NumericValue::Substituted(sub) = v else { panic!() };
397 assert!(matches!(&*sub, NumericSubstitutionFunction::TreeCounting(_)));
398 });
399 assert_parse!(CssAtomSet::ATOMS, NumericInt, "sibling-count()");
400 }
401
402 #[test]
403 fn numeric_covers_alternations_containing_number() {
404 type NumericNumberPercentage<'a> = NumericValue<'a, crate::NumberPercentage>;
407 type NumericAlpha<'a> = NumericValue<'a, crate::OpacityValue>;
408 assert_parse!(CssAtomSet::ATOMS, NumericNumberPercentage, "sibling-index()");
409 assert_parse!(CssAtomSet::ATOMS, NumericNumberPercentage, "50%");
410 assert_parse!(CssAtomSet::ATOMS, NumericAlpha, "sibling-count()");
411 assert_parse!(CssAtomSet::ATOMS, NumericAlpha, "0.5");
412 }
413
414 #[test]
415 fn numeric_keeps_literal_and_calcable_behaviour() {
416 assert_parse!(CssAtomSet::ATOMS, NumericInt, "3", |v| {
417 assert!(matches!(v, NumericValue::Literal(_)));
418 });
419 assert_parse!(CssAtomSet::ATOMS, NumericInt, "calc(sibling-index() + 1)", |v| {
420 let NumericValue::Substituted(sub) = v else { panic!() };
421 assert!(matches!(&*sub, NumericSubstitutionFunction::Math(_)));
422 });
423 }
424
425 #[test]
426 fn numeric_fallback_recurses_into_numeric_slot() {
427 assert_parse!(CssAtomSet::ATOMS, NumericInt, "var(--i, sibling-index())", |v| {
428 let NumericValue::Substituted(sub) = v else { panic!() };
429 let NumericSubstitutionFunction::Var(var) = &*sub else { panic!() };
430 assert!(matches!(var.fallback.as_deref(), Some(NumericValue::Substituted(_))));
431 });
432 }
433
434 #[test]
435 fn calcable_rejects_tree_counting_functions() {
436 assert_peek_false!(CssAtomSet::ATOMS, CalcLength, "sibling-index()");
437 }
438
439 #[test]
440 fn depth_limit_rejects_to_unresolved() {
441 use css_lexer::Lexer;
443 use css_parse::{Arena, Parser};
444
445 let depth = (Parser::<std::vec::IntoIter<css_parse::Cursor>>::MAX_SUBSTITUTION_DEPTH as usize) + 5;
446 let mut input = String::new();
447 for _ in 0..depth {
448 input.push_str("var(--a,");
449 }
450 input.push_str("red");
451 for _ in 0..depth {
452 input.push(')');
453 }
454
455 let alloc = Arena::new();
456 let lexer = Lexer::new(&CssAtomSet::ATOMS, &input);
457 let mut p = Parser::new(&alloc, &input, lexer);
458 let result = p.parse_entirely::<ValueColor>();
460 assert!(result.output.is_some(), "expected parse to succeed via Unresolved degradation");
461 }
462
463 #[test]
464 fn keyword_literal() {
465 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "flex", |v| {
466 assert!(matches!(v, KeywordValue::Literal(_)));
467 });
468 }
469
470 #[test]
471 fn keyword_ident_function() {
472 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "ident('vtl-'sibling-index())", |v| {
473 let KeywordValue::Substituted(sub) = v else { panic!("expected Substituted") };
474 assert!(matches!(&*sub, KeywordSubstitutionFunction::Ident(_)));
475 });
476 }
477
478 #[test]
479 fn keyword_substituted_var_typed_fallback() {
480 assert_parse!(CssAtomSet::ATOMS, KeywordIdent, "var(--k, flex)", |v| {
482 let KeywordValue::Substituted(sub) = v else { panic!("expected Substituted") };
483 let KeywordSubstitutionFunction::Var(var) = &*sub else { panic!("expected Var") };
484 assert!(matches!(var.fallback.as_deref(), Some(KeywordValue::Literal(_))));
485 });
486 }
487
488 #[test]
489 fn query_math_and_env() {
490 assert_parse!(CssAtomSet::ATOMS, QueryLength, "48rem", |v| {
491 assert!(matches!(v, QueryValue::Literal(_)));
492 });
493 assert_parse!(CssAtomSet::ATOMS, QueryLength, "calc(48rem - 0.02px)", |v| {
494 let QueryValue::Substituted(sub) = v else { panic!() };
495 assert!(matches!(&*sub, QuerySubstitutionFunction::Math(_)));
496 });
497 assert_parse!(CssAtomSet::ATOMS, QueryLength, "env(safe-area-inset-top, 10px)", |v| {
498 let QueryValue::Substituted(sub) = v else { panic!() };
499 let QuerySubstitutionFunction::Env(env) = &*sub else { panic!() };
500 assert!(matches!(env.fallback.as_deref(), Some(QueryValue::Literal(_))));
501 });
502 }
503
504 #[test]
505 fn query_rejects_other_substitution_functions() {
506 assert_peek_false!(CssAtomSet::ATOMS, QueryLength, "var(--w)");
507 assert_peek_false!(CssAtomSet::ATOMS, QueryLength, "attr(data-w)");
508 }
509
510 #[test]
511 fn keyword_rejects_tree_counting_functions() {
512 assert_peek_false!(CssAtomSet::ATOMS, KeywordIdent, "sibling-index()");
513 }
514}