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
4 changes: 2 additions & 2 deletions src/node-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -332,10 +332,10 @@ describe('type narrowing — compile-time', () => {
}
})

test('FeatureRange children are typed as Dimension | PreludeOperator, not Operator', () => {
test('FeatureRange children are typed as Dimension | Function | PreludeOperator, not Operator', () => {
const atrule = parse('@media (width >= 400px) {}').first_child! as Atrule
const range = atrule.prelude!.first_child!.first_child! as FeatureRange
expectTypeOf(range.children).toEqualTypeOf<(Dimension | PreludeOperator)[]>()
expectTypeOf(range.children).toEqualTypeOf<(Dimension | Function | PreludeOperator)[]>()

const [op, dim] = range.children
expect(op.type_name).toBe('Operator')
Expand Down
2 changes: 1 addition & 1 deletion src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -622,7 +622,7 @@ export type PreludeOperator = Leaf<typeof PRELUDE_OPERATOR, 'Operator'>

export type FeatureRange = WithClone<
CSSNode &
WithChildren<Dimension | PreludeOperator> & {
WithChildren<Dimension | Function | PreludeOperator> & {
readonly type: typeof FEATURE_RANGE
readonly type_name: 'MediaFeatureRange'
/** The feature name in a range comparison, e.g. "width" from "(width >= 400px)" */
Expand Down
32 changes: 32 additions & 0 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -769,6 +769,38 @@ describe('At-Rule Prelude Nodes', () => {
expect(range?.children[1].type).toBe(DIMENSION) // 400px
})

test('should keep a function value whole in range syntax', () => {
const [query] = parse_atrule_prelude('media', '(width >= calc(1px + 2px)) and (color)')
const range = (query as MediaQuery).children[0] as FeatureRange

expect(range.text).toBe('(width >= calc(1px + 2px))')
expect(range.children.map((c) => c.text)).toEqual(['>=', 'calc(1px + 2px)'])
const fn = range.children[1] as Function
expect(fn.name).toBe('calc')
expect(fn.value).toBe('1px + 2px')
})

test('should keep a leading function value whole in range syntax', () => {
const [query] = parse_atrule_prelude('media', '(calc(1px) < width < 5px)')
const range = (query as MediaQuery).children[0] as FeatureRange

expect(range.children.map((c) => c.text)).toEqual(['calc(1px)', '<', '<', '5px'])
})

test('should tokenize a bare = as its own comparator', () => {
const [query] = parse_atrule_prelude('media', '(width=>1000px)')
const range = (query as MediaQuery).children[0] as FeatureRange

expect(range.children.map((c) => c.text)).toEqual(['=', '>', '1000px'])
})

test('should tokenize >= and <= as a single comparator', () => {
const [query] = parse_atrule_prelude('media', '(100px<=width>=1000px)')
const range = (query as MediaQuery).children[0] as FeatureRange

expect(range.children.map((c) => c.text)).toEqual(['100px', '<=', '>=', '1000px'])
})

test('should parse range syntax with double comparison', () => {
const css = '@media (50px <= width <= 100px) { }'
const ast = parse(css)
Expand Down
17 changes: 14 additions & 3 deletions src/parse-condition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,12 @@ export class ConditionParser {
// Comparison operator
if (ch === CHAR_LESS_THAN || ch === CHAR_GREATER_THAN || ch === CHAR_EQUALS) {
const op_start = pos++
if (pos < content_end && this.source.charCodeAt(pos) === CHAR_EQUALS) {
// '<=' and '>=' are one comparator; a bare '=' stands alone, so '=>' is '=' then '>'
if (
ch !== CHAR_EQUALS &&
pos < content_end &&
this.source.charCodeAt(pos) === CHAR_EQUALS
) {
pos++
}

Expand All @@ -268,8 +273,14 @@ export class ConditionParser {
feature_name_start = this.lexer.token_start
feature_name_end = this.lexer.token_end
} else {
// Value (may itself be a short chain, e.g. a single dimension node)
const value_first = this.parse_feature_value(this.lexer.token_start, this.lexer.token_end)
// Value (may itself be a short chain, e.g. a single dimension node). A function
// token only covers 'name(' — extend to its matching ')' so calc() etc. stay whole
const value_start = this.lexer.token_start
let value_end = this.lexer.token_end
if (this.lexer.token_type === TOKEN_FUNCTION && this.scan_matching_paren()) {
value_end = this.paren_close_end
}
const value_first = this.parse_feature_value(value_start, value_end)
if (value_first !== 0) {
if (first_child === 0) {
first_child = value_first
Expand Down
Loading