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
14include!(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 #[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 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 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 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 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 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 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 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 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 let mut meta = decl.value.metadata().with_declaration();
268 if decl.important.is_some() {
269 meta.declaration_kinds |= DeclarationKind::Important;
270 }
271 if decl.name.is_dashed_ident() {
273 meta.node_kinds |= NodeKinds::Custom;
274 }
275 if decl.value.is_unknown() {
277 meta.node_kinds |= NodeKinds::Unknown;
278 }
279 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 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 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}