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
77 changes: 77 additions & 0 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1798,6 +1798,43 @@ describe('parse_atrule_prelude()', () => {
})
})

describe('function', () => {
test('should parse function name', () => {
const result = parse_atrule_prelude('function', '--transparent(--color, --alpha)')

expect(result.length).toBe(1)
expect(result[0].type).toBe(IDENTIFIER)
expect(result[0].text).toBe('--transparent')
})

test('should parse function name with no parameters', () => {
const result = parse_atrule_prelude('function', '--my-func()')

expect(result.length).toBe(1)
expect(result[0].type).toBe(IDENTIFIER)
expect(result[0].text).toBe('--my-func')
})

test('should parse function name with type annotations', () => {
const result = parse_atrule_prelude('function', '--add(--a <integer>, --b <integer>)')

expect(result.length).toBe(1)
expect(result[0].type).toBe(IDENTIFIER)
expect(result[0].text).toBe('--add')
})

test('should parse function name with returns clause', () => {
const result = parse_atrule_prelude(
'function',
'--clamp-it(--val, --min, --max) returns <number>',
)

expect(result.length).toBe(1)
expect(result[0].type).toBe(IDENTIFIER)
expect(result[0].text).toBe('--clamp-it')
})
})

describe('@import', () => {
test('should parse import with URL string', () => {
const result = parse_atrule_prelude('import', 'url("styles.css")')
Expand Down Expand Up @@ -2093,4 +2130,44 @@ and (min-width: 768px) { }`)
expect(mediaQuery?.children.length).toBeGreaterThan(0)
})
})

describe('@function', () => {
it('should parse function name as IDENTIFIER', () => {
const css =
'@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }'
const ast = parse(css)
const atRule = ast.first_child!

expect(atRule.type).toBe(AT_RULE)
expect(atRule.name).toBe('function')

const children = atRule.prelude?.children
expect(children?.length).toBe(1)
expect(children?.[0].type).toBe(IDENTIFIER)
expect(children?.[0].text).toBe('--transparent')
})

it('should parse function with no parameters', () => {
const css = '@function --my-func() { result: 42px; }'
const ast = parse(css)
const atRule = ast.first_child!

const children = atRule.prelude?.children || []
expect(children.length).toBe(1)
expect(children[0].type).toBe(IDENTIFIER)
expect(children[0].text).toBe('--my-func')
})

it('should parse function name location', () => {
const css = '@function --my-func(--x) { result: var(--x); }'
const ast = parse(css)
const atRule = ast.first_child!
const ident = atRule.prelude!.first_child!

// '@function ' = 10 chars, so name starts at offset 10
expect(ident.start).toBe(10)
expect(ident.text).toBe('--my-func')
expect(ident.length).toBe(9)
})
})
})
22 changes: 22 additions & 0 deletions src/parse-atrule-prelude.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export class AtRulePreludeParser {
case 'font-feature-values':
case 'page':
return this.parse_identifier()
case 'function':
return this.parse_function_prelude()
case 'import':
return this.parse_import_prelude()
case 'charset':
Expand Down Expand Up @@ -502,6 +504,26 @@ export class AtRulePreludeParser {
return nodes
}

// Parse @function prelude: --function-name(--param1, --param2, ...) [returns <type>]?
// The function name is a dashed-ident immediately followed by '(' (TOKEN_FUNCTION).
// Parameters and return type remain in the raw prelude text (accessible via .value).
private parse_function_prelude(): number[] {
this.skip_whitespace()
if (this.lexer.pos >= this.prelude_end) return []

this.next_token()

// @function prelude starts with a <dashed-function> token like --name(
// which the CSS tokenizer produces as TOKEN_FUNCTION (includes the '(')
if (this.lexer.token_type !== TOKEN_FUNCTION) return []

// Create an IDENTIFIER node for just the function name (without '(')
let name_start = this.lexer.token_start
let name_end = this.lexer.token_end - 1 // Exclude '('

return [this.create_node(IDENTIFIER, name_start, name_end)]
}

// Parse single identifier: keyframe name, property name
private parse_identifier(): number[] {
this.skip_whitespace()
Expand Down
62 changes: 62 additions & 0 deletions src/parse.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2111,6 +2111,68 @@ describe('Core Nodes', () => {
})
})

describe('@function at-rule', () => {
test('@function basic', () => {
let source =
'@function --transparent(--color, --alpha) { result: oklch(from var(--color) l c h / var(--alpha)); }'
let root = parse(source, { parse_atrule_preludes: false })

let fn = root.first_child!
expect(fn.type).toBe(AT_RULE)
expect(fn.name).toBe('function')
expect(fn.has_block).toBe(true)

let block = fn.block!
expect(block.children.length).toBe(1)

let result_decl = block.first_child!
expect(result_decl.type).toBe(DECLARATION)
expect(result_decl.property).toBe('result')
})

test('@function with local custom properties', () => {
let source =
'@function --anim(--animation, --count) { --duration: 1s; --easing: linear; result: var(--animation) var(--duration) var(--count) var(--easing); }'
let root = parse(source, { parse_atrule_preludes: false })

let fn = root.first_child!
let block = fn.block!
expect(block.children.length).toBe(3)

let [dur, ease, result_decl] = block.children
expect(dur.type).toBe(DECLARATION)
expect(dur.property).toBe('--duration')
expect(ease.property).toBe('--easing')
expect(result_decl.property).toBe('result')
})

test('@function with nested @media', () => {
let source =
'@function --narrow-wide(--narrow, --wide) { result: var(--wide); @media (width < 700px) { result: var(--narrow); } }'
let root = parse(source, { parse_atrule_preludes: false })

let fn = root.first_child!
let block = fn.block!
expect(block.children.length).toBe(2)

let [result_decl, media] = block.children
expect(result_decl.type).toBe(DECLARATION)
expect(result_decl.property).toBe('result')
expect(media.type).toBe(AT_RULE)
expect(media.name).toBe('media')
})

test('@function with no parameters', () => {
let source = '@function --my-func() { result: 42px; }'
let root = parse(source, { parse_atrule_preludes: false })

let fn = root.first_child!
expect(fn.type).toBe(AT_RULE)
expect(fn.name).toBe('function')
expect(fn.has_block).toBe(true)
})
})

describe('@nest at-rule', () => {
test('@nest with & selector', () => {
let source = '.parent { @nest & .child { color: blue; } }'
Expand Down
1 change: 1 addition & 0 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ let CONDITIONAL_AT_RULES = new Set([
'nest',
'scope',
'starting-style',
'function',
])

/** @internal */
Expand Down