css_ast/rules/media/
mod.rs

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