Skip to content

Commit 38bf8e5

Browse files
authored
breaking: add intermedia AtrulePrelude node type (#107)
breaking: make AtRule.prelude return CSSNode instead of string Changes the `.prelude` getter on at-rule nodes to return `CSSNode | null` instead of `string | null`, matching CSSTree's API and enabling traversal of structured prelude children (media queries, layer names, etc.). BREAKING CHANGES: - AtRule.prelude now returns CSSNode | null instead of string | null - Use .prelude.text to get the text representation - Use .value for raw string when prelude parsing is disabled or unsupported - At-rules without structured prelude parsing return null - clone() no longer extracts prelude as a property (now part of children) Implementation: - Added AT_RULE_PRELUDE (40) wrapper node type - Parser wraps parsed prelude nodes before adding to at-rule children - Updated prelude getter to return AT_RULE_PRELUDE node or null - Exported AT_RULE_PRELUDE constant via constants.ts - Updated all 1107 tests to use new API Benefits: - Enables walking prelude children like CSSTree - Provides structured access to media queries, layer names, etc. - Maintains text access via .prelude.text - Preserves arena efficiency (one wrapper per at-rule) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <[email protected]>
1 parent 599f11a commit 38bf8e5

File tree

7 files changed

+297
-232
lines changed

7 files changed

+297
-232
lines changed

src/api.test.ts

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,28 @@ describe('CSSNode', () => {
7676

7777
expect(media.type).toBe(AT_RULE)
7878
expect(media.has_prelude).toBe(true)
79-
expect(media.prelude).toBe('(min-width: 768px)')
79+
expect(media.prelude).not.toBeNull()
80+
expect(media.prelude?.text).toBe('(min-width: 768px)')
81+
})
82+
83+
test('prelude node can be walked to access structured children', () => {
84+
const source = '@media screen and (min-width: 768px) { }'
85+
const root = parse(source)
86+
const media = root.first_child!
87+
const prelude = media.prelude!
88+
89+
// Prelude is a wrapper node containing the structured children
90+
expect(prelude).not.toBeNull()
91+
expect(prelude.has_children).toBe(true)
92+
93+
// Can iterate over prelude children (media queries)
94+
const children = [...prelude]
95+
expect(children.length).toBeGreaterThan(0)
96+
97+
// Can access structured nodes within prelude
98+
const mediaQuery = prelude.first_child!
99+
expect(mediaQuery.type_name).toBe('MediaQuery')
100+
expect(mediaQuery.text).toBe('screen and (min-width: 768px)')
80101
})
81102

82103
test('should return true for @supports with prelude', () => {
@@ -86,7 +107,8 @@ describe('CSSNode', () => {
86107

87108
expect(supports.type).toBe(AT_RULE)
88109
expect(supports.has_prelude).toBe(true)
89-
expect(supports.prelude).toBe('(display: grid)')
110+
expect(supports.prelude).not.toBeNull()
111+
expect(supports.prelude?.text).toBe('(display: grid)')
90112
})
91113

92114
test('should return true for @layer with name', () => {
@@ -96,7 +118,8 @@ describe('CSSNode', () => {
96118

97119
expect(layer.type).toBe(AT_RULE)
98120
expect(layer.has_prelude).toBe(true)
99-
expect(layer.prelude).toBe('utilities')
121+
expect(layer.prelude).not.toBeNull()
122+
expect(layer.prelude?.text).toBe('utilities')
100123
})
101124

102125
test('should return false for @layer without name', () => {
@@ -116,7 +139,8 @@ describe('CSSNode', () => {
116139

117140
expect(keyframes.type).toBe(AT_RULE)
118141
expect(keyframes.has_prelude).toBe(true)
119-
expect(keyframes.prelude).toBe('fadeIn')
142+
expect(keyframes.prelude).not.toBeNull()
143+
expect(keyframes.prelude?.text).toBe('fadeIn')
120144
})
121145

122146
test('should return false for @font-face without prelude', () => {
@@ -573,9 +597,10 @@ describe('CSSNode', () => {
573597
const source = '@media screen and (min-width: 768px) { body { color: red; } }'
574598
const root = parse(source)
575599
const media = root.first_child!
576-
const prelude = media.first_child!
600+
const preludeWrapper = media.prelude!
601+
const mediaQuery = preludeWrapper.first_child!
577602

578-
expect(prelude.type_name).toBe('MediaQuery')
603+
expect(mediaQuery.type_name).toBe('MediaQuery')
579604
})
580605
})
581606

@@ -1082,15 +1107,16 @@ describe('CSSNode', () => {
10821107
})
10831108

10841109
test('extracts at-rule properties', () => {
1085-
const ast = parse('@media screen { }', { parse_atrule_preludes: false })
1110+
const ast = parse('@media screen { }')
10861111
const atrule = ast.first_child!
10871112

10881113
const clone = atrule.clone({ deep: false })
10891114

10901115
expect(clone.type).toBe(AT_RULE)
10911116
expect(clone.type_name).toBe('Atrule')
10921117
expect(clone.name).toBe('media')
1093-
expect(clone.prelude).toBe('screen')
1118+
// Prelude is now a child node, not extracted as property
1119+
expect(clone.children).toEqual([])
10941120
})
10951121

10961122
test('extracts dimension value with unit', () => {

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export const SUPPORTS_QUERY = 36 // supports query: (display: flex)
8888
export const LAYER_NAME = 37 // layer name: base, components
8989
export const PRELUDE_OPERATOR = 38 // logical operator: and, or, not
9090
export const FEATURE_RANGE = 39 // Range syntax: (50px <= width <= 100px)
91+
export const AT_RULE_PRELUDE = 40 // Wrapper for at-rule prelude children
9192

9293
// Flag constants (bit-packed in 1 byte)
9394
export const FLAG_IMPORTANT = 1 << 0 // Has !important

src/constants.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import {
4040
LAYER_NAME,
4141
PRELUDE_OPERATOR,
4242
FEATURE_RANGE,
43+
AT_RULE_PRELUDE,
4344
FLAG_IMPORTANT,
4445
ATTR_OPERATOR_NONE,
4546
ATTR_OPERATOR_EQUAL,
@@ -92,6 +93,7 @@ export {
9293
LAYER_NAME,
9394
PRELUDE_OPERATOR,
9495
FEATURE_RANGE,
96+
AT_RULE_PRELUDE,
9597
FLAG_IMPORTANT,
9698
ATTR_OPERATOR_NONE,
9799
ATTR_OPERATOR_EQUAL,
@@ -149,4 +151,5 @@ export const NODE_TYPES = {
149151
LAYER_NAME,
150152
PRELUDE_OPERATOR,
151153
FEATURE_RANGE,
154+
AT_RULE_PRELUDE,
152155
} as const

src/css-node.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
LAYER_NAME,
4040
PRELUDE_OPERATOR,
4141
FEATURE_RANGE,
42+
AT_RULE_PRELUDE,
4243
FLAG_IMPORTANT,
4344
FLAG_HAS_ERROR,
4445
FLAG_HAS_BLOCK,
@@ -90,6 +91,7 @@ export const TYPE_NAMES = {
9091
[LAYER_NAME]: 'Layer',
9192
[PRELUDE_OPERATOR]: 'Operator',
9293
[FEATURE_RANGE]: 'MediaFeatureRange',
94+
[AT_RULE_PRELUDE]: 'AtrulePrelude',
9395
} as const
9496

9597
export type TypeName = (typeof TYPE_NAMES)[keyof typeof TYPE_NAMES] | 'unknown'
@@ -134,6 +136,7 @@ export type CSSNodeType =
134136
| typeof LAYER_NAME
135137
| typeof PRELUDE_OPERATOR
136138
| typeof FEATURE_RANGE
139+
| typeof AT_RULE_PRELUDE
137140

138141
// Options for cloning nodes
139142
export interface CloneOptions {
@@ -304,12 +307,16 @@ export class CSSNode {
304307
}
305308

306309
/**
307-
* Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
308-
* This is an alias for `value` to make at-rule usage more semantic
310+
* Get the prelude node (for at-rules: structured prelude with media queries, layer names, etc.)
311+
* Returns the AT_RULE_PRELUDE wrapper node containing prelude children, or null if no prelude
309312
*/
310-
get prelude(): string | null {
311-
let val = this.value
312-
return typeof val === 'string' ? val : null
313+
get prelude(): CSSNode | null {
314+
if (this.type !== AT_RULE) return null
315+
let first = this.first_child
316+
if (first && first.type === AT_RULE_PRELUDE) {
317+
return first
318+
}
319+
return null
313320
}
314321

315322
/**
@@ -734,10 +741,9 @@ export class CSSNode {
734741
if (this.unit) plain.unit = this.unit
735742
}
736743

737-
// 4. Extract prelude for at-rules
738-
if (this.type === AT_RULE && this.prelude) {
739-
plain.prelude = this.prelude
740-
}
744+
// 4. At-rule preludes are now child nodes (AT_RULE_PRELUDE wrapper)
745+
// They will be cloned as part of children in deep clones
746+
// No special extraction needed - breaking change from string to CSSNode
741747

742748
// 5. Extract flags
743749
if (this.type === DECLARATION) {

0 commit comments

Comments
 (0)