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
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(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 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_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 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 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 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 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 let mut meta = decl.value.metadata().with_declaration();
315 if decl.important.is_some() {
316 meta.declaration_kinds |= DeclarationKind::Important;
317 }
318 if decl.name.is_dashed_ident() {
320 meta.node_kinds |= NodeKinds::Custom;
321 }
322 if decl.value.is_unknown() {
324 meta.node_kinds |= NodeKinds::Unknown;
325 }
326 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 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 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}