css_ast/properties/
mod.rs

1use crate::{
2	AppliesTo, BoxPortion, BoxSide, CssAtomSet, CssMetadata, DeclarationKind, DeclarationMetadata, Inherits, NodeKinds,
3	PropertyGroup, PropertyKind, VendorPrefixes, values,
4};
5use css_lexer::Kind;
6use css_parse::{
7	AtomSet, ComponentValues, Cursor, Declaration, DeclarationValue, Diagnostic, KindSet, NodeWithMetadata, Parser,
8	Peek, Result as ParserResult, SemanticEq as SemanticEqTrait, State, T,
9};
10use csskit_derives::{Parse, SemanticEq, ToCursors, ToSpan};
11use std::{fmt::Debug, hash::Hash};
12
13// The build.rs generates a list of CSS properties from the value mods
14include!(concat!(env!("OUT_DIR"), "/css_apply_properties.rs"));
15
16#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
17#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
18#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
19#[parse(state = State::Nested, stop = KindSet::RIGHT_CURLY_OR_SEMICOLON)]
20pub struct Custom<'a>(pub ComponentValues<'a>);
21
22#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
23#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
24#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
25#[parse(state = State::Nested, stop = KindSet::RIGHT_CURLY_OR_SEMICOLON)]
26pub struct Computed<'a>(pub ComponentValues<'a>);
27
28impl<'a> Peek<'a> for Computed<'a> {
29	fn peek<I>(p: &Parser<'a, I>, c: Cursor) -> bool
30	where
31		I: Iterator<Item = Cursor> + Clone,
32	{
33		<T![Function]>::peek(p, c)
34			&& matches!(
35				p.to_atom::<CssAtomSet>(c),
36				CssAtomSet::Var
37					| CssAtomSet::Calc
38					| CssAtomSet::Min
39					| CssAtomSet::Max
40					| CssAtomSet::Clamp
41					| CssAtomSet::Round
42					| CssAtomSet::Mod
43					| CssAtomSet::Rem
44					| CssAtomSet::Sin
45					| CssAtomSet::Cos
46					| CssAtomSet::Tan
47					| CssAtomSet::Asin
48					| CssAtomSet::Atan
49					| CssAtomSet::Atan2
50					| CssAtomSet::Pow
51					| CssAtomSet::Sqrt
52					| CssAtomSet::Hypot
53					| CssAtomSet::Log
54					| CssAtomSet::Exp
55					| CssAtomSet::Abs
56					| CssAtomSet::Sign
57			)
58	}
59}
60
61#[derive(Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
62#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
63#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
64#[parse(state = State::Nested, stop = KindSet::RIGHT_CURLY_OR_SEMICOLON)]
65pub struct Unknown<'a>(pub ComponentValues<'a>);
66
67macro_rules! style_value {
68	( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
69		#[derive(ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
70		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
71		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
72		pub enum StyleValue<'a> {
73			#[cfg_attr(feature = "visitable", visit(skip))]
74			Initial(T![Ident]),
75			#[cfg_attr(feature = "visitable", visit(skip))]
76			Inherit(T![Ident]),
77			#[cfg_attr(feature = "visitable", visit(skip))]
78			Unset(T![Ident]),
79			#[cfg_attr(feature = "visitable", visit(skip))]
80			Revert(T![Ident]),
81			#[cfg_attr(feature = "visitable", visit(skip))]
82			RevertLayer(T![Ident]),
83			#[cfg_attr(feature = "serde", serde(untagged))]
84			Custom(Custom<'a>),
85			#[cfg_attr(feature = "serde", serde(untagged))]
86			Computed(Computed<'a>),
87			#[cfg_attr(feature = "serde", serde(untagged))]
88			Unknown(Unknown<'a>),
89			$(
90				#[cfg_attr(feature = "serde", serde(untagged))]
91				$name(values::$ty$(<$a>)?),
92			)+
93		}
94	}
95}
96
97apply_properties!(style_value);
98
99impl<'a> NodeWithMetadata<CssMetadata> for StyleValue<'a> {
100	fn metadata(&self) -> CssMetadata {
101		macro_rules! metadata {
102			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
103				match self {
104					Self::Initial(_) |
105					Self::Inherit(_)|
106					Self::Unset(_)|
107					Self::Revert(_)|
108					Self::RevertLayer(_) => {
109						CssMetadata {
110							declaration_kinds: DeclarationKind::CssWideKeywords,
111							..Default::default()
112						}
113					}
114					Self::Custom(_) => {
115						CssMetadata {
116							declaration_kinds: DeclarationKind::Custom,
117							..Default::default()
118						}
119					}
120					Self::Computed(_) => {
121						CssMetadata {
122							declaration_kinds: DeclarationKind::Computed,
123							..Default::default()
124						}
125					},
126					Self::Unknown(_) => {
127						CssMetadata {
128							node_kinds: NodeKinds::Unknown,
129							..Default::default()
130						}
131					},
132					$(
133					Self::$name(_) => {
134						let mut declaration_kinds = DeclarationKind::none();
135						if values::$ty::is_shorthand() {
136							declaration_kinds |= DeclarationKind::Shorthands;
137						} else {
138							declaration_kinds |= DeclarationKind::Longhands;
139						}
140						CssMetadata {
141							property_groups: values::$ty::property_group(),
142							applies_to: values::$ty::applies_to(),
143							box_sides: values::$ty::box_side(),
144							box_portions: values::$ty::box_portion(),
145							declaration_kinds,
146							unitless_zero_resolves: values::$ty::unitless_zero_resolves(),
147							..Default::default()
148						}
149					}
150					)+
151				}
152			};
153		}
154		apply_properties!(metadata)
155	}
156}
157
158impl<'a> StyleValue<'a> {
159	/// Returns the initial value string for a given property name.
160	/// This is useful when you have `StyleValue::Initial` and need to know what the initial value
161	/// should be based on the property name.
162	pub fn initial_by_name(property_name: CssAtomSet) -> Option<&'static str> {
163		macro_rules! get_initial_by_name {
164			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
165				match property_name {
166					$(
167					CssAtomSet::$name => Some(values::$ty::initial()),
168					)+
169					_ => None,
170				}
171			};
172		}
173		apply_properties!(get_initial_by_name)
174	}
175
176	/// Returns the inherits value for a given property name.
177	pub fn inherits_by_name(property_name: CssAtomSet) -> Option<Inherits> {
178		macro_rules! get_inherits_by_name {
179			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
180				match property_name {
181					$(
182					CssAtomSet::$name => Some(values::$ty::inherits()),
183					)+
184					_ => None,
185				}
186			};
187		}
188		apply_properties!(get_inherits_by_name)
189	}
190
191	/// Returns the applies_to value for a given property name.
192	pub fn applies_to_by_name(property_name: CssAtomSet) -> Option<AppliesTo> {
193		macro_rules! get_applies_to_by_name {
194			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
195				match property_name {
196					$(
197					CssAtomSet::$name => Some(values::$ty::applies_to()),
198					)+
199					_ => None,
200				}
201			};
202		}
203		apply_properties!(get_applies_to_by_name)
204	}
205
206	/// Returns the property_group for a given property name.
207	pub fn property_group_by_name(property_name: CssAtomSet) -> Option<PropertyGroup> {
208		macro_rules! get_property_group_by_name {
209			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
210				match property_name {
211					$(
212					CssAtomSet::$name => Some(values::$ty::property_group()),
213					)+
214					_ => None,
215				}
216			};
217		}
218		apply_properties!(get_property_group_by_name)
219	}
220
221	/// Returns the box_side for a given property name.
222	pub fn box_side_by_name(property_name: CssAtomSet) -> Option<BoxSide> {
223		macro_rules! get_box_side_by_name {
224			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
225				match property_name {
226					$(
227					CssAtomSet::$name => Some(values::$ty::box_side()),
228					)+
229					_ => None,
230				}
231			};
232		}
233		apply_properties!(get_box_side_by_name)
234	}
235
236	/// Returns the box_portion for a given property name.
237	pub fn box_portion_by_name(property_name: CssAtomSet) -> Option<BoxPortion> {
238		macro_rules! get_box_portion_by_name {
239			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
240				match property_name {
241					$(
242					CssAtomSet::$name => Some(values::$ty::box_portion()),
243					)+
244					_ => None,
245				}
246			};
247		}
248		apply_properties!(get_box_portion_by_name)
249	}
250
251	/// Returns the shorthand group for a given property name.
252	/// For longhand properties, returns the shorthand they belong to (e.g., MarginTop -> Margin).
253	/// For shorthands and non-longhand properties, returns CssAtomSet::_None.
254	pub fn shorthand_group_by_name(property_name: CssAtomSet) -> CssAtomSet {
255		macro_rules! get_shorthand_group_by_name {
256			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
257				match property_name {
258					$(
259					CssAtomSet::$name => values::$ty::shorthand_group(),
260					)+
261					_ => CssAtomSet::_None,
262				}
263			};
264		}
265		apply_properties!(get_shorthand_group_by_name)
266	}
267
268	/// Returns the longhands for a given shorthand property name.
269	/// For shorthand properties, returns Some(&[...]) with the list of longhands.
270	/// For non-shorthand properties, returns None.
271	pub fn longhands_by_name(property_name: CssAtomSet) -> Option<&'static [CssAtomSet]> {
272		macro_rules! get_longhands_by_name {
273			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
274				match property_name {
275					$(
276					CssAtomSet::$name => values::$ty::longhands(),
277					)+
278					_ => None,
279				}
280			};
281		}
282		apply_properties!(get_longhands_by_name)
283	}
284
285	/// Returns whether a given property name is a shorthand.
286	pub fn is_shorthand_by_name(property_name: CssAtomSet) -> bool {
287		macro_rules! get_is_shorthand_by_name {
288			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
289				match property_name {
290					$(
291					CssAtomSet::$name => values::$ty::is_shorthand(),
292					)+
293					_ => false,
294				}
295			};
296		}
297		apply_properties!(get_is_shorthand_by_name)
298	}
299}
300
301impl<'a> DeclarationValue<'a, CssMetadata> for StyleValue<'a> {
302	type ComputedValue = Computed<'a>;
303
304	fn declaration_metadata(decl: &Declaration<'a, Self, CssMetadata>) -> CssMetadata {
305		let mut meta = decl.value.metadata();
306		// Mark this node as a declaration
307		meta.node_kinds |= NodeKinds::Declaration;
308		if decl.important.is_some() {
309			meta.declaration_kinds |= DeclarationKind::Important;
310		}
311		// Check if this is a custom property (dashed ident)
312		if decl.name.is_dashed_ident() {
313			meta.node_kinds |= NodeKinds::Custom;
314		}
315		// Check if the value is unknown
316		if decl.value.is_unknown() {
317			meta.node_kinds |= NodeKinds::Unknown;
318		}
319		// Extract vendor prefix from property name cursor
320		let cursor: Cursor = decl.name.into();
321		meta.vendor_prefixes = CssAtomSet::from_bits(cursor.atom_bits()).try_into().unwrap_or(VendorPrefixes::none());
322		// Declarations always have a name property
323		meta.property_kinds |= PropertyKind::Name;
324		meta
325	}
326
327	fn valid_declaration_name<I>(p: &Parser<'a, I>, c: Cursor) -> bool
328	where
329		I: Iterator<Item = Cursor> + Clone,
330	{
331		c.token().is_dashed_ident() || crate::property_atoms::CSS_PROPERTY_ATOMS.contains(&p.to_atom::<CssAtomSet>(c))
332	}
333
334	fn is_unknown(&self) -> bool {
335		matches!(self, Self::Unknown(_))
336	}
337
338	fn is_custom(&self) -> bool {
339		matches!(self, Self::Custom(_))
340	}
341
342	fn is_initial(&self) -> bool {
343		matches!(self, Self::Initial(_))
344	}
345
346	fn is_inherit(&self) -> bool {
347		matches!(self, Self::Inherit(_))
348	}
349
350	fn is_unset(&self) -> bool {
351		matches!(self, Self::Unset(_))
352	}
353
354	fn is_revert(&self) -> bool {
355		matches!(self, Self::Revert(_))
356	}
357
358	fn is_revert_layer(&self) -> bool {
359		matches!(self, Self::RevertLayer(_))
360	}
361
362	fn needs_computing(&self) -> bool {
363		matches!(self, Self::Computed(_))
364	}
365
366	fn parse_custom_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
367	where
368		I: Iterator<Item = Cursor> + Clone,
369	{
370		p.parse::<Custom>().map(Self::Custom)
371	}
372
373	fn parse_computed_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
374	where
375		I: Iterator<Item = Cursor> + Clone,
376	{
377		p.parse::<Computed>().map(Self::Computed)
378	}
379
380	fn parse_specified_declaration_value<I>(p: &mut Parser<'a, I>, name: Cursor) -> ParserResult<Self>
381	where
382		I: Iterator<Item = Cursor> + Clone,
383	{
384		let c = p.peek_n(1);
385		if c == Kind::Ident {
386			match p.to_atom::<CssAtomSet>(c) {
387				CssAtomSet::Initial => return Ok(Self::Initial(p.parse::<T![Ident]>()?)),
388				CssAtomSet::Inherit => return Ok(Self::Inherit(p.parse::<T![Ident]>()?)),
389				CssAtomSet::Unset => return Ok(Self::Unset(p.parse::<T![Ident]>()?)),
390				CssAtomSet::Revert => return Ok(Self::Revert(p.parse::<T![Ident]>()?)),
391				CssAtomSet::RevertLayer => return Ok(Self::RevertLayer(p.parse::<T![Ident]>()?)),
392				_ => {}
393			}
394		}
395		macro_rules! parse_declaration_value {
396			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $atom: ident,)+ ) => {
397				match p.to_atom::<CssAtomSet>(name) {
398					$(CssAtomSet::$atom => p.parse::<values::$ty>().map(Self::$name),)+
399					_ => Err(Diagnostic::new(name, Diagnostic::unexpected))?,
400				}
401			}
402		}
403		apply_properties!(parse_declaration_value)
404	}
405
406	fn parse_unknown_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
407	where
408		I: Iterator<Item = Cursor> + Clone,
409	{
410		p.parse::<Unknown>().map(Self::Unknown)
411	}
412}
413
414impl<'a> SemanticEqTrait for crate::StyleValue<'a> {
415	fn semantic_eq(&self, other: &Self) -> bool {
416		macro_rules! semantic_eq {
417			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
418				match (self, other) {
419					(Self::Initial(_), Self::Initial(_)) => true,
420					(Self::Inherit(_), Self::Inherit(_)) => true,
421					(Self::Unset(_), Self::Unset(_)) => true,
422					(Self::Revert(_), Self::Revert(_)) => true,
423					(Self::RevertLayer(_), Self::RevertLayer(_)) => true,
424					(Self::Custom(a), Self::Custom(b)) => a.semantic_eq(b),
425					(Self::Computed(a), Self::Computed(b)) => a.semantic_eq(b),
426					(Self::Unknown(a), Self::Unknown(b)) => a.semantic_eq(b),
427					$((Self::$name(a), Self::$name(b)) => a.semantic_eq(b),)+
428					(_, _) => false,
429				}
430			};
431		}
432		apply_properties!(semantic_eq)
433	}
434}
435
436#[cfg(test)]
437mod tests {
438	use super::*;
439	use crate::{CssAtomSet, CssMetadata};
440	use bumpalo::Bump;
441	use css_lexer::Lexer;
442	use css_parse::{Declaration, Parser, assert_parse};
443
444	type Property<'a> = Declaration<'a, StyleValue<'a>, CssMetadata>;
445
446	#[test]
447	fn size_test() {
448		assert_eq!(std::mem::size_of::<Property>(), 488);
449		assert_eq!(std::mem::size_of::<StyleValue>(), 416);
450	}
451
452	#[test]
453	fn test_writes() {
454		assert_parse!(CssAtomSet::ATOMS, Property, "width:inherit", Property { value: StyleValue::Inherit(_), .. });
455		assert_parse!(
456			CssAtomSet::ATOMS,
457			Property,
458			"width:inherit!important",
459			Property { value: StyleValue::Inherit(_), important: Some(_), .. }
460		);
461		assert_parse!(
462			CssAtomSet::ATOMS,
463			Property,
464			"width:revert;",
465			Property { value: StyleValue::Revert(_), semicolon: Some(_), .. }
466		);
467		assert_parse!(CssAtomSet::ATOMS, Property, "width:var(--a)", Property { value: StyleValue::Computed(_), .. });
468
469		assert_parse!(CssAtomSet::ATOMS, Property, "float:none!important");
470		assert_parse!(CssAtomSet::ATOMS, Property, "width:1px");
471		assert_parse!(CssAtomSet::ATOMS, Property, "width:min(1px, 2px)");
472		assert_parse!(CssAtomSet::ATOMS, Property, "border:1px solid var(--red)");
473		// Should still parse unknown properties
474		assert_parse!(CssAtomSet::ATOMS, Property, "dunno:like whatever");
475		assert_parse!(CssAtomSet::ATOMS, Property, "rotate:1.21gw");
476		assert_parse!(CssAtomSet::ATOMS, Property, "_background:black");
477		assert_parse!(CssAtomSet::ATOMS, Property, "--custom:{foo:{bar};baz:(bing);}");
478	}
479
480	#[test]
481	fn test_property_validation() {
482		let bump = Bump::new();
483
484		let input = "width:1px";
485		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
486		let mut p = Parser::new(&bump, input, lexer);
487		let decl = p.parse::<Property>().unwrap();
488		assert!(!decl.value.is_unknown(), "width should be recognized as a known property");
489
490		let input = "notarealproperty:value";
491		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
492		let mut p = Parser::new(&bump, input, lexer);
493		let decl = p.parse::<Property>().unwrap();
494		assert!(decl.value.is_unknown(), "notarealproperty should be parsed as unknown");
495
496		let input = "--custom:value";
497		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
498		let mut p = Parser::new(&bump, input, lexer);
499		let decl = p.parse::<Property>().unwrap();
500		assert!(decl.value.is_custom(), "--custom should be parsed as custom property");
501	}
502}