import { describe, expect, it } from 'vitest'
import { parse, toCss } from './selector-parser'
import { walk, WalkAction } from './walk'
describe('parse', () => {
it('should parse a simple selector', () => {
expect(parse('.foo')).toEqual([{ kind: 'selector', value: '.foo' }])
})
it('should parse a compound selector', () => {
expect(parse('.foo.bar:hover#id')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
{ kind: 'selector', value: ':hover' },
{ kind: 'selector', value: '#id' },
],
},
])
})
it('should parse a pseudo-element selector with double ::', () => {
expect(parse('.foo::before')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '::before' },
],
},
])
})
it('should parse a selector list', () => {
expect(parse('.foo,.bar')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
],
},
])
})
it('should safely parse an invalid selector list', () => {
expect(parse('.foo,')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'compound', nodes: [] },
],
},
])
expect(parse(',.foo')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'compound', nodes: [] },
{ kind: 'selector', value: '.foo' },
],
},
])
expect(parse(',.foo,')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'compound', nodes: [] },
{ kind: 'selector', value: '.foo' },
{ kind: 'compound', nodes: [] },
],
},
])
expect(parse('.foo,,.bar')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'compound', nodes: [] },
{ kind: 'selector', value: '.bar' },
],
},
])
})
it('should parse selector lists with whitespace around the comma', () => {
expect(parse('.foo, .bar')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
],
},
])
expect(parse('.foo , .bar')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
],
},
])
expect(parse('.foo ,.bar')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
],
},
])
})
it('should parse a list of attribute selectors', () => {
expect(parse('[foo],[bar]')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '[foo]' },
{ kind: 'selector', value: '[bar]' },
],
},
])
})
it('should parse a list of selectors with just functions', () => {
expect(parse(':is(.a),:is(.b)')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'function', value: ':is', nodes: [{ kind: 'selector', value: '.a' }] },
{ kind: 'function', value: ':is', nodes: [{ kind: 'selector', value: '.b' }] },
],
},
])
})
it('should parse selector lists with more than two selectors', () => {
expect(parse('.foo,.bar,.baz')).toEqual([
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
{ kind: 'selector', value: '.baz' },
],
},
])
})
it('should group selectors with combinators in a selector list', () => {
expect(parse('.a+.b, .c .d[attr], .e')).toEqual([
{
kind: 'list',
nodes: [
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.a' },
{ kind: 'combinator', value: '+' },
{ kind: 'selector', value: '.b' },
],
},
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.c' },
{ kind: 'combinator', value: ' ' },
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.d' },
{ kind: 'selector', value: '[attr]' },
],
},
],
},
{ kind: 'selector', value: '.e' },
],
},
])
})
it('should parse complex selectors in a selector list', () => {
expect(parse('#a.b > .c, .d')).toEqual([
{
kind: 'list',
nodes: [
{
kind: 'complex',
nodes: [
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '#a' },
{ kind: 'selector', value: '.b' },
],
},
{ kind: 'combinator', value: '>' },
{ kind: 'selector', value: '.c' },
],
},
{ kind: 'selector', value: '.d' },
],
},
])
})
it('should combine everything within attribute selectors', () => {
expect(parse('.foo[bar="baz"]')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '[bar="baz"]' },
],
},
])
})
it('should parse functions', () => {
expect(parse('.foo:hover:not(.bar:focus)')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: ':hover' },
{
kind: 'function',
value: ':not',
nodes: [
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.bar' },
{ kind: 'selector', value: ':focus' },
],
},
],
},
],
},
])
})
it('should parse selector lists in functions', () => {
expect(parse(':is(.foo, .bar)')).toEqual([
{
kind: 'function',
value: ':is',
nodes: [
{
kind: 'list',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '.bar' },
],
},
],
},
])
})
it('should handle next-children combinator', () => {
expect(parse('.foo + p')).toEqual([
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'combinator', value: '+' },
{ kind: 'selector', value: 'p' },
],
},
])
})
it('should normalize combinators', () => {
expect(parse('.foo + .bar')).toEqual([
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'combinator', value: '+' },
{ kind: 'selector', value: '.bar' },
],
},
])
expect(parse('.foo \n\t .bar')).toEqual([
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'combinator', value: ' ' },
{ kind: 'selector', value: '.bar' },
],
},
])
})
it('should handle escaped characters', () => {
expect(parse('foo\\.bar')).toEqual([{ kind: 'selector', value: 'foo\\.bar' }])
})
it('parses :nth-child()', () => {
expect(parse(':nth-child(n+1)')).toEqual([
{
kind: 'function',
value: ':nth-child',
nodes: [
{
kind: 'value',
value: 'n+1',
},
],
},
])
})
it('parses &:has(.child:nth-child(2))', () => {
expect(parse('&:has(.child:nth-child(2))')).toEqual([
{
kind: 'compound',
nodes: [
{
kind: 'selector',
value: '&',
},
{
kind: 'function',
value: ':has',
nodes: [
{
kind: 'compound',
nodes: [
{
kind: 'selector',
value: '.child',
},
{
kind: 'function',
value: ':nth-child',
nodes: [
{
kind: 'value',
value: '2',
},
],
},
],
},
],
},
],
},
])
})
it('parses &:has(:nth-child(2))', () => {
expect(parse('&:has(:nth-child(2))')).toEqual([
{
kind: 'compound',
nodes: [
{
kind: 'selector',
value: '&',
},
{
kind: 'function',
value: ':has',
nodes: [
{
kind: 'function',
value: ':nth-child',
nodes: [
{
kind: 'value',
value: '2',
},
],
},
],
},
],
},
])
})
it('parses nesting selector before attribute selector', () => {
expect(parse('&[data-foo]')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '&' },
{ kind: 'selector', value: '[data-foo]' },
],
},
])
})
it('parses nesting selector after an attribute selector', () => {
expect(parse('[data-foo]&')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: '&' },
],
},
])
})
it('parses universal selector before attribute selector', () => {
expect(parse('*[data-foo]')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '*' },
{ kind: 'selector', value: '[data-foo]' },
],
},
])
})
it('parses universal selector after an attribute selector', () => {
expect(parse('[data-foo]*')).toEqual([
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '[data-foo]' },
{ kind: 'selector', value: '*' },
],
},
])
})
it('should parse selector lists as real selectors', () => {
expect(parse('.foo[attr], .bar#id, .baz + .qux')).toEqual([
{
kind: 'list',
nodes: [
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.foo' },
{ kind: 'selector', value: '[attr]' },
],
},
{
kind: 'compound',
nodes: [
{ kind: 'selector', value: '.bar' },
{ kind: 'selector', value: '#id' },
],
},
{
kind: 'complex',
nodes: [
{ kind: 'selector', value: '.baz' },
{ kind: 'combinator', value: '+' },
{ kind: 'selector', value: '.qux' },
],
},
],
},
])
})
})
describe('toCss', () => {
it('should print a simple selector', () => {
expect(toCss(parse('.foo'))).toBe('.foo')
})
it('should print a compound selector', () => {
expect(toCss(parse('.foo.bar:hover#id'))).toBe('.foo.bar:hover#id')
})
it('should print a selector list', () => {
expect(toCss(parse('.foo, .bar'))).toBe('.foo, .bar')
})
it('should print a selector list with normalized commas', () => {
expect(toCss(parse('.foo,.bar'))).toBe('.foo, .bar')
expect(toCss(parse('.foo, .bar'))).toBe('.foo, .bar')
expect(toCss(parse('.foo , .bar'))).toBe('.foo, .bar')
expect(toCss(parse('.foo ,.bar'))).toBe('.foo, .bar')
})
it('should print a selector list but minimized', () => {
expect(toCss(parse('.foo, .bar'), true)).toBe('.foo,.bar')
expect(toCss(parse('.foo , .bar'), true)).toBe('.foo,.bar')
expect(toCss(parse('.foo ,.bar'), true)).toBe('.foo,.bar')
})
it('should print an attribute selectors', () => {
expect(toCss(parse('.foo[bar="baz"]'))).toBe('.foo[bar="baz"]')
})
it('should print a function', () => {
expect(toCss(parse('.foo:hover:not(.bar:focus)'))).toBe('.foo:hover:not(.bar:focus)')
})
it('should print escaped characters', () => {
expect(toCss(parse('foo\\.bar'))).toBe('foo\\.bar')
})
it('should print :nth-child()', () => {
expect(toCss(parse(':nth-child(n+1)'))).toBe(':nth-child(n+1)')
})
it('should pretty print a complex selector', () => {
expect(toCss(parse('.a+.b, .c .d[attr], .e'))).toBe('.a + .b, .c .d[attr], .e')
})
it('should minify a complex selector during printing', () => {
expect(toCss(parse('.a+.b, .c .d[attr], .e'), true)).toBe('.a+.b,.c .d[attr],.e')
})
})
describe('walk', () => {
it('can be used to replace a function call', () => {
const ast = parse('.foo:hover:not(.bar:focus)')
walk(ast, (node) => {
if (node.kind === 'function' && node.value === ':not') {
return WalkAction.Replace({ kind: 'selector', value: '.inverted-bar' } as const)
}
})
expect(toCss(ast)).toBe('.foo:hover.inverted-bar')
})
})