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
7 changes: 7 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,10 @@ export class CSSNode {
return first_child // VALUE node (when parse_values=true)
}

if (type === MEDIA_FEATURE) {
return first_child ?? null
}

if (type === DIMENSION) {
return parse_dimension(text).value
}
Expand Down Expand Up @@ -506,6 +510,9 @@ export class CSSNode {
case IDENTIFIER:
// Check identifier value (e.g., -webkit-sticky)
return is_vendor_prefixed(this.text)
case MEDIA_FEATURE:
// Check property name (e.g., -ms-high-contrast)
return is_vendor_prefixed(this.get_content())
default:
return false
}
Expand Down
15 changes: 8 additions & 7 deletions src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -462,13 +462,14 @@ export type MediaQuery = CSSNode &
clone(options?: CloneOptions): ToPlain<MediaQuery>
}

export type MediaFeature = CSSNode &
WithChildren & {
readonly type: typeof MEDIA_FEATURE
/** Feature name, e.g. "min-width" */
readonly property: string
clone(options?: CloneOptions): ToPlain<MediaFeature>
}
export type MediaFeature = CSSNode & {
readonly type: typeof MEDIA_FEATURE
/** Feature name, e.g. "min-width" */
readonly property: string
/** Feature value node, or null for boolean features like (hover) */
readonly value: CSSNode | null
clone(options?: CloneOptions): ToPlain<MediaFeature>
}

export type MediaType = CSSNode & {
readonly type: typeof MEDIA_TYPE
Expand Down
44 changes: 31 additions & 13 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,8 +515,7 @@ describe('At-Rule Prelude Nodes', () => {
| undefined

expect(feature?.property).toBe('orientation')
expect(feature?.children.length).toBe(1)
expect(feature?.children[0].type).toBe(IDENTIFIER)
expect(feature?.value?.type).toBe(IDENTIFIER)
})

test('should extract feature name from boolean feature', () => {
Expand All @@ -533,7 +532,7 @@ describe('At-Rule Prelude Nodes', () => {
expect(feature?.property).toBe('hover')
})

test('should parse feature values as typed children', () => {
test('should parse feature value as typed node', () => {
const css = '@media (min-width: 768px) { }'
const ast = parse(css)
const atRule = ast.first_child! as Atrule
Expand All @@ -545,34 +544,53 @@ describe('At-Rule Prelude Nodes', () => {
| undefined

expect(feature?.property).toBe('min-width')
expect(feature?.children.length).toBe(1)
expect(feature?.children[0].type).toBe(DIMENSION)
expect(feature?.value?.type).toBe(DIMENSION)
})

test('should parse identifier value as child', () => {
test('should parse identifier value', () => {
const css = '@media (orientation: portrait) { }'
const ast = parse(css)
const atRule = ast.first_child! as Atrule
const queryChildren =
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
?.children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
| MediaFeature
| undefined

expect(feature?.children.length).toBe(1)
expect(feature?.children[0].type).toBe(IDENTIFIER)
expect(feature?.children[0].text).toBe('portrait')
expect(feature?.value?.type).toBe(IDENTIFIER)
expect(feature?.value?.text).toBe('portrait')
})

test('should have no children for boolean features', () => {
test('should have null value for boolean features', () => {
const css = '@media (hover) { }'
const ast = parse(css)
const atRule = ast.first_child! as Atrule
const queryChildren =
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
?.children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
| MediaFeature
| undefined

expect(feature?.value).toBeNull()
})

test('should parse vendor-prefixed media feature (-ms-high-contrast: active)', () => {
const css = '@media (-ms-high-contrast: active) {}'
const ast = parse(css)
const atRule = ast.first_child! as Atrule
const queryChildren =
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
?.children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
| MediaFeature
| undefined

expect(feature?.children.length).toBe(0)
expect(feature?.property).toBe('-ms-high-contrast')
expect(feature?.is_vendor_prefixed).toBe(true)
expect(feature?.value?.type).toBe(IDENTIFIER)
expect(feature?.value?.text).toBe('active')
})

test('should parse range syntax with single comparison', () => {
Expand Down