Skip to content

Commit 67d50ba

Browse files
authored
fix: improve nested selectors (#20)
1 parent 180a682 commit 67d50ba

File tree

3 files changed

+286
-3
lines changed

3 files changed

+286
-3
lines changed

src/parse-selector.test.ts

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,166 @@ describe('SelectorParser', () => {
10701070
})
10711071
})
10721072

1073+
describe('Relaxed nesting (CSS Nesting Module Level 1)', () => {
1074+
it('should parse selector starting with child combinator', () => {
1075+
const { arena, rootNode, source } = parseSelectorInternal('> a')
1076+
1077+
expect(rootNode).not.toBeNull()
1078+
if (!rootNode) return
1079+
1080+
// Should have one selector
1081+
const selectorWrappers = getChildren(arena, source, rootNode)
1082+
expect(selectorWrappers).toHaveLength(1)
1083+
1084+
// The selector should have 2 children: combinator (>) and type selector (a)
1085+
const selectorWrapper = selectorWrappers[0]
1086+
const children = getChildren(arena, source, selectorWrapper)
1087+
expect(children).toHaveLength(2)
1088+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1089+
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
1090+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1091+
expect(getNodeText(arena, source, children[1])).toBe('a')
1092+
})
1093+
1094+
it('should parse selector starting with next-sibling combinator', () => {
1095+
const { arena, rootNode, source } = parseSelectorInternal('+ div')
1096+
1097+
expect(rootNode).not.toBeNull()
1098+
if (!rootNode) return
1099+
1100+
const selectorWrappers = getChildren(arena, source, rootNode)
1101+
expect(selectorWrappers).toHaveLength(1)
1102+
1103+
const selectorWrapper = selectorWrappers[0]
1104+
const children = getChildren(arena, source, selectorWrapper)
1105+
expect(children).toHaveLength(2)
1106+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1107+
expect(getNodeText(arena, source, children[0]).trim()).toBe('+')
1108+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1109+
expect(getNodeText(arena, source, children[1])).toBe('div')
1110+
})
1111+
1112+
it('should parse selector starting with subsequent-sibling combinator', () => {
1113+
const { arena, rootNode, source } = parseSelectorInternal('~ span')
1114+
1115+
expect(rootNode).not.toBeNull()
1116+
if (!rootNode) return
1117+
1118+
const selectorWrappers = getChildren(arena, source, rootNode)
1119+
expect(selectorWrappers).toHaveLength(1)
1120+
1121+
const selectorWrapper = selectorWrappers[0]
1122+
const children = getChildren(arena, source, selectorWrapper)
1123+
expect(children).toHaveLength(2)
1124+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1125+
expect(getNodeText(arena, source, children[0]).trim()).toBe('~')
1126+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1127+
expect(getNodeText(arena, source, children[1])).toBe('span')
1128+
})
1129+
1130+
it('should parse complex selector after leading combinator', () => {
1131+
const { arena, rootNode, source } = parseSelectorInternal('> a.link#nav[href]:hover')
1132+
1133+
expect(rootNode).not.toBeNull()
1134+
if (!rootNode) return
1135+
1136+
const selectorWrappers = getChildren(arena, source, rootNode)
1137+
expect(selectorWrappers).toHaveLength(1)
1138+
1139+
const selectorWrapper = selectorWrappers[0]
1140+
const children = getChildren(arena, source, selectorWrapper)
1141+
1142+
// Should have: combinator (>), type (a), class (.link), id (#nav), attribute ([href]), pseudo-class (:hover)
1143+
expect(children.length).toBeGreaterThanOrEqual(6)
1144+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1145+
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
1146+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1147+
expect(getNodeText(arena, source, children[1])).toBe('a')
1148+
expect(arena.get_type(children[2])).toBe(NODE_SELECTOR_CLASS)
1149+
expect(getNodeText(arena, source, children[2])).toBe('.link')
1150+
expect(arena.get_type(children[3])).toBe(NODE_SELECTOR_ID)
1151+
expect(getNodeText(arena, source, children[3])).toBe('#nav')
1152+
expect(arena.get_type(children[4])).toBe(NODE_SELECTOR_ATTRIBUTE)
1153+
expect(arena.get_type(children[5])).toBe(NODE_SELECTOR_PSEUDO_CLASS)
1154+
})
1155+
1156+
it('should parse multiple selectors with leading combinators', () => {
1157+
const { arena, rootNode, source } = parseSelectorInternal('> a, ~ span, + div')
1158+
1159+
expect(rootNode).not.toBeNull()
1160+
if (!rootNode) return
1161+
1162+
// Should have three selectors
1163+
const selectorWrappers = getChildren(arena, source, rootNode)
1164+
expect(selectorWrappers).toHaveLength(3)
1165+
1166+
// First selector: > a
1167+
let children = getChildren(arena, source, selectorWrappers[0])
1168+
expect(children).toHaveLength(2)
1169+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1170+
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
1171+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1172+
expect(getNodeText(arena, source, children[1])).toBe('a')
1173+
1174+
// Second selector: ~ span
1175+
children = getChildren(arena, source, selectorWrappers[1])
1176+
expect(children).toHaveLength(2)
1177+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1178+
expect(getNodeText(arena, source, children[0]).trim()).toBe('~')
1179+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1180+
expect(getNodeText(arena, source, children[1])).toBe('span')
1181+
1182+
// Third selector: + div
1183+
children = getChildren(arena, source, selectorWrappers[2])
1184+
expect(children).toHaveLength(2)
1185+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1186+
expect(getNodeText(arena, source, children[0]).trim()).toBe('+')
1187+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1188+
expect(getNodeText(arena, source, children[1])).toBe('div')
1189+
})
1190+
1191+
it('should parse leading combinator with whitespace', () => {
1192+
const { arena, rootNode, source } = parseSelectorInternal('> a')
1193+
1194+
expect(rootNode).not.toBeNull()
1195+
if (!rootNode) return
1196+
1197+
const selectorWrappers = getChildren(arena, source, rootNode)
1198+
expect(selectorWrappers).toHaveLength(1)
1199+
1200+
const selectorWrapper = selectorWrappers[0]
1201+
const children = getChildren(arena, source, selectorWrapper)
1202+
expect(children).toHaveLength(2)
1203+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1204+
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
1205+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1206+
expect(getNodeText(arena, source, children[1])).toBe('a')
1207+
})
1208+
1209+
it('should parse selector with both leading and middle combinators', () => {
1210+
const { arena, rootNode, source } = parseSelectorInternal('> div span')
1211+
1212+
expect(rootNode).not.toBeNull()
1213+
if (!rootNode) return
1214+
1215+
const selectorWrappers = getChildren(arena, source, rootNode)
1216+
expect(selectorWrappers).toHaveLength(1)
1217+
1218+
const selectorWrapper = selectorWrappers[0]
1219+
const children = getChildren(arena, source, selectorWrapper)
1220+
1221+
// Should have: combinator (>), type (div), combinator (descendant), type (span)
1222+
expect(children).toHaveLength(4)
1223+
expect(arena.get_type(children[0])).toBe(NODE_SELECTOR_COMBINATOR)
1224+
expect(getNodeText(arena, source, children[0]).trim()).toBe('>')
1225+
expect(arena.get_type(children[1])).toBe(NODE_SELECTOR_TYPE)
1226+
expect(getNodeText(arena, source, children[1])).toBe('div')
1227+
expect(arena.get_type(children[2])).toBe(NODE_SELECTOR_COMBINATOR)
1228+
expect(arena.get_type(children[3])).toBe(NODE_SELECTOR_TYPE)
1229+
expect(getNodeText(arena, source, children[3])).toBe('span')
1230+
})
1231+
})
1232+
10731233
describe('Edge cases', () => {
10741234
it('should parse selector with multiple spaces', () => {
10751235
const { arena, rootNode, source } = parseSelectorInternal('div p')

src/parse-selector.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class SelectorParser {
8181

8282
// Parse a selector range into selector nodes
8383
// Always returns a NODE_SELECTOR_LIST with selector components as children
84-
parse_selector(start: number, end: number, line: number = 1, column: number = 1, allow_relative: boolean = false): number | null {
84+
parse_selector(start: number, end: number, line: number = 1, column: number = 1, allow_relative: boolean = true): number | null {
8585
this.selector_end = end
8686

8787
// Position lexer at selector start
@@ -95,7 +95,7 @@ export class SelectorParser {
9595
}
9696

9797
// Parse comma-separated selectors
98-
private parse_selector_list(allow_relative: boolean = false): number | null {
98+
private parse_selector_list(allow_relative: boolean = true): number | null {
9999
let selectors: number[] = []
100100
let list_start = this.lexer.pos
101101
let list_line = this.lexer.line
@@ -170,7 +170,8 @@ export class SelectorParser {
170170

171171
// Parse a complex selector (with combinators)
172172
// e.g., "div.class > p + span"
173-
private parse_complex_selector(allow_relative: boolean = false): number | null {
173+
// Also supports CSS Nesting relaxed syntax: "> a", "~ span", etc.
174+
private parse_complex_selector(allow_relative: boolean = true): number | null {
174175
let components: number[] = []
175176

176177
// Skip leading whitespace

src/parse.test.ts

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,128 @@ describe('Parser', () => {
603603

604604
expect(body.type).toBe(NODE_STYLE_RULE)
605605
})
606+
607+
describe('Relaxed nesting (CSS Nesting Module Level 1)', () => {
608+
test('should parse nested rule with leading child combinator', () => {
609+
let source = '.parent { > a { color: red; } }'
610+
let parser = new Parser(source)
611+
let root = parser.parse()
612+
613+
let parent = root.first_child!
614+
expect(parent.type).toBe(NODE_STYLE_RULE)
615+
616+
let [_selector, block] = parent.children
617+
let nested_rule = block.first_child!
618+
expect(nested_rule.type).toBe(NODE_STYLE_RULE)
619+
620+
let nested_selector = nested_rule.first_child!
621+
expect(nested_selector.text).toBe('> a')
622+
// Verify selector has children (was parsed, not left empty)
623+
expect(nested_selector.has_children).toBe(true)
624+
})
625+
626+
test('should parse nested rule with leading next-sibling combinator', () => {
627+
let source = '.parent { + span { color: blue; } }'
628+
let parser = new Parser(source)
629+
let root = parser.parse()
630+
631+
let parent = root.first_child!
632+
let [_selector, block] = parent.children
633+
let nested_rule = block.first_child!
634+
expect(nested_rule.type).toBe(NODE_STYLE_RULE)
635+
636+
let nested_selector = nested_rule.first_child!
637+
expect(nested_selector.text).toBe('+ span')
638+
expect(nested_selector.has_children).toBe(true)
639+
})
640+
641+
test('should parse nested rule with leading subsequent-sibling combinator', () => {
642+
let source = '.parent { ~ div { color: green; } }'
643+
let parser = new Parser(source)
644+
let root = parser.parse()
645+
646+
let parent = root.first_child!
647+
let [_selector, block] = parent.children
648+
let nested_rule = block.first_child!
649+
expect(nested_rule.type).toBe(NODE_STYLE_RULE)
650+
651+
let nested_selector = nested_rule.first_child!
652+
expect(nested_selector.text).toBe('~ div')
653+
expect(nested_selector.has_children).toBe(true)
654+
})
655+
656+
test('should parse multiple nested rules with different leading combinators', () => {
657+
let source = '.parent { > a { color: red; } ~ span { color: blue; } + div { color: green; } }'
658+
let parser = new Parser(source)
659+
let root = parser.parse()
660+
661+
let parent = root.first_child!
662+
let [_selector, block] = parent.children
663+
let [rule1, rule2, rule3] = block.children
664+
665+
expect(rule1.type).toBe(NODE_STYLE_RULE)
666+
expect(rule1.first_child!.text).toBe('> a')
667+
expect(rule1.first_child!.has_children).toBe(true)
668+
669+
expect(rule2.type).toBe(NODE_STYLE_RULE)
670+
expect(rule2.first_child!.text).toBe('~ span')
671+
expect(rule2.first_child!.has_children).toBe(true)
672+
673+
expect(rule3.type).toBe(NODE_STYLE_RULE)
674+
expect(rule3.first_child!.text).toBe('+ div')
675+
expect(rule3.first_child!.has_children).toBe(true)
676+
})
677+
678+
test('should parse complex selector after leading combinator', () => {
679+
let source = '.parent { > a.link#nav[href]:hover { color: red; } }'
680+
let parser = new Parser(source)
681+
let root = parser.parse()
682+
683+
let parent = root.first_child!
684+
let [_selector, block] = parent.children
685+
let nested_rule = block.first_child!
686+
687+
let nested_selector = nested_rule.first_child!
688+
expect(nested_selector.text).toBe('> a.link#nav[href]:hover')
689+
expect(nested_selector.has_children).toBe(true)
690+
})
691+
692+
test('should parse deeply nested rules with leading combinators', () => {
693+
let source = '.a { > .b { > .c { color: red; } } }'
694+
let parser = new Parser(source)
695+
let root = parser.parse()
696+
697+
let a = root.first_child!
698+
let [_selector_a, block_a] = a.children
699+
let b = block_a.first_child!
700+
expect(b.type).toBe(NODE_STYLE_RULE)
701+
expect(b.first_child!.text).toBe('> .b')
702+
expect(b.first_child!.has_children).toBe(true)
703+
704+
let [_selector_b, block_b] = b.children
705+
let c = block_b.first_child!
706+
expect(c.type).toBe(NODE_STYLE_RULE)
707+
expect(c.first_child!.text).toBe('> .c')
708+
expect(c.first_child!.has_children).toBe(true)
709+
})
710+
711+
test('should parse mixed nested rules with and without leading combinators', () => {
712+
let source = '.parent { .normal { } > .combinator { } }'
713+
let parser = new Parser(source)
714+
let root = parser.parse()
715+
716+
let parent = root.first_child!
717+
let [_selector, block] = parent.children
718+
let [normal, combinator] = block.children
719+
720+
expect(normal.type).toBe(NODE_STYLE_RULE)
721+
expect(normal.first_child!.text).toBe('.normal')
722+
723+
expect(combinator.type).toBe(NODE_STYLE_RULE)
724+
expect(combinator.first_child!.text).toBe('> .combinator')
725+
expect(combinator.first_child!.has_children).toBe(true)
726+
})
727+
})
606728
})
607729

608730
describe('@keyframes parsing', () => {

0 commit comments

Comments
 (0)