Skip to main content

css_ast/properties/
mod.rs

1use crate::{
2	AppliesTo, BoxPortion, BoxSide, CssAtomSet, CssMetadata, DeclarationKind, DeclarationMetadata, Inherits, Longhand,
3	NodeKinds, PropertyGroup, PropertyKind, Shorthand, Unresolved, VendorPrefixes, values,
4};
5use css_lexer::Kind;
6use css_parse::{
7	AtomSet, Box, 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(Box<'a, 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::shorthand().is_some() {
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 what a shorthand property does to the properties it covers, or [None] if the
232	/// property is not a shorthand.
233	pub fn shorthand_by_name(property_name: CssAtomSet) -> Option<&'static Shorthand> {
234		macro_rules! get_shorthand_by_name {
235			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
236				match property_name {
237					$(
238					CssAtomSet::$name => values::$ty::shorthand(),
239					)+
240					_ => None,
241				}
242			};
243		}
244		apply_properties!(get_shorthand_by_name)
245	}
246
247	/// Returns what the shorthands which cover a given property name do to it, or [None] if no
248	/// shorthand covers it.
249	pub fn longhand_by_name(property_name: CssAtomSet) -> Option<&'static Longhand> {
250		macro_rules! get_longhand_by_name {
251			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
252				match property_name {
253					$(
254					CssAtomSet::$name => values::$ty::longhand(),
255					)+
256					_ => None,
257				}
258			};
259		}
260		apply_properties!(get_longhand_by_name)
261	}
262}
263
264impl<'a> DeclarationValue<'a, CssMetadata> for StyleValue<'a> {
265	fn declaration_metadata(decl: &Declaration<'a, Self, CssMetadata>) -> CssMetadata {
266		// Mark this node as a declaration
267		let mut meta = decl.value.metadata().with_declaration();
268		if decl.important.is_some() {
269			meta.declaration_kinds |= DeclarationKind::Important;
270		}
271		// Check if this is a custom property (dashed ident)
272		if decl.name.is_dashed_ident() {
273			meta.node_kinds |= NodeKinds::Custom;
274		}
275		// Check if the value is unknown
276		if decl.value.is_unknown() {
277			meta.node_kinds |= NodeKinds::Unknown;
278		}
279		// Extract vendor prefix from property name cursor
280		let cursor: Cursor = decl.name.into();
281		meta.vendor_prefixes =
282			CssAtomSet::from_bits(cursor.token().atom_bits()).try_into().unwrap_or(VendorPrefixes::none());
283		// Declarations always have a name property
284		meta.property_kinds |= PropertyKind::Name;
285		meta
286	}
287
288	fn valid_declaration_name<I>(p: &Parser<'a, I>, c: Cursor) -> bool
289	where
290		I: Iterator<Item = Cursor> + Clone,
291	{
292		let atom = p.to_atom::<CssAtomSet>(c);
293		c.token().is_dashed_ident()
294			|| crate::property_atoms::CSS_PROPERTY_ATOMS.contains(&atom)
295			|| CSS_VENDOR_PROPERTY_ATOMS.contains(&atom)
296	}
297
298	fn is_unknown(&self) -> bool {
299		matches!(self, Self::Unknown(_))
300	}
301
302	fn is_custom(&self) -> bool {
303		matches!(self, Self::Custom(_))
304	}
305
306	fn is_initial(&self) -> bool {
307		matches!(self, Self::Initial(_))
308	}
309
310	fn is_inherit(&self) -> bool {
311		matches!(self, Self::Inherit(_))
312	}
313
314	fn is_unset(&self) -> bool {
315		matches!(self, Self::Unset(_))
316	}
317
318	fn is_revert(&self) -> bool {
319		matches!(self, Self::Revert(_))
320	}
321
322	fn is_revert_layer(&self) -> bool {
323		matches!(self, Self::RevertLayer(_))
324	}
325
326	fn is_revert_rule(&self) -> bool {
327		matches!(self, Self::RevertRule(_))
328	}
329
330	fn needs_computing(&self) -> bool {
331		self.metadata().has_computed()
332	}
333
334	fn parse_custom_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
335	where
336		I: Iterator<Item = Cursor> + Clone,
337	{
338		p.parse::<Custom>().map(Self::Custom)
339	}
340
341	fn is_computed_declaration_value<I>(p: &Parser<'a, I>, c: Cursor) -> bool
342	where
343		I: Iterator<Item = Cursor> + Clone,
344	{
345		if !<T![Function]>::peek(p, c) {
346			return false;
347		}
348		let atom = p.to_atom::<CssAtomSet>(c);
349		values::is_substitution_function(atom) || crate::is_math_function(atom)
350	}
351
352	fn parse_computed_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
353	where
354		I: Iterator<Item = Cursor> + Clone,
355	{
356		p.parse::<Unresolved>().map(Self::Unresolved)
357	}
358
359	fn parse_specified_declaration_value<I>(p: &mut Parser<'a, I>, name: Cursor) -> ParserResult<Self>
360	where
361		I: Iterator<Item = Cursor> + Clone,
362	{
363		let c = p.peek_n(1);
364		if c == Kind::Ident {
365			match p.to_atom::<CssAtomSet>(c) {
366				CssAtomSet::Initial => return Ok(Self::Initial(p.parse::<T![Ident]>()?)),
367				CssAtomSet::Inherit => return Ok(Self::Inherit(p.parse::<T![Ident]>()?)),
368				CssAtomSet::Unset => return Ok(Self::Unset(p.parse::<T![Ident]>()?)),
369				CssAtomSet::Revert => return Ok(Self::Revert(p.parse::<T![Ident]>()?)),
370				CssAtomSet::RevertLayer => return Ok(Self::RevertLayer(p.parse::<T![Ident]>()?)),
371				CssAtomSet::RevertRule => return Ok(Self::RevertRule(p.parse::<T![Ident]>()?)),
372				_ => {}
373			}
374		}
375		macro_rules! parse_declaration_value {
376			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $atom: ident,)+ ) => {
377				match p.to_atom::<CssAtomSet>(name) {
378					$(CssAtomSet::$atom => p.parse::<Box<values::$ty>>().map(Self::$name),)+
379					_ => Err(Diagnostic::new(name, Diagnostic::unexpected))?,
380				}
381			}
382		}
383		apply_properties!(parse_declaration_value)
384	}
385
386	fn parse_unknown_declaration_value<I>(p: &mut Parser<'a, I>, _name: Cursor) -> ParserResult<Self>
387	where
388		I: Iterator<Item = Cursor> + Clone,
389	{
390		p.parse::<Unknown>().map(Self::Unknown)
391	}
392}
393
394impl<'a> SemanticEqTrait for crate::StyleValue<'a> {
395	fn semantic_eq(&self, other: &Self, source_text: &str) -> bool {
396		macro_rules! semantic_eq {
397			( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
398				match (self, other) {
399					(Self::Initial(_), Self::Initial(_)) => true,
400					(Self::Inherit(_), Self::Inherit(_)) => true,
401					(Self::Unset(_), Self::Unset(_)) => true,
402					(Self::Revert(_), Self::Revert(_)) => true,
403					(Self::RevertLayer(_), Self::RevertLayer(_)) => true,
404					(Self::RevertRule(_), Self::RevertRule(_)) => true,
405					(Self::Custom(a), Self::Custom(b)) => a.semantic_eq(b, source_text),
406					(Self::Unresolved(a), Self::Unresolved(b)) => a.semantic_eq(b, source_text),
407					(Self::Unknown(a), Self::Unknown(b)) => a.semantic_eq(b, source_text),
408					$((Self::$name(a), Self::$name(b)) => a.semantic_eq(b, source_text),)+
409					(_, _) => false,
410				}
411			};
412		}
413		apply_properties!(semantic_eq)
414	}
415}
416
417#[cfg(test)]
418mod tests {
419	use super::*;
420	use crate::{CssAtomSet, CssMetadata, ShorthandReset, Writes};
421	use css_lexer::Lexer;
422	use css_parse::{Arena, Declaration, Parser, assert_parse};
423
424	type Property<'a> = Declaration<'a, StyleValue<'a>, CssMetadata>;
425
426	#[test]
427	fn test_writes() {
428		assert_parse!(CssAtomSet::ATOMS, Property, "width:inherit", Property { value: StyleValue::Inherit(_), .. });
429		assert_parse!(
430			CssAtomSet::ATOMS,
431			Property,
432			"width:inherit!important",
433			Property { value: StyleValue::Inherit(_), important: Some(_), .. }
434		);
435		assert_parse!(
436			CssAtomSet::ATOMS,
437			Property,
438			"width:revert;",
439			Property { value: StyleValue::Revert(_), semicolon: Some(_), .. }
440		);
441		assert_parse!(CssAtomSet::ATOMS, Property, "width:var(--a)", Property { value: StyleValue::Width(_), .. });
442		assert_parse!(CssAtomSet::ATOMS, Property, "width: var(--a)", Property { value: StyleValue::Width(_), .. });
443		assert_parse!(
444			CssAtomSet::ATOMS,
445			Property,
446			"width: calc(100px + 50px)",
447			Property { value: StyleValue::Width(_), .. }
448		);
449
450		assert_parse!(CssAtomSet::ATOMS, Property, "float:none!important");
451		assert_parse!(CssAtomSet::ATOMS, Property, "width:1px");
452		assert_parse!(CssAtomSet::ATOMS, Property, "width:min(1px, 2px)");
453		assert_parse!(CssAtomSet::ATOMS, Property, "border:1px solid var(--red)");
454		assert_parse!(
455			CssAtomSet::ATOMS,
456			Property,
457			"background:var(--background) var(--select-arrow) calc(100% - 12px) 50%",
458			Property { value: StyleValue::Unresolved(_), .. }
459		);
460		// Should still parse unknown properties
461		assert_parse!(CssAtomSet::ATOMS, Property, "dunno:like whatever");
462		assert_parse!(CssAtomSet::ATOMS, Property, "rotate:1.21gw");
463		assert_parse!(CssAtomSet::ATOMS, Property, "_background:black");
464		assert_parse!(CssAtomSet::ATOMS, Property, "--custom:{foo:{bar};baz:(bing);}");
465	}
466
467	#[test]
468	fn test_property_validation() {
469		let alloc = Arena::new();
470
471		let input = "width:1px";
472		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
473		let mut p = Parser::new(&alloc, input, lexer);
474		let decl = p.parse::<Property>().unwrap();
475		assert!(!decl.value.is_unknown(), "width should be recognized as a known property");
476
477		let input = "notarealproperty:value";
478		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
479		let mut p = Parser::new(&alloc, input, lexer);
480		let decl = p.parse::<Property>().unwrap();
481		assert!(decl.value.is_unknown(), "notarealproperty should be parsed as unknown");
482
483		let input = "-webkit-filter:blur(4px)";
484		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
485		let mut p = Parser::new(&alloc, input, lexer);
486		let decl = p.parse::<Property>().unwrap();
487		assert!(!decl.value.is_unknown(), "-webkit-filter should be recognized as a known property");
488
489		let input = "--custom:value";
490		let lexer = Lexer::new(&CssAtomSet::ATOMS, input);
491		let mut p = Parser::new(&alloc, input, lexer);
492		let decl = p.parse::<Property>().unwrap();
493		assert!(decl.value.is_custom(), "--custom should be parsed as custom property");
494	}
495
496	#[test]
497	fn exposes_additive_shorthand_reset_metadata() {
498		let border = StyleValue::shorthand_by_name(CssAtomSet::Border).unwrap();
499		assert!(border.longhands.contains(&CssAtomSet::BorderWidth));
500		assert!(border.longhands.contains(&CssAtomSet::BorderTopWidth));
501		assert_eq!(border.resets, ShorthandReset::Properties(&[CssAtomSet::BorderImage]));
502		assert_eq!(
503			StyleValue::longhand_by_name(CssAtomSet::BorderLeftColor).unwrap().shorthands,
504			[CssAtomSet::BorderLeft, CssAtomSet::BorderColor, CssAtomSet::Border]
505		);
506		assert_eq!(StyleValue::shorthand_by_name(CssAtomSet::Margin).unwrap().resets, ShorthandReset::Properties(&[]));
507		assert_eq!(StyleValue::shorthand_by_name(CssAtomSet::All).unwrap().resets, ShorthandReset::All);
508		assert_eq!(StyleValue::longhand_by_name(CssAtomSet::BorderImageSource).unwrap().reset_by, [CssAtomSet::Border]);
509		assert!(StyleValue::shorthand_by_name(CssAtomSet::FontSize).is_none());
510	}
511
512	#[test]
513	fn exposes_slots_of_shorthand() {
514		use crate::Slot;
515		assert_eq!(
516			StyleValue::shorthand_by_name(CssAtomSet::Font).unwrap().writes,
517			Some(Writes::Slots(&[
518				Slot { property: CssAtomSet::FontStyle, before: "", after: "", optional: true, copies: false },
519				Slot { property: CssAtomSet::FontVariant, before: "", after: "", optional: true, copies: false },
520				Slot { property: CssAtomSet::FontWeight, before: "", after: "", optional: true, copies: false },
521				Slot { property: CssAtomSet::FontWidth, before: "", after: "", optional: true, copies: false },
522				Slot { property: CssAtomSet::FontSize, before: "", after: "", optional: false, copies: false },
523				Slot { property: CssAtomSet::LineHeight, before: "/", after: "", optional: true, copies: false },
524				Slot { property: CssAtomSet::FontFamily, before: "", after: "", optional: false, copies: false },
525			]))
526		);
527		assert_eq!(
528			StyleValue::shorthand_by_name(CssAtomSet::Flex).unwrap().writes,
529			Some(Writes::Slots(&[
530				Slot { property: CssAtomSet::FlexGrow, before: "", after: "", optional: false, copies: false },
531				Slot { property: CssAtomSet::FlexShrink, before: "", after: "", optional: false, copies: false },
532				Slot { property: CssAtomSet::FlexBasis, before: "", after: "", optional: false, copies: false },
533			]))
534		);
535
536		assert_eq!(StyleValue::shorthand_by_name(CssAtomSet::Margin).unwrap().writes, Some(Writes::Repeat));
537		assert_eq!(
538			StyleValue::shorthand_by_name(CssAtomSet::Margin).unwrap().longhands,
539			[CssAtomSet::MarginTop, CssAtomSet::MarginRight, CssAtomSet::MarginBottom, CssAtomSet::MarginLeft]
540		);
541
542		assert_eq!(
543			StyleValue::shorthand_by_name(CssAtomSet::Border).unwrap().writes,
544			Some(Writes::Slots(&[
545				Slot { property: CssAtomSet::BorderWidth, before: "", after: "", optional: true, copies: false },
546				Slot { property: CssAtomSet::BorderStyle, before: "", after: "", optional: true, copies: false },
547				Slot { property: CssAtomSet::BorderColor, before: "", after: "", optional: true, copies: false },
548			]))
549		);
550
551		assert_eq!(StyleValue::shorthand_by_name(CssAtomSet::BorderColor).unwrap().writes, Some(Writes::Repeat));
552		assert_eq!(
553			StyleValue::shorthand_by_name(CssAtomSet::BorderColor).unwrap().longhands,
554			[
555				CssAtomSet::BorderTopColor,
556				CssAtomSet::BorderRightColor,
557				CssAtomSet::BorderBottomColor,
558				CssAtomSet::BorderLeftColor,
559			]
560		);
561	}
562
563	#[test]
564	fn longhand_by_name_maps_to_shorthands() {
565		for (longhand, shorthands) in [
566			(CssAtomSet::MarginTop, &[CssAtomSet::Margin][..]),
567			(CssAtomSet::JustifyItems, &[CssAtomSet::PlaceItems][..]),
568			(CssAtomSet::ColumnGap, &[CssAtomSet::Gap][..]),
569			(CssAtomSet::RowRuleColor, &[CssAtomSet::RuleColor][..]),
570			(CssAtomSet::LineHeight, &[CssAtomSet::Font][..]),
571			(CssAtomSet::ColumnHeight, &[CssAtomSet::Columns][..]),
572			(CssAtomSet::TextDecorationThickness, &[CssAtomSet::TextDecoration][..]),
573			(CssAtomSet::Top, &[CssAtomSet::Inset][..]),
574		] {
575			assert_eq!(StyleValue::longhand_by_name(longhand).unwrap().shorthands, shorthands, "{}", longhand.to_str());
576		}
577	}
578
579	macro_rules! every_property {
580		( $( $name: ident: $ty: ident$(<$a: lifetime>)? = $str: tt,)+ ) => {
581			[$((CssAtomSet::$name, values::$ty::shorthand())),+]
582		};
583	}
584
585	fn leaves(property: CssAtomSet, out: &mut Vec<CssAtomSet>) {
586		match StyleValue::shorthand_by_name(property) {
587			Some(shorthand) => {
588				for longhand in shorthand.longhands {
589					leaves(*longhand, out);
590				}
591			}
592			None => out.push(property),
593		}
594	}
595
596	#[test]
597	fn every_shorthand_writes_every_longhand_it_sets() {
598		let mut fails = Vec::new();
599		for (property, shorthand) in apply_properties!(every_property) {
600			let Some(shorthand) = shorthand else { continue };
601			let name = property.to_str();
602			let mut set = Vec::new();
603			leaves(property, &mut set);
604			let written = match shorthand.writes {
605				Some(Writes::Slots(slots)) => Some(slots.iter().map(|slot| slot.property).collect::<Vec<_>>()),
606				Some(Writes::Repeat) => Some(shorthand.longhands.to_vec()),
607				Some(Writes::Same) | None => None,
608			};
609			if let Some(written) = written {
610				let mut expanded = Vec::new();
611				for property in &written {
612					leaves(*property, &mut expanded);
613				}
614				for property in &set {
615					if !expanded.contains(property) {
616						fails.push(format!("{name} states no value for {}", property.to_str()));
617					}
618				}
619				for property in &expanded {
620					if !set.contains(property) {
621						fails.push(format!("{name} writes {}, which it does not set", property.to_str()));
622					}
623				}
624				if matches!(shorthand.writes, Some(Writes::Repeat)) && written.len() != expanded.len() {
625					fails.push(format!("{name} repeats its values over a shorthand of its own"));
626				}
627			}
628			if let ShorthandReset::Properties(resets) = shorthand.resets {
629				for reset in resets {
630					if shorthand.longhands.contains(reset) {
631						fails.push(format!("{name} both sets and resets {}", reset.to_str()));
632					}
633				}
634			}
635		}
636		assert_eq!(fails, Vec::<String>::new(), "fails should be empty");
637	}
638
639	#[test]
640	fn every_shorthand_and_longhand_reflect_each_other() {
641		let mut fails = Vec::new();
642		for (property, shorthand) in apply_properties!(every_property) {
643			let name = property.to_str();
644			for longhand in shorthand.map(|shorthand| shorthand.longhands).unwrap_or_default() {
645				let states_it =
646					StyleValue::longhand_by_name(*longhand).is_some_and(|record| record.shorthands.contains(&property));
647				if !states_it {
648					fails.push(format!("{name} sets {}, which does not state it", longhand.to_str()));
649				}
650			}
651			let Some(longhand) = StyleValue::longhand_by_name(property) else { continue };
652			for shorthand in longhand.shorthands {
653				match StyleValue::shorthand_by_name(*shorthand) {
654					Some(record) if !record.longhands.contains(&property) => {
655						fails.push(format!("{name} states the shorthand {}, which does not set it", shorthand.to_str()))
656					}
657					Some(_) => {}
658					None => fails.push(format!("{name} states the shorthand {}, which is not one", shorthand.to_str())),
659				}
660			}
661			for shorthand in longhand.reset_by {
662				match StyleValue::shorthand_by_name(*shorthand) {
663					Some(record) if record.longhands.contains(&property) => {
664						fails.push(format!("{name} is reset by {}, which expresses it instead", shorthand.to_str()))
665					}
666					Some(_) => {}
667					None => fails.push(format!("{name} is reset by {}, which is not a shorthand", shorthand.to_str())),
668				}
669			}
670		}
671		assert_eq!(fails, Vec::<String>::new());
672	}
673
674	#[test]
675	fn lists_the_shorthands_which_state_no_writes() {
676		let mut missing = apply_properties!(every_property)
677			.into_iter()
678			.filter(|(_, shorthand)| shorthand.is_some_and(|shorthand| shorthand.writes.is_none()))
679			.map(|(property, _)| property.to_str())
680			.collect::<Vec<_>>();
681		missing.sort_unstable();
682		assert_eq!(
683			missing,
684			[
685				"-webkit-animation",
686				"all",
687				"animation-delay",
688				"animation-range",
689				"font-synthesis",
690				"font-variant",
691				"grid",
692				"grid-area",
693				"grid-column",
694				"grid-row",
695				"grid-template",
696				"offset",
697				"scroll-timeline",
698				"view-timeline",
699			]
700		);
701	}
702}