Skip to content

Commit 6dd85b5

Browse files
committed
Get more complex media queries parsing
1 parent 72329ad commit 6dd85b5

File tree

1 file changed

+148
-34
lines changed

1 file changed

+148
-34
lines changed

src/media-query.test.ts

Lines changed: 148 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// https://www.w3.org/TR/mediaqueries-5/
22
import {
33
has,
4+
hasMore,
45
mustEnd,
56
optional,
67
parse,
@@ -71,63 +72,176 @@ class ParsedMinWidth {
7172
}
7273
}
7374

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];
7679

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+and\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+
}
80177
}
81178

82179
interface MatchMediaContext {
83180
mediaType: 'screen' | 'print';
84181
viewportWidth: number;
85182
}
86183
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(
90185
mediaQuery,
91-
ParseMediaQuery() as any
186+
ParsedMediaQuery.Parser() as any
92187
);
93188
if (!parsed.success) {
94189
throw Error(`Invalid media query: ${mediaQuery}`);
95190
}
96191

97-
if (parsed.result instanceof ParsedMediaType) {
98-
matches = matches || parsed.result.matches(context);
99-
}
192+
let matches = false;
100193
if (
101194
'matches' in parsed.result &&
102195
typeof parsed.result.matches === 'function'
103196
) {
104-
matches = matches || parsed.result.matches(context);
197+
matches = parsed.result.matches(context);
105198
}
106199

107200
return {
108201
matches,
109202
};
110203
}
111204

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);
114207
expect(result).toEqual({
115208
success: true,
116209
result: new ParsedMinWidth(480, 'px'),
117210
remaining: '',
118211
});
119212
});
120213

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+
121238
test('matchMedia()', () => {
122-
const screen = { mediaType: 'screen' } as const;
239+
const screenSized = (viewportWidth: number, viewportHeight: number) =>
240+
({ mediaType: 'screen', viewportWidth, viewportHeight } as const);
123241
const print = { mediaType: 'print' } as const;
124242

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);
131245

132246
expect(matchMedia({ ...print, viewportWidth: 100 }, 'screen').matches).toBe(
133247
false
@@ -136,16 +250,16 @@ test('matchMedia()', () => {
136250
true
137251
);
138252

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+
);
151265
});

0 commit comments

Comments
 (0)