Skip to content

Commit 180a682

Browse files
authored
add parenthesis node && make contents of url() available via node.value (#19)
1 parent 0a9f6cd commit 180a682

File tree

6 files changed

+303
-2
lines changed

6 files changed

+303
-2
lines changed

src/arena.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export const NODE_VALUE_STRING = 13 // quoted string: "hello", 'world'
3939
export const NODE_VALUE_COLOR = 14 // hex color: #fff, #ff0000
4040
export const NODE_VALUE_FUNCTION = 15 // function: calc(), var(), url()
4141
export const NODE_VALUE_OPERATOR = 16 // operator: +, -, *, /, comma
42+
export const NODE_VALUE_PARENTHESIS = 17 // parenthesized expression: (100% - 50px)
4243

4344
// Selector node type constants (for detailed selector parsing)
4445
export const NODE_SELECTOR_LIST = 20 // comma-separated selectors

src/css-node.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
NODE_VALUE_COLOR,
1616
NODE_VALUE_FUNCTION,
1717
NODE_VALUE_OPERATOR,
18+
NODE_VALUE_PARENTHESIS,
1819
NODE_SELECTOR_LIST,
1920
NODE_SELECTOR_TYPE,
2021
NODE_SELECTOR_CLASS,
@@ -66,6 +67,7 @@ export type CSSNodeType =
6667
| typeof NODE_VALUE_COLOR
6768
| typeof NODE_VALUE_FUNCTION
6869
| typeof NODE_VALUE_OPERATOR
70+
| typeof NODE_VALUE_PARENTHESIS
6971
| typeof NODE_SELECTOR_LIST
7072
| typeof NODE_SELECTOR_TYPE
7173
| typeof NODE_SELECTOR_CLASS

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ export {
4343
NODE_VALUE_COLOR,
4444
NODE_VALUE_FUNCTION,
4545
NODE_VALUE_OPERATOR,
46+
NODE_VALUE_PARENTHESIS,
4647
NODE_SELECTOR_LIST,
4748
NODE_SELECTOR_TYPE,
4849
NODE_SELECTOR_CLASS,

src/parse-value.test.ts

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
NODE_VALUE_COLOR,
99
NODE_VALUE_FUNCTION,
1010
NODE_VALUE_OPERATOR,
11+
NODE_VALUE_PARENTHESIS,
1112
} from './arena'
1213

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

218-
it('should parse url() function', () => {
219+
it('should parse url() function with quoted string', () => {
219220
const parser = new Parser('body { background: url("image.png"); }')
220221
const root = parser.parse()
221222
const rule = root.first_child
@@ -228,6 +229,90 @@ describe('ValueParser', () => {
228229
expect(decl?.values[0].children[0].type).toBe(NODE_VALUE_STRING)
229230
expect(decl?.values[0].children[0].text).toBe('"image.png"')
230231
})
232+
233+
it('should parse url() function with unquoted URL containing dots', () => {
234+
const parser = new Parser('body { cursor: url(mycursor.cur); }')
235+
const root = parser.parse()
236+
const rule = root.first_child
237+
const decl = rule?.first_child?.next_sibling?.first_child
238+
const func = decl?.values[0]
239+
240+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
241+
expect(func?.name).toBe('url')
242+
243+
// URL function should not parse children - content is available via node.value
244+
expect(func?.has_children).toBe(false)
245+
expect(func?.text).toBe('url(mycursor.cur)')
246+
expect(func?.value).toBe('mycursor.cur')
247+
})
248+
249+
it('should parse src() function with unquoted URL', () => {
250+
const parser = new Parser('body { content: src(myfont.woff2); }')
251+
const root = parser.parse()
252+
const rule = root.first_child
253+
const decl = rule?.first_child?.next_sibling?.first_child
254+
const func = decl?.values[0]
255+
256+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
257+
expect(func?.name).toBe('src')
258+
expect(func?.has_children).toBe(false)
259+
expect(func?.text).toBe('src(myfont.woff2)')
260+
expect(func?.value).toBe('myfont.woff2')
261+
})
262+
263+
it('should parse url() with base64 data URL', () => {
264+
const parser = new Parser('body { background: url(data:image/png;base64,iVBORw0KGg); }')
265+
const root = parser.parse()
266+
const rule = root.first_child
267+
const decl = rule?.first_child?.next_sibling?.first_child
268+
const func = decl?.values[0]
269+
270+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
271+
expect(func?.name).toBe('url')
272+
expect(func?.has_children).toBe(false)
273+
expect(func?.value).toBe('data:image/png;base64,iVBORw0KGg')
274+
})
275+
276+
it('should parse url() with inline SVG', () => {
277+
const parser = new Parser('body { background: url(data:image/svg+xml,<svg></svg>); }')
278+
const root = parser.parse()
279+
const rule = root.first_child
280+
const decl = rule?.first_child?.next_sibling?.first_child
281+
const func = decl?.values[0]
282+
283+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
284+
expect(func?.name).toBe('url')
285+
expect(func?.has_children).toBe(false)
286+
expect(func?.value).toBe('data:image/svg+xml,<svg></svg>')
287+
})
288+
289+
it('should provide node.value for other functions like calc()', () => {
290+
const parser = new Parser('body { width: calc(100% - 20px); }')
291+
const root = parser.parse()
292+
const rule = root.first_child
293+
const decl = rule?.first_child?.next_sibling?.first_child
294+
const func = decl?.values[0]
295+
296+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
297+
expect(func?.name).toBe('calc')
298+
expect(func?.text).toBe('calc(100% - 20px)')
299+
expect(func?.value).toBe('100% - 20px')
300+
expect(func?.has_children).toBe(true) // calc() parses its children
301+
})
302+
303+
it('should provide node.value for var() function', () => {
304+
const parser = new Parser('body { color: var(--primary-color); }')
305+
const root = parser.parse()
306+
const rule = root.first_child
307+
const decl = rule?.first_child?.next_sibling?.first_child
308+
const func = decl?.values[0]
309+
310+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
311+
expect(func?.name).toBe('var')
312+
expect(func?.text).toBe('var(--primary-color)')
313+
expect(func?.value).toBe('--primary-color')
314+
expect(func?.has_children).toBe(true) // var() parses its children
315+
})
231316
})
232317

233318
describe('Complex values', () => {
@@ -368,4 +453,79 @@ describe('ValueParser', () => {
368453
expect(operators?.[3].text).toBe('-')
369454
})
370455
})
456+
457+
describe('Parentheses', () => {
458+
it('should parse parenthesized expressions in calc()', () => {
459+
const parser = new Parser('body { width: calc((100% - 50px) / 2); }')
460+
const root = parser.parse()
461+
const rule = root.first_child
462+
const decl = rule?.first_child?.next_sibling?.first_child
463+
const func = decl?.values[0]
464+
465+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
466+
expect(func?.name).toBe('calc')
467+
expect(func?.children).toHaveLength(3)
468+
469+
// First child should be a parenthesis node
470+
expect(func?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
471+
expect(func?.children[0].text).toBe('(100% - 50px)')
472+
473+
// Check parenthesis content
474+
const parenNode = func?.children[0]
475+
expect(parenNode?.children).toHaveLength(3)
476+
expect(parenNode?.children[0].type).toBe(NODE_VALUE_DIMENSION)
477+
expect(parenNode?.children[0].text).toBe('100%')
478+
expect(parenNode?.children[1].type).toBe(NODE_VALUE_OPERATOR)
479+
expect(parenNode?.children[1].text).toBe('-')
480+
expect(parenNode?.children[2].type).toBe(NODE_VALUE_DIMENSION)
481+
expect(parenNode?.children[2].text).toBe('50px')
482+
483+
// Second child should be division operator
484+
expect(func?.children[1].type).toBe(NODE_VALUE_OPERATOR)
485+
expect(func?.children[1].text).toBe('/')
486+
487+
// Third child should be number
488+
expect(func?.children[2].type).toBe(NODE_VALUE_NUMBER)
489+
expect(func?.children[2].text).toBe('2')
490+
})
491+
492+
it('should parse complex nested parentheses', () => {
493+
const parser = new Parser('body { width: calc(((100% - var(--x)) / 12 * 6) + (-1 * var(--y))); }')
494+
const root = parser.parse()
495+
const rule = root.first_child
496+
const decl = rule?.first_child?.next_sibling?.first_child
497+
const func = decl?.values[0]
498+
499+
expect(func?.type).toBe(NODE_VALUE_FUNCTION)
500+
expect(func?.name).toBe('calc')
501+
502+
// The calc function should have 3 children: parenthesis + operator + parenthesis
503+
expect(func?.children).toHaveLength(3)
504+
expect(func?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
505+
expect(func?.children[0].text).toBe('((100% - var(--x)) / 12 * 6)')
506+
expect(func?.children[1].type).toBe(NODE_VALUE_OPERATOR)
507+
expect(func?.children[1].text).toBe('+')
508+
expect(func?.children[2].type).toBe(NODE_VALUE_PARENTHESIS)
509+
expect(func?.children[2].text).toBe('(-1 * var(--y))')
510+
511+
// Check first parenthesis has nested parenthesis and preserves structure
512+
const firstParen = func?.children[0]
513+
expect(firstParen?.children).toHaveLength(5) // paren + / + 12 + * + 6
514+
expect(firstParen?.children[0].type).toBe(NODE_VALUE_PARENTHESIS)
515+
expect(firstParen?.children[0].text).toBe('(100% - var(--x))')
516+
517+
// Check nested parenthesis has function
518+
const nestedParen = firstParen?.children[0]
519+
expect(nestedParen?.children[2].type).toBe(NODE_VALUE_FUNCTION)
520+
expect(nestedParen?.children[2].name).toBe('var')
521+
522+
// Check second parenthesis has content
523+
const secondParen = func?.children[2]
524+
expect(secondParen?.children).toHaveLength(3) // -1 * var(--y)
525+
expect(secondParen?.children[0].type).toBe(NODE_VALUE_NUMBER)
526+
expect(secondParen?.children[0].text).toBe('-1')
527+
expect(secondParen?.children[2].type).toBe(NODE_VALUE_FUNCTION)
528+
expect(secondParen?.children[2].name).toBe('var')
529+
})
530+
})
371531
})

0 commit comments

Comments
 (0)