Skip to main content

media_query_parse/
ast.rs

1//! AST types for the Media Queries Level 4 grammar (`<media-query-list>`
2//! and below), as parsed by [`crate::parser`].
3//!
4//! Pure syntax/structure — see `CLAUDE.md`: this crate has no "matches
5//! a real device/viewport" concept.
6//!
7//! Grammar reference: [Media Queries Level 4][spec] §3, which already
8//! defines the `<mf-range>` alternative (comparison operators, one- and
9//! two-sided ranges) normatively alongside `<mf-plain>`/`<mf-boolean>`
10//! — see `plan/DECISIONS.md` for why this is Level 4, not Level 5.
11//! [`MediaFeature`] covers all three.
12//!
13//! [spec]: https://www.w3.org/TR/mediaqueries-4/
14
15use crate::tokenizer::Token;
16
17/// `<media-query-list> = <media-query>#`
18///
19/// Holds a list of *successfully parsed* queries. The public parsing
20/// entry point ([`crate::parser::parse_media_query_list`]) returns a
21/// `Result` per list entry instead of this type, since one invalid
22/// entry must not fail the whole list (see `plan/DECISIONS.md` for why)
23/// — `MediaQueryList` is here for callers that already have a fully
24/// valid list in hand (e.g. after filtering out the `Err`s themselves).
25#[derive(Debug, Clone, PartialEq)]
26#[non_exhaustive]
27pub struct MediaQueryList(pub Vec<MediaQuery>);
28
29/// `<media-query> = <media-condition>
30///                | [ not | only ]? <media-type> [ and <media-condition-without-or> ]?`
31///
32/// Modeled as an enum rather than a single struct with optional fields:
33/// the two grammar branches allow structurally different condition
34/// kinds (a full `<media-condition>` with `or` only in the first
35/// branch, `<media-condition-without-or>` only in the second), which an
36/// enum expresses type-safely.
37#[derive(Debug, Clone, PartialEq)]
38#[non_exhaustive]
39pub enum MediaQuery {
40    /// The bare `<media-condition>` branch.
41    Condition(MediaCondition),
42    /// The `[ not | only ]? <media-type> [ and <media-condition-without-or> ]?` branch.
43    TypeQuery {
44        /// The optional `not`/`only` prefix, if present.
45        modifier: Option<MediaModifier>,
46        /// The `<media-type>` itself.
47        media_type: MediaType,
48        /// The optional `and <media-condition-without-or>` suffix.
49        condition: Option<MediaConditionWithoutOr>,
50    },
51}
52
53/// `not` | `only`, as used in the `<media-type>` branch of `<media-query>`.
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
55#[non_exhaustive]
56pub enum MediaModifier {
57    /// `not`: negates the whole `<media-query>`.
58    Not,
59    /// `only`: present only to hide the query from legacy UAs that
60    /// don't support media types other than the four originally
61    /// defined ones; has no effect on this crate's parsing result.
62    Only,
63}
64
65/// `<media-type> = <ident>`, structurally excluding `not`/`and`/`or`/
66/// `only`/`layer` (spec §3: "The `<media-type>` production does not
67/// include the keywords `only`, `not`, `and`, `or`, and `layer`").
68#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69#[non_exhaustive]
70pub struct MediaType(pub String);
71
72/// `<media-condition> = <media-not> | <media-in-parens> [ <media-and>* | <media-or>* ]`
73#[derive(Debug, Clone, PartialEq)]
74#[non_exhaustive]
75pub enum MediaCondition {
76    /// `<media-not> = not <media-in-parens>`
77    Not(MediaInParens),
78    /// `<media-in-parens> <media-and>*`. A single element represents
79    /// the bare `<media-in-parens>` case (zero `and`s).
80    And(Vec<MediaInParens>),
81    /// `<media-in-parens> <media-or>*` (at least 2 elements — a bare
82    /// `<media-in-parens>` is always represented as `And` above).
83    Or(Vec<MediaInParens>),
84}
85
86/// `<media-condition-without-or> = <media-not> | <media-in-parens> <media-and>*`
87#[derive(Debug, Clone, PartialEq)]
88#[non_exhaustive]
89pub enum MediaConditionWithoutOr {
90    /// `<media-not> = not <media-in-parens>`
91    Not(MediaInParens),
92    /// `<media-in-parens> <media-and>*`. A single element represents
93    /// the bare `<media-in-parens>` case (zero `and`s).
94    And(Vec<MediaInParens>),
95}
96
97/// `<media-in-parens> = ( <media-condition> ) | ( <media-feature> ) | <general-enclosed>`
98#[derive(Debug, Clone, PartialEq)]
99#[non_exhaustive]
100pub enum MediaInParens {
101    /// `( <media-condition> )`
102    Condition(Box<MediaCondition>),
103    /// `( <media-feature> )`
104    Feature(MediaFeature),
105    /// `<general-enclosed>`, the forward-compatibility fallback (see
106    /// [`GeneralEnclosed`]).
107    GeneralEnclosed(GeneralEnclosed),
108}
109
110/// `<media-feature> = [ <mf-plain> | <mf-boolean> | <mf-range> ]`
111#[derive(Debug, Clone, PartialEq)]
112#[non_exhaustive]
113pub enum MediaFeature {
114    /// `<mf-boolean> = <mf-name>`
115    Boolean(MfName),
116    /// `<mf-plain> = <mf-name> : <mf-value>`
117    Plain {
118        /// The `<mf-name>` (feature name) on the left of `:`.
119        name: MfName,
120        /// The `<mf-value>` on the right of `:`.
121        value: MfValue,
122    },
123    /// `<mf-range>`, see [`MfRange`].
124    Range(MfRange),
125}
126
127/// `<mf-name> = <ident>`
128#[derive(Debug, Clone, PartialEq, Eq, Hash)]
129#[non_exhaustive]
130pub struct MfName(pub String);
131
132/// `<mf-value> = <number> | <dimension> | <ident> | <ratio>`
133#[derive(Debug, Clone, PartialEq)]
134#[non_exhaustive]
135pub enum MfValue {
136    /// `<number>`
137    Number(f64),
138    /// `<dimension>`
139    Dimension {
140        /// The numeric part.
141        value: f64,
142        /// The unit identifier (e.g. `px`).
143        unit: String,
144    },
145    /// `<ident>`
146    Ident(String),
147    /// `<ratio> = <number [0,∞]> <number [0,∞]>`, restricted here to
148    /// non-negative integers, matching typical media-feature usage
149    /// (e.g. `(aspect-ratio: 16/9)`).
150    Ratio {
151        /// The number before `/`.
152        numerator: u32,
153        /// The number after `/`.
154        denominator: u32,
155    },
156}
157
158/// `<mf-range>`, all four grammar alternatives:
159///
160/// ```text
161/// <mf-range> = <mf-name> <mf-comparison> <mf-value>
162///            | <mf-value> <mf-comparison> <mf-name>
163///            | <mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>
164///            | <mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>
165/// ```
166#[derive(Debug, Clone, PartialEq)]
167#[non_exhaustive]
168pub enum MfRange {
169    /// `<mf-name> <mf-comparison> <mf-value>`
170    NameFirst {
171        /// The `<mf-name>` on the left.
172        name: MfName,
173        /// The comparison operator between name and value.
174        operator: MfComparison,
175        /// The `<mf-value>` on the right.
176        value: MfValue,
177    },
178    /// `<mf-value> <mf-comparison> <mf-name>`
179    ValueFirst {
180        /// The `<mf-value>` on the left.
181        value: MfValue,
182        /// The comparison operator between value and name.
183        operator: MfComparison,
184        /// The `<mf-name>` on the right.
185        name: MfName,
186    },
187    /// `<mf-value> <mf-lt> <mf-name> <mf-lt> <mf-value>`
188    ///  | `<mf-value> <mf-gt> <mf-name> <mf-gt> <mf-value>`
189    ///
190    /// One direction (`<mf-lt>` family or `<mf-gt>` family) for both
191    /// operators, never a mixed form — the grammar itself only lists
192    /// these two alternatives, no `<mf-lt> ... <mf-gt>` mix and no
193    /// `<mf-eq>` in this position. Modeled with `direction` plus two
194    /// separate inclusive flags rather than two independent
195    /// `MfComparison` fields, so that a mixed form isn't representable
196    /// in the type at all.
197    Interval {
198        /// The `<mf-value>` bound on the left.
199        lower: MfValue,
200        /// Whether the left operator is inclusive (`<=`/`>=`) rather
201        /// than strict (`<`/`>`).
202        lower_inclusive: bool,
203        /// The `<mf-name>` in the middle.
204        name: MfName,
205        /// Whether the right operator is inclusive (`<=`/`>=`) rather
206        /// than strict (`<`/`>`).
207        upper_inclusive: bool,
208        /// The `<mf-value>` bound on the right.
209        upper: MfValue,
210        /// Which operator family (`<mf-lt>` or `<mf-gt>`) both sides use.
211        direction: MfRangeDirection,
212    },
213}
214
215/// Direction of a two-sided `<mf-range>`: `Ascending` is the `<mf-lt>`
216/// family (`lower < name < upper`), `Descending` is the `<mf-gt>`
217/// family (`lower > name > upper`).
218#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219#[non_exhaustive]
220pub enum MfRangeDirection {
221    /// The `<mf-lt>` family (`lower < name < upper`, or `<=`).
222    Ascending,
223    /// The `<mf-gt>` family (`lower > name > upper`, or `>=`).
224    Descending,
225}
226
227/// `<mf-comparison> = <mf-lt> | <mf-gt> | <mf-eq>`, for the one-sided
228/// `<mf-range>` forms ([`MfRange::NameFirst`]/[`MfRange::ValueFirst`]).
229/// `<mf-lt> = '<' '='?`, `<mf-gt> = '>' '='?`, `<mf-eq> = '='`.
230#[derive(Debug, Clone, Copy, PartialEq, Eq)]
231#[non_exhaustive]
232pub enum MfComparison {
233    /// `<` (strictly less than)
234    Lt,
235    /// `<=` (less than or equal to)
236    Le,
237    /// `>` (strictly greater than)
238    Gt,
239    /// `>=` (greater than or equal to)
240    Ge,
241    /// `=` (equal to)
242    Eq,
243}
244
245/// `<general-enclosed>`: a syntactically well-bracketed but not
246/// further interpretable block, kept verbatim as a forward-
247/// compatibility fallback (spec §3, prose right after the grammar) —
248/// not a parse error. Holds the raw tokens of the block's content: for
249/// the `( <any-value>? )` alternative, the tokens between the
250/// parentheses (parentheses themselves excluded); for the
251/// `<function-token> <any-value>? )` alternative, the function token
252/// followed by its argument tokens (closing `)` excluded). See
253/// `crate::parser`.
254#[derive(Debug, Clone, PartialEq)]
255#[non_exhaustive]
256pub struct GeneralEnclosed {
257    /// The raw tokens of the block's content, see the type-level doc
258    /// comment above for exactly which tokens are included/excluded.
259    pub tokens: Vec<Token>,
260}