1
1
// https://www.w3.org/TR/mediaqueries-5/
2
2
import {
3
3
has ,
4
+ hasMore ,
4
5
mustEnd ,
5
6
optional ,
6
7
parse ,
@@ -71,63 +72,176 @@ class ParsedMinWidth {
71
72
}
72
73
}
73
74
74
- function * ParseMediaQuery ( ) {
75
- type Result = ParsedMediaType | ParsedMinWidth ;
75
+ // See https://www.w3.org/TR/mediaqueries-5/#mq-syntax
76
+ type ParsedMediaFeature = ParsedMinWidth ;
77
+ type ParsedMediaInParens = ParsedMediaFeature ;
78
+ const parsedMediaInParens = [ ParsedMinWidth . Parser ] ;
76
79
77
- const result : Result = yield [ ParsedMediaType . Parser , ParsedMinWidth . Parser ] ;
78
- yield mustEnd ;
79
- return result ;
80
+ class ParsedMediaCondition {
81
+ constructor (
82
+ public readonly first : ParsedMediaFeature ,
83
+ public readonly conditions ?: ParsedMediaAnds
84
+ ) { }
85
+
86
+ matches ( context : { mediaType : 'screen' | 'print' ; viewportWidth : number } ) {
87
+ const base = this . first . matches ( context ) ;
88
+ if ( this . conditions ) {
89
+ return base && this . conditions . matches ( context ) ;
90
+ } else {
91
+ return base ;
92
+ }
93
+ }
94
+
95
+ static * Parser ( ) {
96
+ yield optionalWhitespace ;
97
+ const first : ParsedMediaFeature = yield [ ParsedMinWidth . Parser ] ;
98
+ // const conditions: ParsedMediaAnds | undefined = yield optional(ParsedMediaAnds.Parser);
99
+ const conditions : ParsedMediaAnds | '' = yield [ ParsedMediaAnds . Parser , '' ] ;
100
+ if ( conditions === '' ) {
101
+ return first ;
102
+ } else {
103
+ return new ParsedMediaCondition ( first , conditions ) ;
104
+ }
105
+ }
106
+ }
107
+
108
+ class ParsedMediaAnds {
109
+ constructor ( public readonly list : ReadonlyArray < ParsedMediaInParens > ) { }
110
+
111
+ matches ( context : { mediaType : 'screen' | 'print' ; viewportWidth : number } ) {
112
+ return this . list . every ( ( m ) => m . matches ( context ) ) ;
113
+ }
114
+
115
+ static * Parser ( ) {
116
+ const list : Array < ParsedMediaInParens > = [ ] ;
117
+
118
+ do {
119
+ console . log ( 'and requiredWhitespace 1' ) ;
120
+ yield requiredWhitespace ;
121
+ console . log ( 'and requiredWhitespace 2' ) ;
122
+ yield 'and' ;
123
+ yield requiredWhitespace ;
124
+ list . push ( yield parsedMediaInParens ) ;
125
+ } while ( yield hasMore ) ;
126
+
127
+ return new ParsedMediaAnds ( list ) ;
128
+ }
129
+ }
130
+
131
+ class ParsedMediaTypeThenConditionWithoutOr {
132
+ constructor (
133
+ public readonly mediaType : ParsedMediaType ,
134
+ public readonly and : ReadonlyArray < ParsedMediaInParens >
135
+ ) { }
136
+
137
+ matches ( context : { mediaType : 'screen' | 'print' ; viewportWidth : number } ) {
138
+ return (
139
+ this . mediaType . matches ( context ) &&
140
+ this . and . every ( ( m ) => m . matches ( context ) )
141
+ ) ;
142
+ }
143
+
144
+ static * Parser ( ) {
145
+ const mediaType : ParsedMediaType = yield ParsedMediaType . Parser ;
146
+
147
+ const list : Array < ParsedMediaInParens > = [ ] ;
148
+
149
+ while ( yield has ( / ^ \s + a n d \s / ) ) {
150
+ list . push ( yield parsedMediaInParens ) ;
151
+ }
152
+
153
+ if ( list . length === 0 ) {
154
+ return mediaType ;
155
+ } else {
156
+ return new ParsedMediaTypeThenConditionWithoutOr ( mediaType , list ) ;
157
+ }
158
+ }
159
+ }
160
+
161
+ class ParsedMediaQuery {
162
+ constructor (
163
+ public readonly main :
164
+ | ParsedMediaTypeThenConditionWithoutOr
165
+ | ParsedMediaType
166
+ ) { }
167
+
168
+ static * Parser ( ) {
169
+ const main : ParsedMediaQuery [ 'main' ] = yield [
170
+ ParsedMediaTypeThenConditionWithoutOr . Parser ,
171
+ ParsedMediaCondition . Parser ,
172
+ ] ;
173
+ yield optionalWhitespace ;
174
+ yield mustEnd ;
175
+ return main ;
176
+ }
80
177
}
81
178
82
179
interface MatchMediaContext {
83
180
mediaType : 'screen' | 'print' ;
84
181
viewportWidth : number ;
85
182
}
86
183
function matchMedia ( context : MatchMediaContext , mediaQuery : string ) {
87
- let matches = false ;
88
-
89
- const parsed : ParseResult < ParsedType < typeof ParseMediaQuery > > = parse (
184
+ const parsed : ParseResult < ParsedMediaQuery > = parse (
90
185
mediaQuery ,
91
- ParseMediaQuery ( ) as any
186
+ ParsedMediaQuery . Parser ( ) as any
92
187
) ;
93
188
if ( ! parsed . success ) {
94
189
throw Error ( `Invalid media query: ${ mediaQuery } ` ) ;
95
190
}
96
191
97
- if ( parsed . result instanceof ParsedMediaType ) {
98
- matches = matches || parsed . result . matches ( context ) ;
99
- }
192
+ let matches = false ;
100
193
if (
101
194
'matches' in parsed . result &&
102
195
typeof parsed . result . matches === 'function'
103
196
) {
104
- matches = matches || parsed . result . matches ( context ) ;
197
+ matches = parsed . result . matches ( context ) ;
105
198
}
106
199
107
200
return {
108
201
matches,
109
202
} ;
110
203
}
111
204
112
- test ( 'min-width: 480px' , ( ) => {
113
- const result = parse ( '(min-width: 480px)' , ParseMediaQuery ( ) as any ) ;
205
+ test ( '( min-width: 480px) ' , ( ) => {
206
+ const result = parse ( '(min-width: 480px)' , ParsedMediaQuery . Parser ( ) as any ) ;
114
207
expect ( result ) . toEqual ( {
115
208
success : true ,
116
209
result : new ParsedMinWidth ( 480 , 'px' ) ,
117
210
remaining : '' ,
118
211
} ) ;
119
212
} ) ;
120
213
214
+ test ( 'screen' , ( ) => {
215
+ const result = parse ( 'screen' , ParsedMediaQuery . Parser ( ) as any ) ;
216
+ expect ( result ) . toEqual ( {
217
+ success : true ,
218
+ result : new ParsedMediaType ( 'screen' ) ,
219
+ remaining : '' ,
220
+ } ) ;
221
+ } ) ;
222
+
223
+ test ( 'screen and (min-width: 480px)' , ( ) => {
224
+ const result = parse (
225
+ 'screen and (min-width: 480px)' ,
226
+ ParsedMediaQuery . Parser ( ) as any
227
+ ) ;
228
+ expect ( result ) . toEqual ( {
229
+ success : true ,
230
+ result : new ParsedMediaTypeThenConditionWithoutOr (
231
+ new ParsedMediaType ( 'screen' ) ,
232
+ [ new ParsedMinWidth ( 480 , 'px' ) ]
233
+ ) ,
234
+ remaining : '' ,
235
+ } ) ;
236
+ } ) ;
237
+
121
238
test ( 'matchMedia()' , ( ) => {
122
- const screen = { mediaType : 'screen' } as const ;
239
+ const screenSized = ( viewportWidth : number , viewportHeight : number ) =>
240
+ ( { mediaType : 'screen' , viewportWidth, viewportHeight } as const ) ;
123
241
const print = { mediaType : 'print' } as const ;
124
242
125
- expect ( matchMedia ( { ...screen , viewportWidth : 100 } , 'screen' ) . matches ) . toBe (
126
- true
127
- ) ;
128
- expect ( matchMedia ( { ...screen , viewportWidth : 100 } , 'print' ) . matches ) . toBe (
129
- false
130
- ) ;
243
+ expect ( matchMedia ( screenSized ( 100 , 100 ) , 'screen' ) . matches ) . toBe ( true ) ;
244
+ expect ( matchMedia ( screenSized ( 100 , 100 ) , 'print' ) . matches ) . toBe ( false ) ;
131
245
132
246
expect ( matchMedia ( { ...print , viewportWidth : 100 } , 'screen' ) . matches ) . toBe (
133
247
false
@@ -136,16 +250,16 @@ test('matchMedia()', () => {
136
250
true
137
251
) ;
138
252
139
- expect (
140
- matchMedia ( { ... screen , viewportWidth : 478 } , '(min-width: 480px)' ) . matches
141
- ) . toBe ( false ) ;
142
- expect (
143
- matchMedia ( { ... screen , viewportWidth : 479 } , '(min-width: 480px)' ) . matches
144
- ) . toBe ( false ) ;
145
- expect (
146
- matchMedia ( { ... screen , viewportWidth : 480 } , '(min-width: 480px)' ) . matches
147
- ) . toBe ( true ) ;
148
- expect (
149
- matchMedia ( { ... screen , viewportWidth : 481 } , '(min-width: 480px)' ) . matches
150
- ) . toBe ( true ) ;
253
+ expect ( matchMedia ( screenSized ( 478 , 100 ) , '(min-width: 480px)' ) . matches ) . toBe (
254
+ false
255
+ ) ;
256
+ expect ( matchMedia ( screenSized ( 479 , 100 ) , '(min-width: 480px)' ) . matches ) . toBe (
257
+ false
258
+ ) ;
259
+ expect ( matchMedia ( screenSized ( 480 , 100 ) , '(min-width: 480px)' ) . matches ) . toBe (
260
+ true
261
+ ) ;
262
+ expect ( matchMedia ( screenSized ( 481 , 100 ) , '(min-width: 480px)' ) . matches ) . toBe (
263
+ true
264
+ ) ;
151
265
} ) ;
0 commit comments