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