Skip to content

Commit 2397901

Browse files
committed
Resolve the subject class at the earliest delimiter
LeadingClass checked delimiters in a fixed order (":" first), so a tag or name filter written before a pseudo-class — the conventional order, e.g. "Part.Enemy:not(#Boss)" — cut at the ":" and widened the result to Instance[]. Cutting at every delimiter in turn keeps the prefix before the earliest one, making the order irrelevant. The "[" check is dropped: filters are stripped before LeadingClass runs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01B24Kfw4PGCgm4NQDmUVEMN
1 parent 48f31b6 commit 2397901

2 files changed

Lines changed: 7 additions & 14 deletions

File tree

include/selector.d.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -76,20 +76,9 @@ declare namespace Selector {
7676
// Only the final combinator segment can determine the subject type.
7777
type LastSegment<S extends string> = S extends `${string}>${infer R}` ? LastSegment<R> : S;
7878

79-
type LeadingClass<S extends string> =
80-
Trim<S> extends infer T extends string
81-
? T extends `${infer C}:${string}`
82-
? Trim<C>
83-
: T extends `${infer C}.${string}`
84-
? Trim<C>
85-
: T extends `${infer C}#${string}`
86-
? Trim<C>
87-
: T extends `${infer C}[${string}`
88-
? Trim<C>
89-
: T extends `${infer C} ${string}`
90-
? Trim<C>
91-
: T
92-
: never;
79+
type CutAt<S extends string, D extends string> = S extends `${infer Prefix}${D}${string}` ? Prefix : S;
80+
81+
type LeadingClass<S extends string> = CutAt<CutAt<CutAt<CutAt<Trim<S>, ":">, ".">, "#">, " ">;
9382

9483
// Filters do not affect the subject class, but their values can contain selector separators.
9584
type StripFiltersAndParens<S extends string> = S extends `${infer A}[${string}]${infer B}`

tests/selector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@ const hasPseudo = game.QueryDescendants("Model:has(.Child)"); //=> Model[]
4949
const pseudoChild = game.QueryDescendants("Folder > Part:not(#Excluded)"); //=> Part[]
5050
const pseudoList = game.QueryDescendants("Part:not(.A), Model:has(#B)"); //=> (Part | Model)[]
5151
const pseudoInnerComma = game.QueryDescendants("Part:not(.A, .B, .C)"); //=> Part[]
52+
const tagThenPseudo = game.QueryDescendants("Part.Enemy:not(#Boss)"); //=> Part[]
53+
const nameThenPseudo = game.QueryDescendants("Part#Boss:has(.Weapon)"); //=> Part[]
54+
const tagThenPseudoList = game.QueryDescendants("Part.Enemy:not(.A), Model"); //=> (Part | Model)[]
55+
const pseudoThenTag = game.QueryDescendants("Part:not(.Rotten).Fruit"); //=> Part[]
5256
const pseudoQuotedInnerComma = game.QueryDescendants("Part:has(Model[Name = 'a,b'], Folder)"); //=> Part[]
5357
const pseudoNestedList = game.QueryDescendants("Part:has(Model:has([Name = 'a,b']), Folder), TextButton"); //=> (Part | TextButton)[]
5458
const pseudoTrailingCombinator = game.QueryDescendants("Part:has(Model:has(.x)) >"); //=> Instance[]

0 commit comments

Comments
 (0)