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#[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 #[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 {
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 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 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 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 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 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 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!(MediaRule, "@media print{}");
367 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(grid){a{padding:4px}}");
375 assert_parse!(MediaRule, "@media(min-width:0){background:white}");
376 }
385
386 }