css_ast/rules/media/
mod.rs

1use super::prelude::*;
2use crate::{AtRuleId, NodeKinds};
3
4mod features;
5pub use features::*;
6
7// https://drafts.csswg.org/mediaqueries-4/
8#[derive(Peek, Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
9#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
10#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.at-rules.media"))]
11#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
12pub struct MediaRule<'a> {
13	#[cfg_attr(feature = "visitable", visit(skip))]
14	#[atom(CssAtomSet::Media)]
15	pub name: T![AtKeyword],
16	pub prelude: MediaQueryList<'a>,
17	pub block: MediaRuleBlock<'a>,
18}
19
20impl<'a> NodeWithMetadata<CssMetadata> for MediaRule<'a> {
21	fn self_metadata(&self) -> CssMetadata {
22		let child_meta = self.block.0.metadata();
23		let is_empty = child_meta.declaration_kinds.is_none() && !child_meta.has_rules();
24		let mut node_kinds = NodeKinds::AtRule;
25		if is_empty {
26			node_kinds |= NodeKinds::EmptyBlock;
27		}
28		CssMetadata { used_at_rules: AtRuleId::Media, node_kinds, ..Default::default() }
29	}
30
31	fn metadata(&self) -> CssMetadata {
32		self.block.0.metadata().merge(self.self_metadata())
33	}
34}
35
36#[derive(Peek, Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
37#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
38#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
39pub struct MediaRuleBlock<'a>(pub Block<'a, StyleValue<'a>, Rule<'a>, CssMetadata>);
40
41#[derive(Peek, Parse, ToSpan, ToCursors, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
43#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
44pub struct MediaQueryList<'a>(pub CommaSeparated<'a, MediaQuery<'a>, 1>);
45
46#[derive(Parse, Peek, ToCursors, IntoCursor, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
47#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
48#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
49#[derive(csskit_derives::NodeWithMetadata)]
50pub enum MediaType {
51	#[atom(CssAtomSet::All)]
52	#[cfg_attr(feature = "visitable", visit(skip))]
53	All(T![Ident]),
54	#[atom(CssAtomSet::Print)]
55	#[cfg_attr(feature = "visitable", visit(skip))]
56	Print(T![Ident]),
57	#[atom(CssAtomSet::Screen)]
58	#[cfg_attr(feature = "visitable", visit(skip))]
59	Screen(T![Ident]),
60	#[cfg_attr(feature = "visitable", visit(skip))]
61	Custom(T![Ident]),
62}
63
64impl MediaType {
65	#[allow(dead_code)]
66	fn invalid_ident(atom: &CssAtomSet) -> bool {
67		matches!(atom, CssAtomSet::Only | CssAtomSet::Not | CssAtomSet::And | CssAtomSet::Or | CssAtomSet::Layer)
68	}
69}
70
71#[derive(Parse, Peek, ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
72#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
73#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable), visit)]
74#[derive(csskit_derives::NodeWithMetadata)]
75pub enum MediaPreCondition {
76	#[atom(CssAtomSet::Not)]
77	#[cfg_attr(feature = "visitable", visit(skip))]
78	Not(T![Ident]),
79	#[atom(CssAtomSet::Only)]
80	#[cfg_attr(feature = "visitable", visit(skip))]
81	Only(T![Ident]),
82}
83
84#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
85#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
86#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
87pub struct MediaQuery<'a> {
88	precondition: Option<MediaPreCondition>,
89	media_type: Option<MediaType>,
90	#[cfg_attr(feature = "visitable", visit(skip))]
91	and: Option<T![Ident]>,
92	condition: Option<MediaCondition<'a>>,
93}
94
95impl<'a> Peek<'a> for MediaQuery<'a> {
96	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Ident, Kind::LeftParen]);
97}
98
99impl<'a> Parse<'a> for MediaQuery<'a> {
100	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
101	where
102		I: Iterator<Item = Cursor> + Clone,
103	{
104		let mut precondition = None;
105		let mut media_type = None;
106		let mut and = None;
107		let mut condition = None;
108		if p.peek::<T!['(']>() {
109			condition = Some(p.parse::<MediaCondition<'a>>()?);
110			return Ok(Self { precondition, media_type, and, condition });
111		}
112		let c = p.peek_n(1);
113		if MediaPreCondition::peek(p, c) {
114			precondition = Some(p.parse::<MediaPreCondition>()?);
115		} else if MediaType::peek(p, c) {
116			media_type = Some(p.parse::<MediaType>()?);
117		} else {
118			Err(Diagnostic::new(c, Diagnostic::expected_ident))?
119		}
120		if p.peek::<T![Ident]>() && precondition.is_some() {
121			let c: Cursor = p.peek_n(1);
122			if MediaType::peek(p, c) {
123				media_type = Some(p.parse::<MediaType>()?);
124			} else {
125				Err(Diagnostic::new(c, Diagnostic::expected_ident))?
126			}
127		}
128		let c = p.peek_n(1);
129		if c == Kind::Ident && p.equals_atom(c, &CssAtomSet::And) {
130			and = Some(p.parse::<T![Ident]>()?);
131			condition = Some(p.parse::<MediaCondition>()?);
132		}
133		Ok(Self { precondition, media_type, and, condition })
134	}
135}
136
137#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
139#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
140pub enum MediaCondition<'a> {
141	Is(MediaFeature),
142	Not(T![Ident], MediaFeature),
143	And(Vec<'a, (MediaFeature, Option<T![Ident]>)>),
144	Or(Vec<'a, (MediaFeature, Option<T![Ident]>)>),
145}
146
147impl<'a> FeatureConditionList<'a> for MediaCondition<'a> {
148	type FeatureCondition = MediaFeature;
149	fn keyword_is_not<I>(p: &Parser<'a, I>, c: Cursor) -> bool
150	where
151		I: Iterator<Item = Cursor> + Clone,
152	{
153		p.equals_atom(c, &CssAtomSet::Not)
154	}
155	fn keyword_is_and<I>(p: &Parser<'a, I>, c: Cursor) -> bool
156	where
157		I: Iterator<Item = Cursor> + Clone,
158	{
159		p.equals_atom(c, &CssAtomSet::And)
160	}
161	fn keyword_is_or<I>(p: &Parser<'a, I>, c: Cursor) -> bool
162	where
163		I: Iterator<Item = Cursor> + Clone,
164	{
165		p.equals_atom(c, &CssAtomSet::Or)
166	}
167	fn build_is(feature: MediaFeature) -> Self {
168		Self::Is(feature)
169	}
170	fn build_not(keyword: T![Ident], feature: MediaFeature) -> Self {
171		Self::Not(keyword, feature)
172	}
173	fn build_and(feature: Vec<'a, (MediaFeature, Option<T![Ident]>)>) -> Self {
174		Self::And(feature)
175	}
176	fn build_or(feature: Vec<'a, (MediaFeature, Option<T![Ident]>)>) -> Self {
177		Self::Or(feature)
178	}
179}
180
181impl<'a> Parse<'a> for MediaCondition<'a> {
182	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
183	where
184		I: Iterator<Item = Cursor> + Clone,
185	{
186		Self::parse_condition(p)
187	}
188}
189
190macro_rules! media_feature {
191	( $($name: ident($typ: ident): $pat: pat,)+) => {
192		// https://drafts.csswg.org/mediaqueries-5/#media-descriptor-table
193		#[derive(ToCursors, ToSpan, SemanticEq, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
194		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
195		#[cfg_attr(feature = "visitable", derive(csskit_derives::Visitable))]
196		pub enum MediaFeature {
197			$($name($typ),)+
198			#[cfg_attr(feature = "visitable", visit(skip))]
199			Hack(HackMediaFeature),
200		}
201	}
202}
203
204apply_medias!(media_feature);
205
206impl<'a> Parse<'a> for MediaFeature {
207	fn parse<I>(p: &mut Parser<'a, I>) -> ParserResult<Self>
208	where
209		I: Iterator<Item = Cursor> + Clone,
210	{
211		let checkpoint = p.checkpoint();
212		let mut c = p.peek_n(2);
213		macro_rules! match_media {
214			( $($name: ident($typ: ident): $pat: pat,)+) => {
215				// Only peek at the token as the underlying media feature parser needs to parse the leading ident.
216				{
217					match p.to_atom::<CssAtomSet>(c) {
218						$($pat => $typ::parse(p).map(Self::$name),)+
219						_ => Err(Diagnostic::new(c, Diagnostic::expected_ident))?
220					}
221				}
222			}
223		}
224		if c == Kind::Ident {
225			let value = apply_medias!(match_media).or_else(|err| {
226				p.rewind(checkpoint);
227				if let Ok(hack) = p.parse::<HackMediaFeature>() { Ok(Self::Hack(hack)) } else { Err(err) }
228			})?;
229			Ok(value)
230		} else {
231			// Styles like (1em < width < 1em) or (1em <= width <= 1em)
232			c = p.peek_n(4);
233			if c != Kind::Ident {
234				c = p.peek_n(5)
235			}
236			if c != Kind::Ident {
237				c = p.next();
238				Err(Diagnostic::new(c, Diagnostic::unexpected))?
239			}
240			apply_medias!(match_media)
241		}
242	}
243}
244
245macro_rules! apply_medias {
246	($macro: ident) => {
247		$macro! {
248			// https://drafts.csswg.org/mediaqueries/#media-descriptor-table
249
250			AnyHover(AnyHoverMediaFeature): CssAtomSet::AnyHover,
251			AnyPointer(AnyPointerMediaFeature): CssAtomSet::AnyPointer,
252			AspectRatio(AspectRatioMediaFeature): CssAtomSet::AspectRatio | CssAtomSet::MinAspectRatio | CssAtomSet::MaxAspectRatio,
253			Color(ColorMediaFeature): CssAtomSet::Color | CssAtomSet::MaxColor | CssAtomSet::MinColor,
254			ColorGamut(ColorGamutMediaFeature): CssAtomSet::ColorGamut,
255			ColorIndex(ColorIndexMediaFeature): CssAtomSet::ColorIndex | CssAtomSet::MaxColorIndex | CssAtomSet::MinColorIndex,
256			DeviceAspectRatio(DeviceAspectRatioMediaFeature): CssAtomSet::DeviceAspectRatio | CssAtomSet::MaxDeviceAspectRatio | CssAtomSet::MinDeviceAspectRatio,
257			DeviceHeight(DeviceHeightMediaFeature): CssAtomSet::DeviceHeight | CssAtomSet::MaxDeviceHeight | CssAtomSet::MinDeviceHeight,
258			DeviceWidth(DeviceWidthMediaFeature): CssAtomSet::DeviceWidth | CssAtomSet::MaxDeviceWidth | CssAtomSet::MinDeviceWidth,
259			DisplayMode(DisplayModeMediaFeature): CssAtomSet::DisplayMode,
260			DynamicRange(DynamicRangeMediaFeature): CssAtomSet::DynamicRange,
261			EnvironmentBlending(EnvironmentBlendingMediaFeature): CssAtomSet::EnvironmentBlending,
262			ForcedColors(ForcedColorsMediaFeature): CssAtomSet::ForcedColors,
263			Grid(GridMediaFeature): CssAtomSet::Grid,
264			Height(HeightMediaFeature): CssAtomSet::Height | CssAtomSet::MaxHeight | CssAtomSet::MinHeight,
265			HorizontalViewportSegments(HorizontalViewportSegmentsMediaFeature): CssAtomSet::HorizontalViewportSegments | CssAtomSet::MaxHorizontalViewportSegments | CssAtomSet::MinHorizontalViewportSegments,
266			Hover(HoverMediaFeature): CssAtomSet::Hover,
267			InvertedColors(InvertedColorsMediaFeature): CssAtomSet::InvertedColors,
268			Monochrome(MonochromeMediaFeature): CssAtomSet::Monochrome | CssAtomSet::MaxMonochrome | CssAtomSet::MinMonochrome,
269			NavControls(NavControlsMediaFeature): CssAtomSet::NavControls,
270			Orientation(OrientationMediaFeature): CssAtomSet::Orientation,
271			OverflowBlock(OverflowBlockMediaFeature): CssAtomSet::OverflowBlock,
272			OverflowInline(OverflowInlineMediaFeature): CssAtomSet::OverflowInline,
273			Pointer(PointerMediaFeature): CssAtomSet::Pointer,
274			PrefersColorScheme(PrefersColorSchemeMediaFeature): CssAtomSet::PrefersColorScheme,
275			PrefersContrast(PrefersContrastMediaFeature): CssAtomSet::PrefersContrast,
276			PrefersReducedData(PrefersReducedDataMediaFeature): CssAtomSet::PrefersReducedData,
277			PrefersReducedMotion(PrefersReducedMotionMediaFeature): CssAtomSet::PrefersReducedMotion,
278			PrefersReducedTransparency(PrefersReducedTransparencyMediaFeature): CssAtomSet::PrefersReducedTransparency,
279			Resolution(ResolutionMediaFeature): CssAtomSet::Resolution | CssAtomSet::MaxResolution | CssAtomSet::MinResolution,
280			Scan(ScanMediaFeature): CssAtomSet::Scan,
281			Scripting(ScriptingMediaFeature): CssAtomSet::Scripting,
282			Update(UpdateMediaFeature): CssAtomSet::Update,
283			VerticalViewportSegments(VerticalViewportSegmentsMediaFeature): CssAtomSet::VerticalViewportSegments | CssAtomSet::MaxVerticalViewportSegments | CssAtomSet::MinVerticalViewportSegments,
284			VideoColorGamut(VideoColorGamutMediaFeature): CssAtomSet::VideoColorGamut,
285			VideoDynamicRange(VideoDynamicRangeMediaFeature): CssAtomSet::VideoDynamicRange,
286			Width(WidthMediaFeature): CssAtomSet::Width | CssAtomSet::MaxWidth | CssAtomSet::MinWidth,
287
288			// https://searchfox.org/wubkat/source/Source/WebCore/css/query/MediaQueryFeatures.cpp#192
289			WebkitAnimationMediaFeature(WebkitAnimationMediaFeature): CssAtomSet::_WebkitAnimation,
290			WebkitDevicePixelRatioMediaFeature(WebkitDevicePixelRatioMediaFeature): CssAtomSet::_WebkitDevicePixelRatio,
291			WebkitTransform2dMediaFeature(WebkitTransform2dMediaFeature): CssAtomSet::_WebkitTransform2d,
292			WebkitTransform3dMediaFeature(WebkitTransform3dMediaFeature): CssAtomSet::_WebkitTransform3d,
293			WebkitTransitionMediaFeature(WebkitTransitionMediaFeature): CssAtomSet::_WebkitTransition,
294			WebkitVideoPlayableInlineMediaFeature(WebkitVideoPlayableInlineMediaFeature): CssAtomSet::_WebkitVideoPlayableInline,
295
296			// https://searchfox.org/mozilla-central/source/servo/components/style/gecko/media_features.rs#744
297			MozDeviceOrientationMediaFeature(MozDeviceOrientationMediaFeature): CssAtomSet::_MozDeviceOrientation,
298			MozDevicePixelRatioMediaFeature(MozDevicePixelRatioMediaFeature): CssAtomSet::_MozDevicePixelRatio | CssAtomSet::_MozMaxDevicePixelRatio | CssAtomSet::_MozMinDevicePixelRatio,
299			MozMacGraphiteThemeMediaFeature(MozMacGraphiteThemeMediaFeature): CssAtomSet::_MozMacGraphiteTheme,
300			MozMaemoClassicMediaFeature(MozMaemoClassicMediaFeature): CssAtomSet::_MozMaemoClassicTheme,
301			MozImagesInMenusMediaFeature(MozImagesInMenusMediaFeature): CssAtomSet::_MozImagesInMenus,
302			MozOsVersionMenusMediaFeature(MozOsVersionMediaFeature): CssAtomSet::_MozOsVersion,
303
304			// https://github.com/search?q=%2F%5C(-ms-%5B%5E)%3A%5D%2B%5B)%3A%5D%2F%20language%3ACSS&type=code
305			MsHighContrastMediaFeature(MsHighContrastMediaFeature): CssAtomSet::_MsHighContrast,
306			MsViewStateMediaFeature(MsViewStateMediaFeature): CssAtomSet::_MsViewState,
307			MsImeAlignMediaFeature(MsImeAlignMediaFeature): CssAtomSet::_MsImeAlign,
308			MsDevicePixelRatioMediaFeature(MsDevicePixelRatioMediaFeature): CssAtomSet::_MsDevicePixelRatio,
309			MsColumnCountMediaFeature(MsColumnCountMediaFeature): CssAtomSet::_MsColumnCount,
310
311			// https://github.com/search?q=%2F%5C(-o-%5B%5E)%3A%5D%2B%5B)%3A%5D%2F%20language%3ACSS&type=code
312			ODevicePixelRatioMediaFeature(ODevicePixelRatioMediaFeature): CssAtomSet::_ODevicePixelRatio,
313		}
314	};
315}
316use apply_medias;
317
318#[cfg(test)]
319mod tests {
320	use super::*;
321	use crate::CssAtomSet;
322	use css_parse::assert_parse;
323
324	#[test]
325	fn size_test() {
326		assert_eq!(std::mem::size_of::<MediaRule>(), 176);
327		assert_eq!(std::mem::size_of::<MediaQueryList>(), 32);
328		assert_eq!(std::mem::size_of::<MediaQuery>(), 192);
329		assert_eq!(std::mem::size_of::<MediaCondition>(), 144);
330	}
331
332	#[test]
333	fn test_writes() {
334		assert_parse!(
335			CssAtomSet::ATOMS,
336			MediaQuery,
337			"print",
338			MediaQuery { precondition: None, media_type: Some(MediaType::Print(_)), and: None, condition: None }
339		);
340		assert_parse!(CssAtomSet::ATOMS, MediaQueryList, "print, tv");
341		assert_parse!(
342			CssAtomSet::ATOMS,
343			MediaQuery,
344			"not embossed",
345			MediaQuery {
346				precondition: Some(MediaPreCondition::Not(_)),
347				media_type: Some(MediaType::Custom(_)),
348				and: None,
349				condition: None
350			}
351		);
352		assert_parse!(
353			CssAtomSet::ATOMS,
354			MediaQuery,
355			"only screen",
356			MediaQuery {
357				precondition: Some(MediaPreCondition::Only(_)),
358				media_type: Some(MediaType::Screen(_)),
359				and: None,
360				condition: None
361			}
362		);
363		assert_parse!(CssAtomSet::ATOMS, MediaFeature, "(grid)", MediaFeature::Grid(_));
364		assert_parse!(
365			CssAtomSet::ATOMS,
366			MediaQuery,
367			"screen and (grid)",
368			MediaQuery {
369				precondition: None,
370				media_type: Some(MediaType::Screen(_)),
371				and: Some(_),
372				condition: Some(MediaCondition::Is(MediaFeature::Grid(_))),
373			}
374		);
375		assert_parse!(
376			CssAtomSet::ATOMS,
377			MediaQuery,
378			"screen and (hover)and (pointer)",
379			MediaQuery {
380				precondition: None,
381				media_type: Some(MediaType::Screen(_)),
382				and: Some(_),
383				condition: Some(MediaCondition::And(_))
384			}
385		);
386		assert_parse!(
387			CssAtomSet::ATOMS,
388			MediaQuery,
389			"screen and (orientation:landscape)",
390			MediaQuery {
391				precondition: None,
392				media_type: Some(MediaType::Screen(_)),
393				and: Some(_),
394				condition: Some(MediaCondition::Is(MediaFeature::Orientation(_))),
395			}
396		);
397		assert_parse!(CssAtomSet::ATOMS, MediaQuery, "(hover)and (pointer)");
398		assert_parse!(CssAtomSet::ATOMS, MediaQuery, "(hover)or (pointer)");
399		// assert_parse!(CssAtomSet::ATOMS, MediaQuery, "not ((width: 2px) or (width: 3px))");
400		// assert_parse!(CssAtomSet::ATOMS, MediaQuery, "not ((hover) or (pointer))");
401		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media print{}");
402		// assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media print,(prefers-reduced-motion: reduce){}");
403		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:1200px){}");
404		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:1200px){body{color:red;}}");
405		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:1200px){@page{}}");
406		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media screen{body{color:black}}");
407		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(max-width:575.98px)and (prefers-reduced-motion:reduce){}");
408		// assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media only screen and(max-device-width:800px),only screen and (device-width:1024px) and (device-height: 600px),only screen and (width:1280px) and (orientation:landscape), only screen and (device-width:800px), only screen and (max-width:767px) {}");
409		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(grid){a{padding:4px}}");
410		assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:0){background:white}");
411		// assert_parse!(
412		// 	MediaRule,
413		// 	"@media(grid){a{color-scheme:light}}",
414		// 	"@media (grid: 0) {\n\ta {\n\t\tcolor-scheme: light;\n\t}\n}"
415		// );
416
417		// IE media hack
418		// assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media (min-width: 0\\0) {\n\n}");
419	}
420
421	// #[test]
422	// fn test_errors() {
423	// 	assert_parse_error!(CssAtomSet::ATOMS, MediaQuery, "(hover) and or (pointer)");
424	// 	assert_parse_error!(CssAtomSet::ATOMS, MediaQuery, "(pointer) or and (pointer)");
425	// 	assert_parse_error!(CssAtomSet::ATOMS, MediaQuery, "(pointer) not and (pointer)");
426	// 	assert_parse_error!(CssAtomSet::ATOMS, MediaQuery, "only and (pointer)");
427	// 	assert_parse_error!(CssAtomSet::ATOMS, MediaQuery, "not and (pointer)");
428	// }
429
430	#[test]
431	#[cfg(feature = "visitable")]
432	fn test_media_rule_visits() {
433		use crate::assert_visits;
434		assert_visits!(
435			"@media (min-width: 768px) { body { color: red; } }",
436			MediaRule,
437			Length,
438			StyleRule,
439			SelectorList,
440			CompoundSelector,
441			Tag,
442			HtmlTag,
443			StyleValue,
444			ColorStyleValue,
445			Color
446		);
447		assert_visits!(
448			"@media screen and (min-width: 768px) { body { color: red; } }",
449			MediaRule,
450			MediaType,
451			Length,
452			StyleRule,
453			SelectorList,
454			CompoundSelector,
455			Tag,
456			HtmlTag,
457			StyleValue,
458			ColorStyleValue,
459			Color
460		);
461		assert_visits!(
462			"@media only screen { body { color: red; } }",
463			MediaRule,
464			MediaPreCondition,
465			MediaType,
466			StyleRule,
467			SelectorList,
468			CompoundSelector,
469			Tag,
470			HtmlTag,
471			StyleValue,
472			ColorStyleValue,
473			Color
474		);
475	}
476}