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
1 change: 1 addition & 0 deletions src/arena.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ export const CONTAINER_QUERY = 35 // container query: sidebar (min-width: 400px)
export const SUPPORTS_QUERY = 36 // supports query: (display: flex)
export const LAYER_NAME = 37 // layer name: base, components
export const PRELUDE_OPERATOR = 38 // logical operator: and, or, not
export const FEATURE_RANGE = 39 // Range syntax: (50px <= width <= 100px)

// Flag constants (bit-packed in 1 byte)
export const FLAG_IMPORTANT = 1 << 0 // Has !important
Expand Down
3 changes: 3 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ import {
SUPPORTS_QUERY,
LAYER_NAME,
PRELUDE_OPERATOR,
FEATURE_RANGE,
FLAG_IMPORTANT,
ATTR_OPERATOR_NONE,
ATTR_OPERATOR_EQUAL,
Expand Down Expand Up @@ -88,6 +89,7 @@ export {
SUPPORTS_QUERY,
LAYER_NAME,
PRELUDE_OPERATOR,
FEATURE_RANGE,
FLAG_IMPORTANT,
ATTR_OPERATOR_NONE,
ATTR_OPERATOR_EQUAL,
Expand Down Expand Up @@ -143,4 +145,5 @@ export const NODE_TYPES = {
SUPPORTS_QUERY,
LAYER_NAME,
PRELUDE_OPERATOR,
FEATURE_RANGE,
} as const
3 changes: 3 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {
SUPPORTS_QUERY,
LAYER_NAME,
PRELUDE_OPERATOR,
FEATURE_RANGE,
FLAG_IMPORTANT,
FLAG_HAS_ERROR,
FLAG_HAS_BLOCK,
Expand Down Expand Up @@ -86,6 +87,7 @@ export const TYPE_NAMES = {
[SUPPORTS_QUERY]: 'SupportsQuery',
[LAYER_NAME]: 'Layer',
[PRELUDE_OPERATOR]: 'Operator',
[FEATURE_RANGE]: 'MediaFeatureRange',
} as const

export type TypeName = (typeof TYPE_NAMES)[keyof typeof TYPE_NAMES] | 'unknown'
Expand Down Expand Up @@ -128,6 +130,7 @@ export type CSSNodeType =
| typeof SUPPORTS_QUERY
| typeof LAYER_NAME
| typeof PRELUDE_OPERATOR
| typeof FEATURE_RANGE

// Options for cloning nodes
export interface CloneOptions {
Expand Down
133 changes: 131 additions & 2 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import {
PRELUDE_OPERATOR,
URL,
FUNCTION,
DIMENSION,
FEATURE_RANGE,
} from './arena'

describe('At-Rule Prelude Nodes', () => {
Expand Down Expand Up @@ -425,7 +427,7 @@ describe('At-Rule Prelude Nodes', () => {

// Feature should have content
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)
expect(feature?.value).toContain('min-width')
expect(feature?.name).toBe('min-width')
})

it('should trim whitespace and comments from media features', () => {
Expand All @@ -436,7 +438,7 @@ describe('At-Rule Prelude Nodes', () => {
const queryChildren = children[0].children
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

expect(feature?.value).toBe('min-width: 768px')
expect(feature?.name).toBe('min-width')
})

it('should parse complex media query with and operator', () => {
Expand Down Expand Up @@ -465,6 +467,128 @@ describe('At-Rule Prelude Nodes', () => {
expect(features.length).toBe(2)
})

it('should extract feature name from standard feature', () => {
const css = '@media (orientation: portrait) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

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

it('should extract feature name from boolean feature', () => {
const css = '@media (hover) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

expect(feature?.name).toBe('hover')
})

it('should parse feature values as typed children', () => {
const css = '@media (min-width: 768px) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

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

it('should parse identifier value as child', () => {
const css = '@media (orientation: portrait) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

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

it('should have no children for boolean features', () => {
const css = '@media (hover) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const feature = queryChildren.find((c) => c.type === MEDIA_FEATURE)

expect(feature?.children.length).toBe(0)
})

it('should parse range syntax with single comparison', () => {
const css = '@media (width >= 400px) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const range = queryChildren.find((c) => c.type === FEATURE_RANGE)

expect(range?.type).toBe(FEATURE_RANGE)
expect(range?.name).toBe('width')
expect(range?.children.length).toBe(2) // dimension + operator

// Verify child types
expect(range?.children[0].type).toBe(PRELUDE_OPERATOR) // >=
expect(range?.children[1].type).toBe(DIMENSION) // 400px
})

it('should parse range syntax with double comparison', () => {
const css = '@media (50px <= width <= 100px) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const range = queryChildren.find((c) => c.type === FEATURE_RANGE)

expect(range?.type).toBe(FEATURE_RANGE)
expect(range?.name).toBe('width')
expect(range?.children.length).toBe(4) // dim, op, op, dim

// Verify child types
expect(range?.children[0].type).toBe(DIMENSION) // 50px
expect(range?.children[1].type).toBe(PRELUDE_OPERATOR) // <=
expect(range?.children[2].type).toBe(PRELUDE_OPERATOR) // <=
expect(range?.children[3].type).toBe(DIMENSION) // 100px
})

it('should parse range syntax with less-than', () => {
const css = '@media (400px < width) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const range = queryChildren.find((c) => c.type === FEATURE_RANGE)

expect(range?.type).toBe(FEATURE_RANGE)
expect(range?.name).toBe('width')
expect(range?.children.length).toBe(2)

// Verify child types
expect(range?.children[0].type).toBe(DIMENSION) // 400px
expect(range?.children[1].type).toBe(PRELUDE_OPERATOR) // <
})

it('should parse range syntax with equals', () => {
const css = '@media (width = 500px) { }'
const ast = parse(css)
const atRule = ast.first_child
const queryChildren = atRule?.children[0].children || []
const range = queryChildren.find((c) => c.type === FEATURE_RANGE)

expect(range?.type).toBe(FEATURE_RANGE)
expect(range?.name).toBe('width')
expect(range?.children.length).toBe(2)

// Verify child types
expect(range?.children[0].type).toBe(PRELUDE_OPERATOR) // =
expect(range?.children[1].type).toBe(DIMENSION) // 500px
})

it('should parse comma-separated media queries', () => {
const css = '@media screen, print { }'
const ast = parse(css)
Expand All @@ -474,6 +598,11 @@ describe('At-Rule Prelude Nodes', () => {
// Should have 2 media query nodes
const queries = children.filter((c) => c.type === MEDIA_QUERY)
expect(queries.length).toBe(2)
const [screen, print] = queries
expect(screen.type_name).toBe('MediaQuery')
expect(screen.text).toBe('screen')
expect(print.type_name).toBe('MediaQuery')
expect(print.text).toBe('print')
})
})

Expand Down
Loading