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(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 #[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 {
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 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 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 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 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 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 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, MediaRule, "@media print{}");
402 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(grid){a{padding:4px}}");
410 assert_parse!(CssAtomSet::ATOMS, MediaRule, "@media(min-width:0){background:white}");
411 }
420
421 #[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}