⚠ This page is served via a proxy. Original site: https://github.com
This service does not collect credentials or authentication data.
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/parse-anplusb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { Lexer } from './tokenize'
import { NTH_SELECTOR, CSSDataArena } from './arena'
import { TOKEN_IDENT, TOKEN_NUMBER, TOKEN_DIMENSION, TOKEN_DELIM, type TokenType } from './token-types'
import { CHAR_MINUS_HYPHEN, CHAR_PLUS, str_equals, str_index_of } from './string-utils'
import { skip_whitespace_forward } from './parse-utils'
import { skip_whitespace_and_comments_forward } from './parse-utils'
import { CSSNode } from './css-node'

/** @internal */
Expand Down Expand Up @@ -262,7 +262,7 @@ export class ANplusBParser {
}

private skip_whitespace(): void {
this.lexer.pos = skip_whitespace_forward(this.source, this.lexer.pos, this.expr_end)
this.lexer.pos = skip_whitespace_and_comments_forward(this.source, this.lexer.pos, this.expr_end)
}

private create_anplusb_node(start: number, a_start: number, a_end: number, b_start: number, b_end: number): number {
Expand Down
144 changes: 144 additions & 0 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1718,3 +1718,147 @@ describe('Case-insensitive at-rule keywords', () => {
expect(atrule?.children.length).toBeGreaterThan(0)
})
})

describe('Comment Handling in At-Rule Preludes', () => {
describe('@media queries with comments', () => {
it('should parse media query with comment before screen', () => {
const root = parse('@media /* comment */ screen { }')
const atrule = root.first_child
expect(atrule?.name).toBe('media')
const mediaQuery = atrule?.prelude?.first_child
expect(mediaQuery?.type).toBe(MEDIA_QUERY)
// Should find the media type
const mediaType = mediaQuery?.first_child
expect(mediaType?.type).toBe(MEDIA_TYPE)
expect(mediaType?.text).toBe('screen')
})

it('should parse media query with comment in media feature', () => {
const root = parse('@media (/* comment */ min-width: 768px) { }')
const atrule = root.first_child
expect(atrule?.name).toBe('media')
const mediaQuery = atrule?.prelude?.first_child
expect(mediaQuery?.type).toBe(MEDIA_QUERY)
const mediaFeature = mediaQuery?.first_child
expect(mediaFeature?.type).toBe(MEDIA_FEATURE)
expect(mediaFeature?.property).toBe('min-width')
})

it('should parse media feature with comment around colon', () => {
const root = parse('@media (min-width /* comment */ : /* comment */ 768px) { }')
const atrule = root.first_child
const mediaFeature = atrule?.prelude?.first_child?.first_child
expect(mediaFeature?.type).toBe(MEDIA_FEATURE)
expect(mediaFeature?.property).toBe('min-width')
})

it('should parse media query list with comments between queries', () => {
const root = parse('@media screen /* comment */ , /* comment */ print { }')
const atrule = root.first_child
expect(atrule?.name).toBe('media')
const prelude = atrule?.prelude
expect(prelude?.children.length).toBe(2)
})

it('should parse media feature range with comments around operators', () => {
const root = parse('@media (/* comment */ 400px /* comment */ <= /* comment */ width) { }')
const atrule = root.first_child
const mediaQuery = atrule?.prelude?.first_child
const featureRange = mediaQuery?.first_child
expect(featureRange?.type).toBe(FEATURE_RANGE)
})

it('should not match operators inside comments in media features', () => {
const root = parse('@media (/* < */ width: 400px) { }')
const atrule = root.first_child
const mediaFeature = atrule?.prelude?.first_child?.first_child
expect(mediaFeature?.type).toBe(MEDIA_FEATURE) // Should be MEDIA_FEATURE, not FEATURE_RANGE
expect(mediaFeature?.property).toBe('width')
})
})

describe('@container queries with comments', () => {
it('should parse container query with comment before feature', () => {
const root = parse('@container /* comment */ (min-width: 400px) { }')
const atrule = root.first_child
expect(atrule?.name).toBe('container')
const containerQuery = atrule?.prelude?.first_child
expect(containerQuery?.type).toBe(CONTAINER_QUERY)
})
})

describe('@supports queries with comments', () => {
it('should parse supports query with comment in feature', () => {
const root = parse('@supports (/* comment */ display: grid) { }')
const atrule = root.first_child
expect(atrule?.name).toBe('supports')
const supportsQuery = atrule?.prelude?.first_child
expect(supportsQuery?.type).toBe(SUPPORTS_QUERY)
})

it('should parse supports with comments between queries', () => {
const root = parse('@supports (display: grid) /* comment */ or /* comment */ (display: flex) { }')
const atrule = root.first_child
expect(atrule?.name).toBe('supports')
expect(atrule?.prelude?.children.length).toBeGreaterThan(0)
})
})

describe('@layer with comments', () => {
it('should parse layer names with comments between them', () => {
const root = parse('@layer foo /* comment */ , /* comment */ bar { }')
const atrule = root.first_child
expect(atrule?.name).toBe('layer')
const prelude = atrule?.prelude
expect(prelude?.children.length).toBe(2)
const [layer1, layer2] = prelude?.children || []
expect(layer1?.type).toBe(LAYER_NAME)
expect(layer1?.value).toBe('foo')
expect(layer2?.type).toBe(LAYER_NAME)
expect(layer2?.value).toBe('bar')
})
})

describe('@import with comments', () => {
it('should parse import with comment before URL', () => {
const root = parse('@import /* comment */ "styles.css";')
const atrule = root.first_child
expect(atrule?.name).toBe('import')
expect(atrule?.prelude?.children.length).toBeGreaterThan(0)
})

it('should parse import with comment before layer', () => {
const root = parse('@import "styles.css" /* comment */ layer(base);')
const atrule = root.first_child
expect(atrule?.name).toBe('import')
expect(atrule?.prelude?.children.length).toBeGreaterThan(0)
})
})

describe('@keyframes with comments', () => {
it('should parse keyframes name with comment before it', () => {
const root = parse('@keyframes /* comment */ slidein { }')
const atrule = root.first_child
expect(atrule?.name).toBe('keyframes')
const identifier = atrule?.prelude?.first_child
expect(identifier?.type).toBe(IDENTIFIER)
expect(identifier?.text).toBe('slidein')
})
})

describe('Multiline comments', () => {
it('should handle multiline comments in @media queries', () => {
const root = parse(`@media screen
/* comment
with
newlines */
and (min-width: 768px) { }`)
const atrule = root.first_child
expect(atrule?.name).toBe('media')
const mediaQuery = atrule?.prelude?.first_child
expect(mediaQuery?.type).toBe(MEDIA_QUERY)
// Should still parse the media feature after the multiline comment
expect(mediaQuery?.children.length).toBeGreaterThan(0)
})
})
})
28 changes: 20 additions & 8 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ import {
type TokenType,
} from './token-types'
import { str_equals, is_whitespace, strip_vendor_prefix, CHAR_COLON, CHAR_LESS_THAN, CHAR_GREATER_THAN, CHAR_EQUALS } from './string-utils'
import { trim_boundaries, skip_whitespace_forward } from './parse-utils'
import { trim_boundaries, skip_whitespace_and_comments_forward } from './parse-utils'
import { CSSNode } from './css-node'

