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 @@ -39,6 +39,7 @@ export const NODE_VALUE_STRING = 13 // quoted string: "hello", 'world'
export const NODE_VALUE_COLOR = 14 // hex color: #fff, #ff0000
export const NODE_VALUE_FUNCTION = 15 // function: calc(), var(), url()
export const NODE_VALUE_OPERATOR = 16 // operator: +, -, *, /, comma
export const NODE_VALUE_PARENTHESIS = 17 // parenthesized expression: (100% - 50px)

// Selector node type constants (for detailed selector parsing)
export const NODE_SELECTOR_LIST = 20 // comma-separated selectors
Expand Down
2 changes: 2 additions & 0 deletions src/css-node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
NODE_VALUE_COLOR,
NODE_VALUE_FUNCTION,
NODE_VALUE_OPERATOR,
NODE_VALUE_PARENTHESIS,
NODE_SELECTOR_LIST,
NODE_SELECTOR_TYPE,
NODE_SELECTOR_CLASS,
Expand Down Expand Up @@ -66,6 +67,7 @@ export type CSSNodeType =
| typeof NODE_VALUE_COLOR
| typeof NODE_VALUE_FUNCTION
| typeof NODE_VALUE_OPERATOR
| typeof NODE_VALUE_PARENTHESIS
| typeof NODE_SELECTOR_LIST
| typeof NODE_SELECTOR_TYPE
| typeof NODE_SELECTOR_CLASS
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export {
NODE_VALUE_COLOR,
NODE_VALUE_FUNCTION,
NODE_VALUE_OPERATOR,
NODE_VALUE_PARENTHESIS,
NODE_SELECTOR_LIST,
NODE_SELECTOR_TYPE,
NODE_SELECTOR_CLASS,
Expand Down
162 changes: 161 additions & 1 deletion src/parse-value.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
NODE_VALUE_COLOR,
NODE_VALUE_FUNCTION,
NODE_VALUE_OPERATOR,
NODE_VALUE_PARENTHESIS,
} from './arena'

describe('ValueParser', () => {
Expand Down Expand Up @@ -215,7 +216,7 @@ describe('ValueParser', () => {
expect(decl?.values[0].children[0].text).toBe('--primary-color')
})

it('should parse url() function', () => {
it('should parse url() function with quoted string', () => {
const parser = new Parser('body { background: url("image.png"); }')
const root = parser.parse()
const rule = root.first_child
Expand All @@ -228,6 +229,90 @@ describe('ValueParser', () => {
expect(decl?.values[0].children[0].type).toBe(NODE_VALUE_STRING)
expect(decl?.values[0].children[0].text).toBe('"image.png"')
})

it('should parse url() function with unquoted URL containing dots', () => {
const parser = new Parser('body { cursor: url(mycursor.cur); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('url')

// URL function should not parse children - content is available via node.value
expect(func?.has_children).toBe(false)
expect(func?.text).toBe('url(mycursor.cur)')
expect(func?.value).toBe('mycursor.cur')
})

it('should parse src() function with unquoted URL', () => {
const parser = new Parser('body { content: src(myfont.woff2); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('src')
expect(func?.has_children).toBe(false)
expect(func?.text).toBe('src(myfont.woff2)')
expect(func?.value).toBe('myfont.woff2')
})

it('should parse url() with base64 data URL', () => {
const parser = new Parser('body { background: url(data:image/png;base64,iVBORw0KGg); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('url')
expect(func?.has_children).toBe(false)
expect(func?.value).toBe('data:image/png;base64,iVBORw0KGg')
})

it('should parse url() with inline SVG', () => {
const parser = new Parser('body { background: url(data:image/svg+xml,<svg></svg>); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('url')
expect(func?.has_children).toBe(false)
expect(func?.value).toBe('data:image/svg+xml,<svg></svg>')
})

it('should provide node.value for other functions like calc()', () => {
const parser = new Parser('body { width: calc(100% - 20px); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('calc')
expect(func?.text).toBe('calc(100% - 20px)')
expect(func?.value).toBe('100% - 20px')
expect(func?.has_children).toBe(true) // calc() parses its children
})

it('should provide node.value for var() function', () => {
const parser = new Parser('body { color: var(--primary-color); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('var')
expect(func?.text).toBe('var(--primary-color)')
expect(func?.value).toBe('--primary-color')
expect(func?.has_children).toBe(true) // var() parses its children
})
})

describe('Complex values', () => {
Expand Down Expand Up @@ -368,4 +453,79 @@ describe('ValueParser', () => {
expect(operators?.[3].text).toBe('-')
})
})

describe('Parentheses', () => {
it('should parse parenthesized expressions in calc()', () => {
const parser = new Parser('body { width: calc((100% - 50px) / 2); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('calc')
expect(func?.children).toHaveLength(3)

// First child should be a parenthesis node
expect(func?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
expect(func?.children[0].text).toBe('(100% - 50px)')

// Check parenthesis content
const parenNode = func?.children[0]
expect(parenNode?.children).toHaveLength(3)
expect(parenNode?.children[0].type).toBe(NODE_VALUE_DIMENSION)
expect(parenNode?.children[0].text).toBe('100%')
expect(parenNode?.children[1].type).toBe(NODE_VALUE_OPERATOR)
expect(parenNode?.children[1].text).toBe('-')
expect(parenNode?.children[2].type).toBe(NODE_VALUE_DIMENSION)
expect(parenNode?.children[2].text).toBe('50px')

// Second child should be division operator
expect(func?.children[1].type).toBe(NODE_VALUE_OPERATOR)
expect(func?.children[1].text).toBe('/')

// Third child should be number
expect(func?.children[2].type).toBe(NODE_VALUE_NUMBER)
expect(func?.children[2].text).toBe('2')
})

it('should parse complex nested parentheses', () => {
const parser = new Parser('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }')
const root = parser.parse()
const rule = root.first_child
const decl = rule?.first_child?.next_sibling?.first_child
const func = decl?.values[0]

expect(func?.type).toBe(NODE_VALUE_FUNCTION)
expect(func?.name).toBe('calc')

// The calc function should have 3 children: parenthesis + operator + parenthesis
expect(func?.children).toHaveLength(3)
expect(func?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
expect(func?.children[0].text).toBe('((100% - var(--x)) / 12 * 6)')
expect(func?.children[1].type).toBe(NODE_VALUE_OPERATOR)
expect(func?.children[1].text).toBe('+')
expect(func?.children[2].type).toBe(NODE_VALUE_PARENTHESIS)
expect(func?.children[2].text).toBe('(-1 * var(--y))')

// Check first parenthesis has nested parenthesis and preserves structure
const firstParen = func?.children[0]
expect(firstParen?.children).toHaveLength(5) // paren + / + 12 + * + 6
expect(firstParen?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
expect(firstParen?.children[0].text).toBe('(100% - var(--x))')

// Check nested parenthesis has function
const nestedParen = firstParen?.children[0]
expect(nestedParen?.children[2].type).toBe(NODE_VALUE_FUNCTION)
expect(nestedParen?.children[2].name).toBe('var')

// Check second parenthesis has content
const secondParen = func?.children[2]
expect(secondParen?.children).toHaveLength(3) // -1 * var(--y)
expect(secondParen?.children[0].type).toBe(NODE_VALUE_NUMBER)
expect(secondParen?.children[0].text).toBe('-1')
expect(secondParen?.children[2].type).toBe(NODE_VALUE_FUNCTION)
expect(secondParen?.children[2].name).toBe('var')
})
})
})
Loading
Loading