Skip to content

Commit 95e119a

Browse files
authored
feat: add vendor-prefix support in mediafeature (#225)
- Add `value` getter to `CSSNode` for `MEDIA_FEATURE` nodes, returning the first child node or `null` for boolean features like `(hover)` - Add `MEDIA_FEATURE` case to `is_vendor_prefixed` getter, checking the feature name (e.g. `-ms-high-contrast` → `true`) - Add `value: CSSNode | null` to the `MediaFeature` type; remove `WithChildren` for consistency with `Declaration`, which also hides children in favour of a typed `value` accessor - Update tests to use `feature.value` instead of `feature.children`
1 parent ad61272 commit 95e119a

3 files changed

Lines changed: 46 additions & 20 deletions

File tree

src/css-node.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ export class CSSNode {
347347
return first_child // VALUE node (when parse_values=true)
348348
}
349349

350+
if (type === MEDIA_FEATURE) {
351+
return first_child ?? null
352+
}
353+
350354
if (type === DIMENSION) {
351355
return parse_dimension(text).value
352356
}
@@ -506,6 +510,9 @@ export class CSSNode {
506510
case IDENTIFIER:
507511
// Check identifier value (e.g., -webkit-sticky)
508512
return is_vendor_prefixed(this.text)
513+
case MEDIA_FEATURE:
514+
// Check property name (e.g., -ms-high-contrast)
515+
return is_vendor_prefixed(this.get_content())
509516
default:
510517
return false
511518
}

src/node-types.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -462,13 +462,14 @@ export type MediaQuery = CSSNode &
462462
clone(options?: CloneOptions): ToPlain<MediaQuery>
463463
}
464464

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

473474
export type MediaType = CSSNode & {
474475
readonly type: typeof MEDIA_TYPE

src/parse-atrule-prelude.test.ts

Lines changed: 31 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -515,8 +515,7 @@ describe('At-Rule Prelude Nodes', () => {
515515
| undefined
516516

517517
expect(feature?.property).toBe('orientation')
518-
expect(feature?.children.length).toBe(1)
519-
expect(feature?.children[0].type).toBe(IDENTIFIER)
518+
expect(feature?.value?.type).toBe(IDENTIFIER)
520519
})
521520

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

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

547546
expect(feature?.property).toBe('min-width')
548-
expect(feature?.children.length).toBe(1)
549-
expect(feature?.children[0].type).toBe(DIMENSION)
547+
expect(feature?.value?.type).toBe(DIMENSION)
550548
})
551549

552-
test('should parse identifier value as child', () => {
550+
test('should parse identifier value', () => {
553551
const css = '@media (orientation: portrait) { }'
554552
const ast = parse(css)
555553
const atRule = ast.first_child! as Atrule
556554
const queryChildren =
557555
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
558556
?.children || []
559-
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)
557+
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
558+
| MediaFeature
559+
| undefined
560560

561-
expect(feature?.children.length).toBe(1)
562-
expect(feature?.children[0].type).toBe(IDENTIFIER)
563-
expect(feature?.children[0].text).toBe('portrait')
561+
expect(feature?.value?.type).toBe(IDENTIFIER)
562+
expect(feature?.value?.text).toBe('portrait')
564563
})
565564

566-
test('should have no children for boolean features', () => {
565+
test('should have null value for boolean features', () => {
567566
const css = '@media (hover) { }'
568567
const ast = parse(css)
569568
const atRule = ast.first_child! as Atrule
570569
const queryChildren =
571570
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
572571
?.children || []
573-
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)
572+
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
573+
| MediaFeature
574+
| undefined
575+
576+
expect(feature?.value).toBeNull()
577+
})
578+
579+
test('should parse vendor-prefixed media feature (-ms-high-contrast: active)', () => {
580+
const css = '@media (-ms-high-contrast: active) {}'
581+
const ast = parse(css)
582+
const atRule = ast.first_child! as Atrule
583+
const queryChildren =
584+
((atRule.prelude as AtrulePrelude | null)?.children[0] as MediaQuery | undefined)
585+
?.children || []
586+
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE) as
587+
| MediaFeature
588+
| undefined
574589

575-
expect(feature?.children.length).toBe(0)
590+
expect(feature?.property).toBe('-ms-high-contrast')
591+
expect(feature?.is_vendor_prefixed).toBe(true)
592+
expect(feature?.value?.type).toBe(IDENTIFIER)
593+
expect(feature?.value?.text).toBe('active')
576594
})
577595

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

0 commit comments

Comments
 (0)