css_ast/rules/media/
mod.rs

1use crate::{Rule, StyleValue, diagnostics};
2use bumpalo::collections::Vec;
3use css_parse::{
4	AtRule, Block, Build, ConditionKeyword, Cursor, FeatureConditionList, Kind, KindSet, Parse, Parser, Peek,
5	PreludeList, Result as ParserResult, T, atkeyword_set, keyword_set,
6};
7use csskit_derives::{IntoCursor, Parse, Peek, ToCursors, ToSpan, Visitable};
8
9mod features;
10use features::*;
11
12atkeyword_set!(pub struct AtMediaKeyword "media");
13
14// https://drafts.csswg.org/mediaqueries-4/
15#[derive(Peek, Parse, ToSpan, ToCursors, Visitable, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
16#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(transparent))]
17#[cfg_attr(feature = "css_feature_data", derive(::csskit_derives::ToCSSFeature), css_feature("css.at-rules.media"))]
18#[visit(self)]
19pub struct MediaRule<'a>(pub AtRule<AtMediaKeyword, MediaQueryList<'a>, MediaRuleBlock<'a>>);
20
21#[derive(Peek, Parse, ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
23pub struct MediaRuleBlock<'a>(pub Block<'a, StyleValue<'a>, Rule<'a>>);
24
25#[derive(Peek, ToSpan, ToCursors, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
27pub struct MediaQueryList<'a>(pub Vec<'a, MediaQuery<'a>>);
28
29impl<'a> PreludeList<'a> for MediaQueryList<'a> {
30	type PreludeItem = MediaQuery<'a>;
31}
32
33impl<'a> Parse<'a> for MediaQueryList<'a> {
34	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
35		Ok(Self(Self::parse_prelude_list(p)?))
36	}
37}
38
39keyword_set!(pub enum MediaPreCondition { Not: "not", Only: "only" });
40
41#[derive(ToCursors, IntoCursor, Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
42#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type"))]
43pub enum MediaType {
44	All(T![Ident]),
45	Print(T![Ident]),
46	Screen(T![Ident]),
47	Custom(T![Ident]),
48}
49
50impl MediaType {
51	const MAP: phf::Map<&'static str, MediaType> = phf::phf_map! {
52			"all" => MediaType::All(<T![Ident]>::dummy()),
53			"print" => MediaType::Print(<T![Ident]>::dummy()),
54			"screen" => MediaType::Screen(<T![Ident]>::dummy()),
55	};
56	const INVALID: phf::Map<&'static str, bool> = phf::phf_map! {
57		"only" => true,
58		"not" => true,
59		"and" => true,
60		"or" => true,
61		"layer" => true,
62	};
63}
64
65impl<'a> Peek<'a> for MediaType {
66	fn peek(p: &Parser<'a>, c: Cursor) -> bool {
67		<T![Ident]>::peek(p, c) && !(*Self::INVALID.get(p.parse_str_lower(c)).unwrap_or(&false))
68	}
69}
70
71impl<'a> Build<'a> for MediaType {
72	fn build(p: &Parser<'a>, c: Cursor) -> Self {
73		let str = &p.parse_str_lower(c);
74		let media_type = Self::MAP.get(str);
75		match media_type {
76			Some(Self::All(_)) => Self::All(<T![Ident]>::build(p, c)),
77			Some(Self::Print(_)) => Self::Print(<T![Ident]>::build(p, c)),
78			Some(Self::Screen(_)) => Self::Screen(<T![Ident]>::build(p, c)),
79			_ if *Self::INVALID.get(str).unwrap_or(&false) => unreachable!(),
80			_ => Self::Custom(<T![Ident]>::build(p, c)),
81		}
82	}
83}
84
85#[derive(ToCursors, ToSpan, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
86#[cfg_attr(feature = "serde", derive(serde::Serialize), serde())]
87pub struct MediaQuery<'a> {
88	precondition: Option<MediaPreCondition>,
89	media_type: Option<MediaType>,
90	and: Option<T![Ident]>,
91	condition: Option<MediaCondition<'a>>,
92}
93
94impl<'a> Peek<'a> for MediaQuery<'a> {
95	const PEEK_KINDSET: KindSet = KindSet::new(&[Kind::Ident, Kind::LeftParen]);
96}
97
98impl<'a> Parse<'a> for MediaQuery<'a> {
99	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
100		let mut precondition = None;
101		let mut media_type = None;
102		let mut and = None;
103		let mut condition = None;
104		if p.peek::<T!['(']>() {
105			condition = Some(p.parse::<MediaCondition<'a>>()?);
106			return Ok(Self { precondition, media_type, and, condition });
107		}
108		let ident = p.parse::<T![Ident]>()?;
109		let c: Cursor = ident.into();
110		if MediaPreCondition::peek(p, c) {
111			precondition = Some(MediaPreCondition::build(p, c));
112		} else if MediaType::peek(p, c) {
113			media_type = Some(MediaType::build(p, c));
114		} else {
115			let source_cursor = p.to_source_cursor(c);
116			Err(diagnostics::UnexpectedIdent(source_cursor.to_string(), c))?
117		}
118		if p.peek::<T![Ident]>() && precondition.is_some() {
119			let ident = p.parse::<T![Ident]>()?;
120			let c: Cursor = ident.into();
121			if MediaType::peek(p, c) {
122				media_type = Some(MediaType::build(p, c));
123			} else {
124				let source_cursor = p.to_source_cursor(c);
125				Err(diagnostics::UnexpectedIdent(source_cursor.to_string(), c))?
126			}
127		}
128		let c = p.peek_n(1);
129		if c == Kind::Ident && p.eq_ignore_ascii_case(c, "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, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type", content = "value"))]
139pub enum MediaCondition<'a> {
140	Is(MediaFeature),
141	Not(ConditionKeyword, MediaFeature),
142	And(Vec<'a, (MediaFeature, Option<ConditionKeyword>)>),
143	Or(Vec<'a, (MediaFeature, Option<ConditionKeyword>)>),
144}
145
146impl<'a> FeatureConditionList<'a> for MediaCondition<'a> {
147	type FeatureCondition = MediaFeature;
148	fn build_is(feature: MediaFeature) -> Self {
149		Self::Is(feature)
150	}
151	fn build_not(keyword: ConditionKeyword, feature: MediaFeature) -> Self {
152		Self::Not(keyword, feature)
153	}
154	fn build_and(feature: Vec<'a, (MediaFeature, Option<ConditionKeyword>)>) -> Self {
155		Self::And(feature)
156	}
157	fn build_or(feature: Vec<'a, (MediaFeature, Option<ConditionKeyword>)>) -> Self {
158		Self::Or(feature)
159	}
160}
161
162impl<'a> Parse<'a> for MediaCondition<'a> {
163	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
164		Self::parse_condition(p)
165	}
166}
167
168macro_rules! media_feature {
169	( $($name: ident($typ: ident): $pat: pat,)+) => {
170		// https://drafts.csswg.org/mediaqueries-5/#media-descriptor-table
171		#[derive(ToCursors, ToSpan, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
172		#[cfg_attr(feature = "serde", derive(serde::Serialize), serde(tag = "type"))]
173		pub enum MediaFeature {
174			$($name($typ),)+
175			Hack(HackMediaFeature),
176		}
177	}
178}
179
180apply_medias!(media_feature);
181
182impl<'a> Parse<'a> for MediaFeature {
183	fn parse(p: &mut Parser<'a>) -> ParserResult<Self> {
184		let checkpoint = p.checkpoint();
185		let mut c = p.peek_n(2);
186		macro_rules! match_media {
187			( $($name: ident($typ: ident): $pat: pat,)+) => {
188				// Only peek at the token as the underlying media feature parser needs to parse the leading ident.
189				{
190					match p.parse_str_lower(c) {
191						$($pat => $typ::parse(p).map(Self::$name),)+
192						str => Err(diagnostics::UnexpectedIdent(str.into(), c))?,
193					}
194				}
195			}
196		}
197		if c == Kind::Ident {
198			let value = apply_medias!(match_media).or_else(|err| {
199				p.rewind(checkpoint);
200				if let Ok(hack) = p.parse::<HackMediaFeature>() { Ok(Self::Hack(hack)) } else { Err(err) }
201			})?;
202			Ok(value)
203		} else {
204			// Styles like (1em < width < 1em) or (1em <= width <= 1em)
205			c = p.peek_n(4);
206			if c != Kind::Ident {
207				c = p.peek_n(5)
208			}
209			if c != Kind::Ident {
210				c = p.parse::<T![Any]>()?.into();
211				Err(diagnostics::Unexpected(c))?
212			}
213			apply_medias!(match_media)
214		}
215	}
216}
217
218macro_rules! apply_medias {
219	($macro: ident) => {
220		$macro! {
221			// https://drafts.csswg.org/mediaqueries/#media-descriptor-table
222
223			AnyHover(AnyHoverMediaFeature): "any-hover",
224			AnyPointer(AnyPointerMediaFeature): "any-pointer",
225			AspectRatio(AspectRatioMediaFeature): "aspect-ratio" | "max-aspect-ratio" | "min-aspect-ratio",
226			Color(ColorMediaFeature): "color" | "max-color" | "min-color",
227			ColorGamut(ColorGamutMediaFeature): "color-gamut",
228			ColorIndex(ColorIndexMediaFeature): "color-index" | "max-color-index" | "min-color-index",
229			DeviceAspectRatio(DeviceAspectRatioMediaFeature): "device-aspect-ratio" | "max-device-aspect-ratio" | "min-device-aspect-ratio",
230			DeviceHeight(DeviceHeightMediaFeature): "device-height" | "max-device-height" | "min-device-height",
231			DeviceWidth(DeviceWidthMediaFeature): "device-width" | "max-device-width" | "min-device-width",
232			DisplayMode(DisplayModeMediaFeature): "display-mode",
233			DynamicRange(DynamicRangeMediaFeature): "dynamic-range",
234			EnvironmentBlending(EnvironmentBlendingMediaFeature): "environment-blending",
235			ForcedColors(ForcedColorsMediaFeature): "forced-colors",
236			Grid(GridMediaFeature): "grid",
237			Height(HeightMediaFeature): "height" | "max-height" | "min-height",
238			HorizontalViewportSegments(HorizontalViewportSegmentsMediaFeature): "horizontal-viewport-segments" | "max-horizontal-viewport-segments" | "min-horizontal-viewport-segments",
239			Hover(HoverMediaFeature): "hover",
240			InvertedColors(InvertedColorsMediaFeature): "inverted-colors",
241			Monochrome(MonochromeMediaFeature): "monochrome" | "max-monochrome" | "min-monochrome",
242			NavControls(NavControlsMediaFeature): "nav-controls",
243			Orientation(OrientationMediaFeature): "orientation",
244			OverflowBlock(OverflowBlockMediaFeature): "overflow-block",
245			OverflowInline(OverflowInlineMediaFeature): "overflow-inline",
246			Pointer(PointerMediaFeature): "pointer",
247			PrefersColorScheme(PrefersColorSchemeMediaFeature): "prefers-color-scheme",
248			PrefersContrast(PrefersContrastMediaFeature): "prefers-contrast",
249			PrefersReducedData(PrefersReducedDataMediaFeature): "prefers-reduced-data",
250			PrefersReducedMotion(PrefersReducedMotionMediaFeature): "prefers-reduced-motion",
251			PrefersReducedTransparency(PrefersReducedTransparencyMediaFeature): "prefers-reduced-transparency",
252			Resolution(ResolutionMediaFeature): "resolution" | "max-resolution" | "min-resolution",
253			Scan(ScanMediaFeature): "scan",
254			Scripting(ScriptingMediaFeature): "scripting",
255			Update(UpdateMediaFeature): "update",
256			VerticalViewportSegments(VerticalViewportSegmentsMediaFeature): "vertical-viewport-segments" | "max-vertical-viewport-segments" | "min-vertical-viewport-segments",
257			VideoColorGamut(VideoColorGamutMediaFeature): "video-color-gamut",
258			VideoDynamicRange(VideoDynamicRangeMediaFeature): "video-dynamic-range",
259			Width(WidthMediaFeature): "width" | "max-width" | "min-width",
260
261			// https://searchfox.org/wubkat/source/Source/WebCore/css/query/MediaQueryFeatures.cpp#192
262			WebkitAnimationMediaFeature(WebkitAnimationMediaFeature): "-webkit-animation",
263			WebkitDevicePixelRatioMediaFeature(WebkitDevicePixelRatioMediaFeature): "-webkit-device-pixel-ratio",
264			WebkitTransform2dMediaFeature(WebkitTransform2dMediaFeature): "-webkit-transform-2d",
265			WebkitTransform3dMediaFeature(WebkitTransform3dMediaFeature): "-webkit-transform-3d",
266			WebkitTransitionMediaFeature(WebkitTransitionMediaFeature): "-webkit-transition",
267			WebkitVideoPlayableInlineMediaFeature(WebkitVideoPlayableInlineMediaFeature): "-webkit-video-playable-inline",
268
269			// https://searchfox.org/mozilla-central/source/servo/components/style/gecko/media_features.rs#744
270			MozDeviceOrientationMediaFeature(MozDeviceOrientationMediaFeature): "-moz-device-orientation",
271			MozDevicePixelRatioMediaFeature(MozDevicePixelRatioMediaFeature): "-moz-device-pixel-ratio" | "max--moz-device-pixel-ratio" | "min--moz-device-pixel-ratio",
272			MozMacGraphiteThemeMediaFeature(MozDevicePixelRatioMediaFeature): "-moz-mac-graphite-theme",
273			MozMaemoClassicMediaFeature(MozDevicePixelRatioMediaFeature): "-moz-maemo-classic",
274			MozImagesInMenusMediaFeature(MozDevicePixelRatioMediaFeature): "-moz-images-in-menus",
275			MozOsVersionMenusMediaFeature(MozDevicePixelRatioMediaFeature): "-moz-os-version",
276
277			// https://github.com/search?q=%2F%5C(-ms-%5B%5E)%3A%5D%2B%5B)%3A%5D%2F%20language%3ACSS&type=code
278			MsHighContrastMediaFeature(MsHighContrastMediaFeature): "-ms-high-contrast",
279			MsViewStateMediaFeature(MsViewStateMediaFeature): "-ms-view-state",
280			MsImeAlignMediaFeature(MsImeAlignMediaFeature): "-ms-ime-align",
281			MsDevicePixelRatioMediaFeature(MsDevicePixelRatioMediaFeature): "-ms-device-pixel-ratio",
282			MsColumnCountMediaFeature(MsColumnCountMediaFeature): "-ms-column-count",
283
284			// https://github.com/search?q=%2F%5C(-o-%5B%5E)%3A%5D%2B%5B)%3A%5D%2F%20language%3ACSS&type=code
285			ODevicePixelRatioMediaFeature(ODevicePixelRatioMediaFeature): "-o-device-pixel-ratio",
286		}
287	};
288}
289use apply_medias;
290
291#[cfg(test)]
292mod tests {
293	use super::*;
294	use css_parse::assert_parse;
295
296	#[test]
297	fn size_test() {
298		assert_eq!(std::mem::size_of::<MediaRule>(), 160);
299		assert_eq!(std::mem::size_of::<MediaQueryList>(), 32);
300		assert_eq!(std::mem::size_of::<MediaQuery>(), 200);
301		assert_eq!(std::mem::size_of::<MediaCondition>(), 152);
302	}
303
304	#[test]
305	fn test_writes() {
306		assert_parse!(
307			MediaQuery,
308			"print",
309			MediaQuery { precondition: None, media_type: Some(MediaType::Print(_)), and: None, condition: None }
310		);
311		assert_parse!(
312			MediaQuery,
313			"not embossed",
314			MediaQuery {
315				precondition: Some(MediaPreCondition::Not(_)),
316				media_type: Some(MediaType::Custom(_)),
317				and: None,
318				condition: None
319			}
320		);
321		assert_parse!(
322			MediaQuery,
323			"only screen",
324			MediaQuery {
325				precondition: Some(MediaPreCondition::Only(_)),
326				media_type: Some(MediaType::Screen(_)),
327				and: None,
328				condition: None
329			}
330		);
331		assert_parse!(MediaFeature, "(grid)", MediaFeature::Grid(_));
332		assert_parse!(
333			MediaQuery,
334			"screen and (grid)",
335			MediaQuery {
336				precondition: None,
337				media_type: Some(MediaType::Screen(_)),
338				and: Some(_),
339				condition: Some(MediaCondition::Is(MediaFeature::Grid(_))),
340			}
341		);
342		assert_parse!(
343			MediaQuery,
344			"screen and (hover)and (pointer)",
345			MediaQuery {
346				precondition: None,
347				media_type: Some(MediaType::Screen(_)),
348				and: Some(_),
349				condition: Some(MediaCondition::And(_))
350			}
351		);
352		assert_parse!(
353			MediaQuery,
354			"screen and (orientation:landscape)",
355			MediaQuery {
356				precondition: None,
357				media_type: Some(MediaType::Screen(_)),
358				and: Some(_),
359				condition: Some(MediaCondition::Is(MediaFeature::Orientation(_))),
360			}
361		);
362		assert_parse!(MediaQuery, "(hover)and (pointer)");
363		assert_parse!(MediaQuery, "(hover)or (pointer)");
364		// assert_parse!(MediaQuery, "not ((width: 2px) or (width: 3px))");
365		// assert_parse!(MediaQuery, "not ((hover) or (pointer))");
366		assert_parse!(MediaRule, "@media print{}");
367		// assert_parse!(MediaRule, "@media print,(prefers-reduced-motion: reduce){}");
368		assert_parse!(MediaRule, "@media(min-width:1200px){}");
369		assert_parse!(MediaRule, "@media(min-width:1200px){body{color:red;}}");
370		assert_parse!(MediaRule, "@media(min-width:1200px){@page{}}");
371		assert_parse!(MediaRule, "@media screen{body{color:black}}");
372		assert_parse!(MediaRule, "@media(max-width:575.98px)and (prefers-reduced-motion:reduce){}");
373		// assert_parse!(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) {}");
374		assert_parse!(MediaRule, "@media(grid){a{padding:4px}}");
375		assert_parse!(MediaRule, "@media(min-width:0){background:white}");
376		// assert_parse!(
377		// 	MediaRule,
378		// 	"@media(grid){a{color-scheme:light}}",
379		// 	"@media (grid: 0) {\n\ta {\n\t\tcolor-scheme: light;\n\t}\n}"
380		// );
381
382		// IE media hack
383		// assert_parse!(MediaRule, "@media (min-width: 0\\0) {\n\n}");
384	}
385
386	// #[test]
387	// fn test_errors() {
388	// 	assert_parse_error!(MediaQuery, "(hover) and or (pointer)");
389	// 	assert_parse_error!(MediaQuery, "(pointer) or and (pointer)");
390	// 	assert_parse_error!(MediaQuery, "(pointer) not and (pointer)");
391	// 	assert_parse_error!(MediaQuery, "only and (pointer)");
392	// 	assert_parse_error!(MediaQuery, "not and (pointer)");
393	// }
394}