/** @internal */
Expand Down Expand Up @@ -224,12 +224,18 @@ export class AtRulePreludeParser {

// Check for range syntax (has comparison operators)
let has_comparison = false
for (let i = content_start; i < content_end; i++) {
let i = content_start
while (i < content_end) {
// Skip whitespace and comments
i = skip_whitespace_and_comments_forward(this.source, i, content_end)
if (i >= content_end) break

let ch = this.source.charCodeAt(i)
if (ch === CHAR_LESS_THAN || ch === CHAR_GREATER_THAN || ch === CHAR_EQUALS) {
has_comparison = true
break
}
i++
}

if (has_comparison) {
Expand All @@ -241,11 +247,17 @@ export class AtRulePreludeParser {

// Find colon to separate name from value
let colon_pos = -1
for (let i = content_start; i < content_end; i++) {
if (this.source.charCodeAt(i) === CHAR_COLON) {
colon_pos = i
let j = content_start
while (j < content_end) {
// Skip whitespace and comments
j = skip_whitespace_and_comments_forward(this.source, j, content_end)
if (j >= content_end) break

if (this.source.charCodeAt(j) === CHAR_COLON) {
colon_pos = j
break
}
j++
}

if (colon_pos !== -1) {
Expand Down Expand Up @@ -686,9 +698,9 @@ export class AtRulePreludeParser {
return null
}

// Helper: Skip whitespace
// Helper: Skip whitespace and comments
private skip_whitespace(): void {
this.lexer.pos = skip_whitespace_forward(this.source, this.lexer.pos, this.prelude_end)
this.lexer.pos = skip_whitespace_and_comments_forward(this.source, this.lexer.pos, this.prelude_end)
}

// Helper: Peek at next token type without consuming
Expand Down Expand Up @@ -774,7 +786,7 @@ export class AtRulePreludeParser {
let pos = content_start

while (pos < content_end) {
pos = skip_whitespace_forward(this.source, pos, content_end)
pos = skip_whitespace_and_comments_forward(this.source, pos, content_end)
if (pos >= content_end) break

let ch = this.source.charCodeAt(pos)
Expand Down
Loading