Skip to content

Commit d787a23

Browse files
committed
fix: preserve whitespace between media keywords and opening parenthesis
CSS media query keywords (and, or, not, only) must be separated from adjacent parentheses by whitespace. Without a space, `and(condition)` is parsed as a function call rather than the keyword followed by a media condition, making the at-rule invalid. Add a replaceAll pass to insert the required space whenever one of these keywords is directly followed by `(`, and expand test coverage to include `@media all and screen {}` and related keyword combinator patterns. https://claude.ai/code/session_01FnNEbE8DRPAFamgWRW4B7L
1 parent fcd6855 commit d787a23

3 files changed

Lines changed: 57 additions & 1 deletion

File tree

src/lib/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,7 @@ export function format_atrule_prelude(
303303
return prelude
304304
.replaceAll(/\s*([:,])/g, prelude.toLowerCase().includes('selector(') ? '$1' : '$1 ') // force whitespace after colon or comma, except inside `selector()`
305305
.replaceAll(/\)([a-zA-Z])/g, ') $1') // force whitespace between closing parenthesis and following text (usually and|or)
306+
.replaceAll(/\b(and|or|not|only)\(/gi, '$1 (') // force whitespace between media/supports keywords and opening parenthesis
306307
.replaceAll(/\s*(=>|>=|<=)\s*/g, `${optional_space}$1${optional_space}`) // add optional spacing around =>, >= and <=
307308
.replaceAll(/([^<>=\s])([<>])([^<>=\s])/g, `$1${optional_space}$2${optional_space}$3`) // add spacing around < or > except when it's part of <=, >=, =>
308309
.replaceAll(/([^<>=\s])\s+([<>])\s+([^<>=\s])/g, `$1${optional_space}$2${optional_space}$3`) // handle spaces around < or > when they already have surrounding whitespace

test/api.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,20 @@ describe('format_atrule_prelude', () => {
103103
})
104104

105105
test('adds space between ) and following word', () => {
106-
expect(format_atrule_prelude('(width > 0)and(height > 0)')).toBe('(width > 0) and(height > 0)')
106+
expect(format_atrule_prelude('(width > 0)and(height > 0)')).toBe('(width > 0) and (height > 0)')
107+
})
108+
109+
test('adds space between media keyword "and" and opening parenthesis', () => {
110+
expect(format_atrule_prelude('(width > 0)and(height > 0)', { minify: true })).toBe('(width>0) and (height>0)')
111+
})
112+
113+
test('adds space between media keyword "not" and opening parenthesis', () => {
114+
expect(format_atrule_prelude('not(color)')).toBe('not (color)')
115+
expect(format_atrule_prelude('not(color)', { minify: true })).toBe('not (color)')
116+
})
117+
118+
test('adds space between media keyword "or" and opening parenthesis', () => {
119+
expect(format_atrule_prelude('(width > 0)or(height > 0)')).toBe('(width > 0) or (height > 0)')
107120
})
108121

109122
test('lowercases function names', () => {

test/atrules.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -373,6 +373,48 @@ test('minify: keeps necessary whitespace between keywords', () => {
373373
expect(actual).toEqual(expected)
374374
})
375375

376+
test('minify: keeps necessary whitespace in @media all and screen', () => {
377+
let actual = minify(`@media all and screen {}`)
378+
let expected = `@media all and screen{}`
379+
expect(actual).toEqual(expected)
380+
})
381+
382+
test('minify: keeps whitespace between "and" keyword and following media feature', () => {
383+
let actual = minify(`@media screen and (min-width: 100px) {}`)
384+
let expected = `@media screen and (min-width:100px){}`
385+
expect(actual).toEqual(expected)
386+
})
387+
388+
test('minify: keeps whitespace between "and" keywords with adjacent media features', () => {
389+
let actual = minify(`@media (min-width: 100px) and (max-width: 200px) {}`)
390+
let expected = `@media (min-width:100px) and (max-width:200px){}`
391+
expect(actual).toEqual(expected)
392+
})
393+
394+
test('minify: keeps whitespace between "not" keyword and media feature', () => {
395+
let actual = minify(`@media not (color) {}`)
396+
let expected = `@media not (color){}`
397+
expect(actual).toEqual(expected)
398+
})
399+
400+
test('minify: keeps whitespace between "not" keyword and media type', () => {
401+
let actual = minify(`@media not screen {}`)
402+
let expected = `@media not screen{}`
403+
expect(actual).toEqual(expected)
404+
})
405+
406+
test('minify: keeps whitespace between "only" keyword and media type', () => {
407+
let actual = minify(`@media only screen {}`)
408+
let expected = `@media only screen{}`
409+
expect(actual).toEqual(expected)
410+
})
411+
412+
test('minify: keeps whitespace between "or" keyword and media feature', () => {
413+
let actual = minify(`@media (min-width: 100px) or (max-width: 200px) {}`)
414+
let expected = `@media (min-width:100px) or (max-width:200px){}`
415+
expect(actual).toEqual(expected)
416+
})
417+
376418
// oxlint-disable-next-line vitest/no-disabled-tests
377419
test.skip('preserves comments', () => {
378420
let actual = format(`

0 commit comments

Comments
 (0)