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
35 changes: 25 additions & 10 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,20 +229,29 @@ export class CSSNode {
// For URL nodes with quoted string: returns the string with quotes (consistent with STRING node)
// For URL nodes with unquoted URL: returns the URL content without quotes
get value(): string | number | null {
let { type, text } = this

if (type === DIMENSION) {
return parse_dimension(text).value
}

if (type === NUMBER) {
return Number.parseFloat(this.text)
}

// Special handling for URL nodes
if (this.type === URL) {
const firstChild = this.first_child
if (type === URL) {
let firstChild = this.first_child
if (firstChild && firstChild.type === STRING) {
// Return the string as-is (with quotes) - consistent with STRING node
return firstChild.text
}
// For URL nodes without children (e.g., @import url(...)), extract from text
// Handle both url("...") and url('...') and just "..." or '...'
const text = this.text
if (str_starts_with(text, 'url(')) {
// url("...") or url('...') or url(...) - extract content between parens
const openParen = text.indexOf('(')
const closeParen = text.lastIndexOf(')')
let openParen = text.indexOf('(')
let closeParen = text.lastIndexOf(')')
if (openParen !== -1 && closeParen !== -1 && closeParen > openParen) {
let content = text.substring(openParen + 1, closeParen).trim()
return content
Expand All @@ -254,18 +263,24 @@ export class CSSNode {
// For unquoted URLs, fall through to value delta logic below
}

// For dimension and number nodes, parse and return as number
if (this.type === DIMENSION || this.type === NUMBER) {
return parse_dimension(this.text).value
}

// For other nodes, return as string
let start = this.arena.get_value_start(this.index)
let length = this.arena.get_value_length(this.index)
if (length === 0) return null
return this.source.substring(start, start + length)
}

get value_as_number(): number | null {
let text = this.text
if (this.type === NUMBER) {
return Number.parseFloat(text)
}
if (this.type === DIMENSION) {
return parse_dimension(text).value
}
return null
}

// Get the prelude text (for at-rules: "(min-width: 768px)" in "@media (min-width: 768px)")
// This is an alias for `value` to make at-rule usage more semantic
get prelude(): string | null {
Expand Down
84 changes: 84 additions & 0 deletions src/parse-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -660,6 +660,90 @@ describe('Value Node Types', () => {
})
})

describe('value_as_number getter', () => {
it('should return number for NUMBER nodes', () => {
const root = parse('div { opacity: 0.5; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const numberNode = decl?.values[0]

expect(numberNode?.type).toBe(NUMBER)
expect(numberNode?.value_as_number).toBe(0.5)
})

it('should return number for DIMENSION nodes', () => {
const root = parse('div { width: 100px; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const dimNode = decl?.values[0]

expect(dimNode?.type).toBe(DIMENSION)
expect(dimNode?.value_as_number).toBe(100)
})

it('should handle negative numbers', () => {
const root = parse('div { margin: -10px; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const dimNode = decl?.values[0]

expect(dimNode?.type).toBe(DIMENSION)
expect(dimNode?.value_as_number).toBe(-10)
})

it('should handle zero', () => {
const root = parse('div { margin: 0; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const numberNode = decl?.values[0]

expect(numberNode?.type).toBe(NUMBER)
expect(numberNode?.value_as_number).toBe(0)
})

it('should handle decimal numbers', () => {
const root = parse('div { line-height: 1.5; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const numberNode = decl?.values[0]

expect(numberNode?.type).toBe(NUMBER)
expect(numberNode?.value_as_number).toBe(1.5)
})

it('should handle percentage dimensions', () => {
const root = parse('div { width: 50%; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const dimNode = decl?.values[0]

expect(dimNode?.type).toBe(DIMENSION)
expect(dimNode?.value_as_number).toBe(50)
expect(dimNode?.unit).toBe('%')
})

it('should return null for IDENTIFIER nodes', () => {
const root = parse('div { color: red; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const identNode = decl?.values[0]

expect(identNode?.type).toBe(IDENTIFIER)
expect(identNode?.value_as_number).toBeNull()
})

it('should return null for STRING nodes', () => {
const root = parse('div { content: "hello"; }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const stringNode = decl?.values[0]

expect(stringNode?.type).toBe(STRING)
expect(stringNode?.value_as_number).toBeNull()
})

it('should return null for FUNCTION nodes', () => {
const root = parse('div { width: calc(100% - 20px); }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
const funcNode = decl?.values[0]

expect(funcNode?.type).toBe(FUNCTION)
expect(funcNode?.value_as_number).toBeNull()
})
})

describe('Case-insensitive function names', () => {
const getValue = (css: string) => {
const root = parse(css)
Expand Down
Loading