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
3 changes: 2 additions & 1 deletion API.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ function parse(source: string, options?: ParserOptions): CSSNode
- `text` - Full text of the node from source
- `name` - Property name, at-rule name, or layer name
- `property` - Alias for `name` (for declarations)
- `value` - Value text (for declarations) or `null`
- `value` - Value text (for declarations), URL content (for URL nodes with quoted strings, includes quotes to match STRING node behavior), or `null`
- `prelude` - At-rule prelude text or `null`
- `line` - Starting line number (1-based)
- `offset` - Starting offset in source
Expand Down Expand Up @@ -174,6 +174,7 @@ console.log(importRule.has_children) // true (has prelude nodes)
const [url, layer, supports, media] = importRule.children
console.log(url.type) // URL
console.log(url.text) // 'url("styles.css")'
console.log(url.value) // '"styles.css"'

console.log(layer.type) // LAYER_NAME
console.log(layer.name) // "base"
Expand Down
27 changes: 27 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,34 @@ export class CSSNode {
// Get the value text (for declarations: "blue" in "color: blue")
// For dimension/number nodes: returns the numeric value as a number
// For string nodes: returns the string content without quotes
// 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 {
// Special handling for URL nodes
if (this.type === URL) {
const 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 (text.startsWith('url(')) {
// url("...") or url('...') or url(...) - extract content between parens
const openParen = text.indexOf('(')
const closeParen = text.lastIndexOf(')')
if (openParen !== -1 && closeParen !== -1 && closeParen > openParen) {
let content = text.substring(openParen + 1, closeParen).trim()
return content
}
} else if (text.startsWith('"') || text.startsWith("'")) {
// Just a quoted string: "..." or '...'
return text
}
// 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
Expand Down
24 changes: 24 additions & 0 deletions src/parse-atrule-prelude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -674,6 +674,30 @@ describe('At-Rule Prelude Nodes', () => {
expect(children[0].text).toBe('"styles.css"')
})


it('should have .value property for URL with quoted url() function', () => {
const css = '@import url("example.com");'
const ast = parse(css, { parse_atrule_preludes: true })
const atRule = ast.first_child
const url = atRule?.children[0]

expect(url?.type).toBe(URL)
expect(url?.text).toBe('url("example.com")')
// URL node in @import returns the content with quotes
expect(url?.value).toBe('"example.com"')
})

it('should have .value property for URL with quoted string', () => {
const css = '@import "example.com";'
const ast = parse(css, { parse_atrule_preludes: true })
const atRule = ast.first_child
const url = atRule?.children[0]

expect(url?.type).toBe(URL)
expect(url?.text).toBe('"example.com"')
// URL node in @import returns the string with quotes
expect(url?.value).toBe('"example.com"')
})
it('should parse with anonymous layer', () => {
const css = '@import url("styles.css") layer;'
const ast = parse(css, { parse_atrule_preludes: true })
Expand Down
16 changes: 16 additions & 0 deletions src/parse-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,8 @@ describe('Value Node Types', () => {
expect(decl?.values[0].children).toHaveLength(1)
expect(decl?.values[0].children[0].type).toBe(STRING)
expect(decl?.values[0].children[0].text).toBe('"image.png"')
// URL node with quoted string returns the string value with quotes
expect(decl?.values[0].value).toBe('"image.png"')
})

it('should parse url() function with unquoted URL containing dots', () => {
Expand Down Expand Up @@ -575,6 +577,20 @@ describe('Value Node Types', () => {
expect(func?.value).toBe('myfont.woff2')
})

it('should parse url() function with single-quoted string', () => {
const root = parse("body { background: url('image.png'); }")
const decl = root.first_child?.first_child?.next_sibling?.first_child

expect(decl?.values).toHaveLength(1)
expect(decl?.values[0].type).toBe(URL)
expect(decl?.values[0].name).toBe('url')
expect(decl?.values[0].children).toHaveLength(1)
expect(decl?.values[0].children[0].type).toBe(STRING)
expect(decl?.values[0].children[0].text).toBe("'image.png'")
// URL node with single-quoted string returns the string value with quotes
expect(decl?.values[0].value).toBe("'image.png'")
})

it('should parse url() with base64 data URL', () => {
const root = parse('body { background: url(data:image/png;base64,iVBORw0KGg); }')
const decl = root.first_child?.first_child?.next_sibling?.first_child
Expand Down