Skip to main content

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