1use super::prelude::*;
2use crate::{AtRuleId, NodeKinds};
3
4mod features;
5pub use features::*;
6
7#[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 #[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 {
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 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 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 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 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 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 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, MediaRule, "@media print{}");
408 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(grid){a{padding:4px}}");
416 assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:0){background:white}");
417 }
426
427 #[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}