Skip to main content

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