@@ -5,81 +5,72 @@ export interface Selector {
55}
66
77export function toSelector ( selectorString : string ) : Selector {
8- // We parse the selector back-to-front, so that the front-most selectors end up at the bottom of the tree we create
9- // and therefore get resolved first.
108 if ( selectorString === '' ) {
119 // ERROR CASE: The selector is empty. Possible if you do something like "()" or "a:()".
1210 throw new Error ( getMessage ( "SelectorCannotBeEmpty" ) ) ;
13- } else if ( selectorString . endsWith ( ')' ) ) {
14- // If the selector ends in close-paren, then we need to find the open-paren that matches it.
15- const correspondingOpenParen : number = identifyCorrespondingOpenParen ( selectorString ) ;
16- if ( correspondingOpenParen === 0 ) {
17- // RECURSIVE CASE: The entire selector is wrapped in parens. Pop them off and call recursively.
18- return toSelector ( selectorString . slice ( 1 , - 1 ) )
19- } else {
20- // RECURSIVE CASE: The open-paren is somewhere in the middle of the selector and accompanied by an operator.
21- const left : string = selectorString . slice ( 0 , correspondingOpenParen - 1 ) ;
22- const right : string = selectorString . slice ( correspondingOpenParen ) ;
23- const op : string = selectorString [ correspondingOpenParen - 1 ] ;
24- return toComplexSelector ( left , right , op ) ;
25- }
26- } else {
27- // If there's a close-paren in the string, only look for operators after it.
28- const lastCloseParen : number = Math . max ( selectorString . lastIndexOf ( ')' ) , 0 ) ;
29- const lastComma : number = selectorString . slice ( lastCloseParen ) . lastIndexOf ( ',' ) ;
30- const lastColon : number = selectorString . slice ( lastCloseParen ) . lastIndexOf ( ':' ) ;
31-
32- // BASE CASE: The selector contains no commas or colons.
33- if ( lastComma === - 1 && lastColon === - 1 ) {
34- // Parens only make sense in conjunction with operators, so if we find any, the selector is malformed.
35- if ( selectorString . includes ( ')' ) || selectorString . includes ( '(' ) ) {
36- throw new Error ( getMessage ( 'SelectorLooksIncorrect' , selectorString ) ) ;
37- }
38- return new SimpleSelector ( selectorString ) ;
39- } else if ( lastComma !== - 1 ) {
40- // Commas resolve before colons, so that "x,a:b" and "a:b,x" both resolve equivalently the combination of
41- // "x" and "a:b".
42- const left : string = selectorString . slice ( 0 , lastComma + lastCloseParen ) ;
43- const right : string = selectorString . slice ( lastComma + lastCloseParen + 1 ) ;
44- return toComplexSelector ( left , right , ',' ) ;
45- } else {
46- const left : string = selectorString . slice ( 0 , lastColon + lastCloseParen ) ;
47- const right : string = selectorString . slice ( lastColon + lastCloseParen + 1 ) ;
48- return toComplexSelector ( left , right , ':' ) ;
49- }
5011 }
51- }
5212
53- function identifyCorrespondingOpenParen ( selectorString : string ) : number {
54- const reversedLetters : string [ ] = selectorString . split ( '' ) . reverse ( ) ;
13+ let commaIdx : number | null = null ;
14+ let colonIdx : number | null = null ;
5515 let parenBalance : number = 0 ;
56- let idx = 0 ;
57- for ( const letter of reversedLetters ) {
58- if ( letter === ')' ) {
16+ let hasParens : boolean = false ;
17+ for ( let i = 0 ; i < selectorString . length ; i ++ ) {
18+ const char : string = selectorString [ i ] ;
19+ if ( ( i === 0 || i === selectorString . length - 1 ) && ( char === ',' || char === ':' ) ) {
20+ // ERROR CASE: The selector cannot start or end with a binary operator, because that's nonsense.
21+ throw new Error ( getMessage ( "SelectorStartsOrEndsWithOperator" , selectorString ) ) ;
22+ } else if ( char === ' ' ) {
23+ // ERROR CASE: The selector contains whitespace.
24+ throw new Error ( getMessage ( "SelectorLooksIncorrect" , selectorString ) ) ;
25+ } else if ( char === '(' ) {
5926 parenBalance += 1 ;
60- } else if ( letter === '(' ) {
27+ hasParens = true ;
28+ } else if ( char === ')' ) {
6129 parenBalance -= 1 ;
30+ hasParens = true ;
31+ // If our parenthesis balance is negative, it means there are more close-parens than open-parens, which is a problem.
32+ if ( parenBalance < 0 ) {
33+ throw new Error ( getMessage ( "SelectorLooksIncorrect" , selectorString ) ) ;
34+ }
35+ } else if ( char === ',' ) {
36+ // If we're not inside of parentheses, and we haven't already found a comma, note the location of this one.
37+ if ( parenBalance === 0 ) {
38+ commaIdx = i ;
39+ // Commas trump everything else, so we can just break.
40+ break ;
41+ }
42+ } else if ( char === ':' ) {
43+ // If we're not inside of parentheses, and we haven't already found a colon, note the location of this one.
44+ if ( parenBalance === 0 && colonIdx === null ) {
45+ colonIdx = i ;
46+ }
6247 }
63- if ( parenBalance === 0 ) {
64- break ;
65- }
66- idx += 1 ;
6748 }
6849
50+ // If our final parenthesis balance is negative, it means there are more open-parens than close-parens, which is a problem.
6951 if ( parenBalance > 0 ) {
7052 throw new Error ( getMessage ( "SelectorLooksIncorrect" , selectorString ) ) ;
7153 }
7254
73- return selectorString . length - idx - 1 ;
74- }
75-
76- function toComplexSelector ( left : string , right : string , op : string ) : Selector {
77- if ( op === ',' ) {
55+ // Commas trump colons, so if we have a comma, split along that.
56+ if ( commaIdx != null ) {
57+ const left : string = selectorString . slice ( 0 , commaIdx ) ;
58+ const right : string = selectorString . slice ( commaIdx + 1 ) ;
7859 return new OrSelector ( toSelector ( left ) , toSelector ( right ) ) ;
79- } else if ( op === ':' ) {
60+ } else if ( colonIdx != null ) {
61+ // If there are colons but no commas, split along the first colon.
62+ const left : string = selectorString . slice ( 0 , colonIdx ) ;
63+ const right : string = selectorString . slice ( colonIdx + 1 ) ;
8064 return new AndSelector ( toSelector ( left ) , toSelector ( right ) ) ;
65+ } else if ( selectorString [ 0 ] === '(' && selectorString [ selectorString . length - 1 ] === ')' ) {
66+ // If the first and last character are parentheses, then pop those off and run again.
67+ return toSelector ( selectorString . slice ( 1 , selectorString . length - 1 ) ) ;
68+ } else if ( hasParens ) {
69+ // There shouldn't be parentheses in the middle of a selector that has no operators.
70+ throw new Error ( getMessage ( 'SelectorLooksIncorrect' , selectorString ) ) ;
8171 } else {
82- throw new Error ( getMessage ( "SelectorLooksIncorrect" , `${ left } ${ op } ${ right } ` ) ) ;
72+ // A string with no operators or problems is just a simple string-selector.
73+ return new SimpleSelector ( selectorString ) ;
8374 }
8475}
8576
0 commit comments