Skip to main content

css_ast/properties/
mod.rs

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