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
63 changes: 62 additions & 1 deletion src/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
ATTRIBUTE_SELECTOR,
NESTING_SELECTOR,
URL,
RAW,
AT_RULE_PRELUDE,
} from './constants'
import { ATTR_OPERATOR_PIPE_EQUAL } from './arena'

Expand Down Expand Up @@ -1109,11 +1111,70 @@ describe('Core Nodes', () => {
let atrule = root.first_child!
expect(atrule.name).toBe('imaginary-atrule')
expect(atrule.value).toBe('prelude-stuff')
// Unknown at-rules don't have structured prelude parsing, but prelude wrapper exists
// Unknown at-rules get a RAW prelude (not AT_RULE_PRELUDE)
expect(atrule.prelude).not.toBeNull()
expect(atrule.prelude?.type).toBe(RAW)
expect(atrule.prelude?.text).toBe('prelude-stuff')
})

test('unknown at-rule without block has RAW prelude and no block', () => {
let source = '@custom prelude;'
let root = parse(source)

let atrule = root.first_child!
expect(atrule.name).toBe('custom')
expect(atrule.prelude?.type).toBe(RAW)
expect(atrule.prelude?.text).toBe('prelude')
expect(atrule.block).toBeNull()
})

test('unknown at-rule block can contain declarations', () => {
let source = '@custom { color: red }'
let root = parse(source)

let atrule = root.first_child!
expect(atrule.name).toBe('custom')
let block = atrule.block!
expect(block).not.toBeNull()
let declaration = block.first_child!
expect(declaration.type).toBe(DECLARATION)
expect(declaration.property).toBe('color')
})

test('unknown at-rule block can contain style rules', () => {
let source = '@custom { .a { color: red } }'
let root = parse(source)

let atrule = root.first_child!
let block = atrule.block!
let rule = block.first_child!
expect(rule.type).toBe(STYLE_RULE)
})

test('unknown at-rule block can contain nested at-rules', () => {
let source = '@custom { @media (width) { } }'
let root = parse(source)

let atrule = root.first_child!
let block = atrule.block!
let nested = block.first_child!
expect(nested.type).toBe(AT_RULE)
expect(nested.name).toBe('media')
})

test('known at-rule @keyframes still has AT_RULE_PRELUDE and correctly parsed frames', () => {
let source = '@keyframes foo { from { opacity: 0 } to { opacity: 1 } }'
let root = parse(source)

let atrule = root.first_child!
expect(atrule.name).toBe('keyframes')
expect(atrule.prelude?.type).toBe(AT_RULE_PRELUDE)
expect(atrule.prelude?.text).toBe('foo')
let block = atrule.block!
let from_rule = block.first_child!
expect(from_rule.type).toBe(STYLE_RULE)
})

test('value string matches prelude text for at-rules', () => {
let source = '@media (min-width: 768px) { }'
let root = parse(source)
Expand Down
56 changes: 17 additions & 39 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,16 +61,6 @@ let DECLARATION_AT_RULES = new Set([
'position-try',
'view-transition',
])
let CONDITIONAL_AT_RULES = new Set([
'media',
'supports',
'container',
'layer',
'nest',
'scope',
'starting-style',
'function',
])

/** @internal */
export class Parser {
Expand Down Expand Up @@ -424,14 +414,6 @@ export class Parser {

// Create AT_RULE_PRELUDE wrapper if prelude parsing is enabled
if (this.prelude_parser) {
prelude_wrapper = this.arena.create_node(
AT_RULE_PRELUDE,
trimmed[0],
trimmed[1] - trimmed[0],
at_rule_line,
at_rule_column,
)

// Parse prelude and add structured nodes as children
let prelude_nodes = this.prelude_parser.parse_prelude(
at_rule_name,
Expand All @@ -441,7 +423,22 @@ export class Parser {
at_rule_column,
)
if (prelude_nodes.length > 0) {
prelude_wrapper = this.arena.create_node(
AT_RULE_PRELUDE,
trimmed[0],
trimmed[1] - trimmed[0],
at_rule_line,
at_rule_column,
)
this.arena.append_children(prelude_wrapper, prelude_nodes)
} else {
prelude_wrapper = this.arena.create_node(
RAW,
trimmed[0],
trimmed[1] - trimmed[0],
at_rule_line,
at_rule_column,
)
}
} else {
prelude_wrapper = this.arena.create_node(
Expand Down Expand Up @@ -476,7 +473,6 @@ export class Parser {

// Determine what to parse inside the block based on the at-rule name
let has_declarations = this.atrule_has_declarations(at_rule_name)
let is_conditional = this.atrule_is_conditional(at_rule_name)
let block_children: number[] = []

if (has_declarations) {
Expand All @@ -492,8 +488,8 @@ export class Parser {
this.next_token()
}
}
} else if (is_conditional) {
// Conditional at-rules can contain both declarations and rules (CSS Nesting)
} else {
// Parse declarations + rules + at-rules (like @media, @keyframes, unknown at-rules)
while (!this.is_eof()) {
let token_type = this.peek_type()
if (token_type === TOKEN_RIGHT_BRACE) break
Expand Down Expand Up @@ -525,19 +521,6 @@ export class Parser {
this.next_token()
}
}
} else {
// Parse nested rules only (like @keyframes)
while (!this.is_eof()) {
let token_type = this.peek_type()
if (token_type === TOKEN_RIGHT_BRACE) break

let rule = this.parse_rule()
if (rule !== null) {
block_children.push(rule)
} else {
this.next_token()
}
}
}

// Consume '}' (block excludes closing brace, but at-rule includes it)
Expand Down Expand Up @@ -591,11 +574,6 @@ export class Parser {
private atrule_has_declarations(name: string): boolean {
return DECLARATION_AT_RULES.has(name.toLowerCase())
}

// Determine if an at-rule is conditional (can contain both declarations and rules in CSS Nesting)
private atrule_is_conditional(name: string): boolean {
return CONDITIONAL_AT_RULES.has(name.toLowerCase())
}
}

/**
Expand Down